Git Product home page Git Product logo

quickbase-client's Introduction

Quickbase-Client

A High-Level Quickbase Python API Client & Model Generator

Pipeline Status

Coverage Report

Documentation Status

PyPI

Black Code Style

Quickbase-Client is a library for interacting with Quickbase applications through their RESTful JSON API (https://developer.quickbase.com/). It has features to generate model classes for tables in your Quickbase app, and provides high level classes to interface between Python objects and the Quickbase tables.

Quick Start

Installation

Installation can be done through pip:

pip install quickbase-client

This will install both the library quickbase_client, and a command line tool qbc for running some handy scripts.

Generating your Models

To interact and authenticate with your Quickbase applications you need a User Token. You can read the Quickbase documentation here on how to create one. It is recommended to set an environment variable QB_USER_TOKEN with this value:

export QB_USER_TOKEN=mytokenfromquickbase;

Next, say you have a hypothetical Quickbase Application named MyApp at https://foo.quickbase.com/db/abcdef that has tables for tracking things against a repository like Issues & Pipelines.

Example Table

Running the following:

qbc run model-generate -a https://foo.quickbase.com/db/abcdef

Would generate a directory structure like

models
├── __init__.py
└── my_app
    ├── __init__.py
    ├── app.py
    ├── github_issue.py
    └── gitlab_pipeline.py

And classes like GitHubIssue where you can interact with the data model through a Python object.

Writing Records to Quickbase

Classes like GitHubIssue that subclass QuickbaseTable also get a factory class-method client(user_tok) which creates an instance of the higher-level QuickbaseTableClient to make API requests for things related to that table:

client = GitHubIssue.client(user_tok=os.environ['QB_USER_TOKEN'])
new_issue = GitHubIssue(
    title='Something broke',   # you get friendly-kwargs for fields without worrying about ID's
    description='Please fix!',
    date_opened=date.today()   # things like Python date objects will be serialized
)
response = client.add_record(new_issue)
print(response.json())  # all methods (except for query) return the requests Response object

Querying Records from Quickbase

You can also use the client object to send queries to the Quickbase API through the query method. This method will serialize the data back in to a Python object. The query method on the table class takes a QuickbaseQuery object which is high level wrapper around the parameters needed to make a query.

Notably, the where parameter for specifying the query string. There is one (and in the future there will be more) implementation of this which allows you to build query-strings through higher-level python functions.

You can use the methods exposed in the quickbase_client.query module like so:

# convention to append an underscore to these methods to avoid clashing
# with any python keywords
from quickbase_client.query import on_or_before_
from quickbase_client.query import eq_
from quickbase_client.query import and_

schema = GitHubIssue.schema
q = and_(
    eq_(schema.date_opened, schema.date_created),
    on_or_before_(schema.date_closed, date(2020, 11, 16))
)
print(q.where)  # ({'9'.EX.'_FID_1'}AND{'10'.OBF.'11-16-2020'})
recs = client.query(q)  # recs will be GitHubIssue objects unless passing raw=True
print([str(r) for r in recs])  # ['<GitHubIssue title="Made And Closed Today" id="10000">']

Controlling Lower-Level API Calls

Lastly, say you want to deal with just posting the specific json/data Quickbase is looking for. The QuickbaseTableClient object wraps the lower-level QuickbaseApiClient object which has methods for just sending the actual data (with an even lower-level utility QuickbaseRequestFactory you could also use). These classes manage hanging on to the user token, and the realm hostname, etc. for each request that is made.

For example, note the signature of query in QuickbaseApiClient:

def query(self, table_id, fields_to_select=None, where_str=None,
          sort_by=None, group_by=None, options=None):

You can get to this class by going through the table client: api = client.api, or from instantiating it directly api = QuickbaseApiClient(my_user_token, my_realm)

With this, we could make the exact same request as before:

api = QuickbaseApiClient(user_token='my_token', realm_hostname='foo.quickbase.com')
response = api.query(
    table_id='abcdef',
    where_str="({'9'.EX.'_FID_1'}AND{'10'.OBF.'11-16-2020'})")
data = response.json()

More Resources

Other Notes

Currently a bunch of duplicate aliases for QuickBase to Quickbase since this was originally released with everything prefixed as QuickBase-. But since Quickbase is branding more to "Quickbase", this will eventually be the main naming for version 1.0 in an effort to keep more consistent. So prefer to use Quickbase- prefixed classes as in the future the other aliases will be dropped.

quickbase-client's People

Contributors

mklaber avatar sanelson avatar tkutcher avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

quickbase-client's Issues

Handle composite fields

Currently for things like address, you'd still have to work with the composite fields in bulk with the lower level json.

Support updating for files generated from model-generate

Currently, to get any updates to a QuickBase table in the python classes you'd have to re-run model-generate which would overwrite the entire file.

Current workaround to not lose code added to generated models files is to refactor (methods, etc.) in to a separate module and you can pass the relevant QuickBaseTable as needed.

Another issue is if a field label changes then the class attribute would get overwritten potentially breaking code.

Could introduce an update command which instead of straight writing to a file, reads from the file it is going to update and only inserts new fields/reports/etc. Fixes first issue, and second issue could be addressed by just having both fields there in the class then user can manually delete the one they don't want + fix referring code.

Better interface for specifying default fields when querying records

Would like to have a little more explicit behavior in what fields to ask to get returned from Quickbase. Currently comes from the select property of the actual QuickBaseQuery object. Ideally, the client, or the table object itself, specifies what the default fields to return are.

Fix serialization

(Temporary?) solution to serialize data is to encode it as a string using custom encoder and then decode it back to json since didn't seem to be happy with the string that was posted.

Handle pagination

Currently user would have to manually handle pagination to make the next request, the client classes should have this built in. Especially for query where raw is False, the secondary requests should be (optionally) made before returning.

Release v0.2

  • Update pyproject.toml
  • Update CHANGELOG.rst
  • Branch & Tag
  • Publish

eq_ case sensitive?

Hello,
I am running this very simple query:

            client_client = Client.client(TOKEN)
            client_schema = Client.schema
            q = eq_(client_schema.client_name, client_name)
            recs = client_client.query(q)

There is a client in the Client table called "John Davis Llc".
When I search for "JOHN DAVIS LLC" I get zero results.
When I search for "John Davis Llc" I get one result.
Is the case sensitivity introduced by "eq_" or is it already there in the "EX" Quickbase API?

FYI, the q.where for the first case is:

"{'6'.EX.'JOHN DAVIS LLC'}"

for the second case is:

"{'6'.EX.'John Davis Llc'}"

Multi-line strings for formulas in generated model classes

Formulas with newline characters are output on multiple lines when running qbc run model-generate, but they are not enclosed in multiline strings, thus making them invalid python files. Should just need to enclose them in triple quoted strings.

Also while dealing with styling, add an extra newline before the class declaration.

Bug: can't clear values on update

Describe the bug

When updating a record, I want to be able to clear out an existing value without clearing out any value not explicitly set. As it stands, the serialisation step omits any fields that have been explicitly set to None.

To Reproduce

Let's assume you have a GitHubIssue that has already been added to QB. And it has Record ID# 5. When you inserted it in to QB, it was labeled with bug. Now, the GH user is not so sure and removes all labels.

issue = GitHubIssue(recordid=5, labels=None)
client = GitHubIssue.client(os.environ['QB_USER_TOKEN'])

response = client.add_records(issues, merge_field_id=GitHubIssue.recordid.fid)

Expected behavior

I would expect our labels field to be empty and our title, description, etc. to be populated as they were before.

Screenshots

n/a

Additional context

I believe the issue is that the RecordJsonSeralizer is checking for None here and therefore excluding it from the request that's sent to QB. The solution is likely somehow tracking what attributes of a QuickBaseTable get set (regardless of the value they are set to).

General Housekeeping & Prepare Naming Changes

  • Update dependencies documentation to be a little more inclusive for API methods.
  • Deprecate anything named QuickBase in favor of future name Quickbase (once 1.0 will force the new name)

Set Up CI

CI for running pytest (with coverage), flake8

Adopt black coding style

Update flake8 configuration to be consistent with black formatting and include black for auto formatting in development.

Add support for making requests to the old XML API

Useful for a few things the new JSON API doesn't support:

  • Changing record owner
  • Setting formula fields (I think)

And probably a few more.

Should provide a general way to make requests to the XML API, and then integrate core ones that supplement the JSON API to the main client

Fix docs

Broke at some point, not sure when...

Delete items

Is your feature request related to a problem? Please describe.
I may be misreading the documentation, but cannot find a simple way to delete items in Quickbase. Do I need to use the legacy API? If so, can you send me an example, please?

Describe the solution you'd like
It would be nice to have deletion added to the main API

Cannot get the record ID

Describe the bug
My code is very simple

project_client = Project.client(TOKEN)
recs = project_client.query()
for r in recs:
    print(r.recordid)

the recordid is always null

The class generated by qbc contains the recordid defined as a number:

class Project(QuickBaseTable):
    __dbid__ = 'xxxxxxxxx'
    __app__ = ItsmanagementDashboard
    
    date_created = QuickBaseField(fid=1, field_type=Qb.DATETIME)
    date_modified = QuickBaseField(fid=2, field_type=Qb.DATETIME)
    recordid = QuickBaseField(fid=3, field_type=Qb.NUMERIC)
    record_owner = QuickBaseField(fid=4, field_type=Qb.USER)
    last_modified = QuickBaseField(fid=5, field_type=Qb.USER)

I even tried to get the record by its record ID, and it worked fine, but the recordid field is None. The following code:

project_client = Project.client(TOKEN)
project_schema = Project.schema
q1 = eq_(project_schema.recordid, 1)
r = project_client.query(q1)
print(r[0].recordid)

prints None on the screen.

How can I get the record ID?

Thanks,
// Francesco

Query not handing UTF-8

Describe the bug
It's possible that this is not an issue with your code but with the QB API itself.
I say this because I have tried getting the raw response of the following query:

project_client = Project.client(TOKEN)
project_schema = Project.schema
query = eq_(project_schema.recordid, 17)
r = project_client.query(query, raw=True)

When I look at r._content in the binary string I see the following: You will see the tailored T&C\x92s in the final contract.

When I look at r.text I see the following: You will see the tailored T&C�s in the final contract.

image

When I run the same query without the raw=True parameter, in the corresponding field I see: You will see the tailored T&C�s in the final contract.

image

Is there anything I can do to preserve the special characters entered by the user in those fields?

Serialize based on field type

I'd consider submitting this as a pull request but not sure where you'd prefer it go (if anywhere). But, here are a couple changes I've made to the generated class files that make the add_records method actually work for us. At the very least, they probably belong in the QuickBaseTable base class.

What the following excerpts demonstrate are ways to handle:

  • QB errors when sending a datetime to a date field — what's particularly annoying about the REST API is that the XML API has no problem receiving a datetime for a date field
  • QB's lack of support for unicode characters — this isn't an API problem so much as it is a QB problem but since we're programatically adding data to QB, might as well nip the problem where we can

The API (and the front end) will store Tobias Fünke or Lindsay Bluth Fünke like so:
image

The code I've added normalises ü to u. Note that I've explicitly decided not to include Rich Text fields in this operation because it's reasonable to assume whatever WYSIWYG that is feeding HTML to this field would URL encode these properly.

Class Excerpts

Ideally this would go in QuickBaseJsonEncoder but that class does not have access to the field/attribute information. So I'm currently doing the modification at the QBT setter level.

class UnitApplication(QuickBaseTable):
    __dbid__ = "abcd12345"
    __app__ = MyHeyloApp

    def __setattr__(self, name: str, value: Any) -> None:
        if isinstance(value, datetime):
            fi = self.__class__.get_field_info(name)
            if fi.field_type == Qb.DATE:
                value = value.date()
        if isinstance(value, str):
            fi = self.__class__.get_field_info(name)
            if fi.field_type in [
                Qb.TEXT,
                Qb.TEXT_MULTILINE,
                Qb.TEXT_MULTI_SELECT,
                Qb.TEXT_MULTIPLE_CHOICE,
            ]:
                # from: https://stackoverflow.com/a/517974/612166
                nfkd_form = normalize("NFKD", value)
                value = "".join([c for c in nfkd_form if not combining(c)])
        return super().__setattr__(name, value)

    date_created = QuickBaseField(fid=1, field_type=Qb.DATETIME)
    date_modified = QuickBaseField(fid=2, field_type=Qb.DATETIME)
    recordid = QuickBaseField(fid=3, field_type=Qb.NUMERIC)
    record_owner = QuickBaseField(fid=4, field_type=Qb.USER)
    last_modified = QuickBaseField(fid=5, field_type=Qb.USER)

    related_unit = QuickBaseField(fid=6, field_type=Qb.NUMERIC)
    unit_name = QuickBaseField(fid=7, field_type=Qb.TEXT)
    # etc. etc. etc.

Tests

Using pytest and the lovely assertpy library and the mediocre Faker library.

class UnitApplicationForTesting(UnitApplication):
    date_time_as_date = QuickBaseField(fid=900, field_type=Qb.DATE)
    date_time_as_date_time = QuickBaseField(fid=901, field_type=Qb.DATETIME)
    rich_text_as_rich_text = QuickBaseField(fid=902, field_type=Qb.RICH_TEXT)
    text_as_ascii_text = QuickBaseField(fid=903, field_type=Qb.TEXT)
    text_multiline_as_ascii_text = QuickBaseField(fid=904, field_type=Qb.TEXT_MULTILINE)
    text_multi_select_as_ascii_text = QuickBaseField(
        fid=905, field_type=Qb.TEXT_MULTI_SELECT
    )
    text_multiple_choice_as_ascii_text = QuickBaseField(
        fid=906, field_type=Qb.TEXT_MULTIPLE_CHOICE
    )


test_ua_schema: UnitApplicationForTesting = UnitApplicationForTesting.schema


def test_stores_datetime_as_date() -> None:
    # arrange
    dt = fake.date_time()
    # act
    sut = UnitApplicationForTesting(date_time_as_date=dt)
    # assert
    assert_that(sut.date_time_as_date).is_equal_to(dt.date())


def test_stores_datetime_as_datetime() -> None:
    # arrange
    dt = fake.date_time()
    # act
    sut = UnitApplicationForTesting(date_time_as_date_time=dt)
    # assert
    assert_that(sut.date_time_as_date_time).is_equal_to(dt)


def test_stores_rich_text_as_rich_text() -> None:
    # arrange
    s = "<p>Tobias Fünke has been to <b>Juárez</b>, \nMéxico with his niña &amp; hermosa.</p>"
    # act
    sut = UnitApplicationForTesting(rich_text_as_rich_text=s)
    # assert
    assert_that(sut.rich_text_as_rich_text).described_as(
        "RICH_TEXT should go untouched"
    ).is_equal_to(s)


@pytest.mark.parametrize(
    "field",
    [
        (test_ua_schema.text_as_ascii_text),
        (test_ua_schema.text_multiline_as_ascii_text),
        (test_ua_schema.text_multi_select_as_ascii_text),
        (test_ua_schema.text_multiple_choice_as_ascii_text),
    ],
)
def test_stores_accented_text_as_ascii_text(field: QuickBaseField) -> None:
    # arrange
    sut = UnitApplicationForTesting()
    attr_name = sut.get_attr_from_fid(field.fid)
    # act
    sut.__setattr__(attr_name, "Tobias Fünke has been to Juárez, México with his niña")
    # assert
    assert_that(sut.__getattribute__(attr_name)).is_equal_to(
        "Tobias Funke has been to Juarez, Mexico with his nina"
    )

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.