From dbd1f95d98234f0b6ec25ec68ccd00abbf73a15f Mon Sep 17 00:00:00 2001 From: Clayton Daley Date: Mon, 20 Apr 2015 20:16:50 -0400 Subject: [PATCH] - typo - Person and Organization needed custom PROPERTIES to handle different readonly fields --- README.md | 87 ++++++++++++++++++++++++++++++++------------------ prototype.py | 3 +- v1/entity.py | 2 +- v2/resource.py | 81 +++++++++++++++++++++++++++++++++------------- 4 files changed, 117 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index cb10411..82ae7ea 100644 --- a/README.md +++ b/README.md @@ -4,22 +4,27 @@ Introduction The Python client for BaseCRM reflects a somewhat odd heritage: - There is no official Python client (they're a Ruby shop so no surprise). - - The [original BaseCRM API Client (for Python)](http://github.com/npinger/base-crm-api-client) focused on replicating the [Base API Documentation](http://dev.futuresimple.com/api/overview). - - An enhanced version of this branch (formerly OfficialSupport) can be found under the "official-1.x" branch. - - By early 2014, it became clear that FutureSimple was not regularly updating their API documents. To bring the API up to parity with the web application, @claytondaley reviewed the messages exchanged by the BaseCRM web interface and expanded the API client to take advantage of many of these capabilities. + - The [original BaseCRM API Client (for Python)](http://github.com/npinger/base-crm-api-client) focused on replicating the (soon to be deprecated) v1 [Base API Documentation](http://dev.futuresimple.com/api/overview). + - An enhanced version of this branch (formerly OfficialSupport) can be found under the [official-1.x branch](https://github.com/claytondaley/basecrm-client/tree/official-1.x). + - By early 2014, @claytondaley reviewed the messages exchanged by the BaseCRM web interface and expanded the client to access many of these capabilities. - In related communications, FutureSimple made it clear that much of this functionality was not set in stone. - - Users who wish to continue to use this client will find it in the master-1.x branch, but the API is deprecated - - @claytondaley also experimented with (stateful) access to the feed in a "stateful" branch - - In early 2015, FutureSimple announced an updated (v2) API that integrated many of these features into the official API documentation. - - Due to substantial improvements in the master-1.x branch, v2 support will be built off this branch. - - We've also heard that FutureSimple is releasing a [Sync](https://developers.getbase.com/docs/rest/articles/sync) and WebHooks option that may be added to this client. + - Users who wish to continue to use this client will find it in the [master-1.x branch](https://github.com/claytondaley/basecrm-client/tree/master-1.x). + - @claytondaley also experimented with (stateful) access to APIv1 Feeds in a [stateful-1.x branch](https://github.com/claytondaley/basecrm-client/tree/stateful-1.x) + - In early 2015, FutureSimple announced an updated (v2) API with revised [documentation](https://developers.getbase.com/). + - FutureSimple is also releasing a [Sync](https://developers.getbase.com/docs/rest/articles/sync) and WebHooks option that may be added to this client. -I'm taking advantage of the v2 rewrite to convert the BaseCRM API to a more object-oriented pattern. If you prefer to stick with the procedural approach, feel free to use (and update) the 1.x branch. +In early 2015 and as part of a complete rewrite of the API client, the master branch has been upgraded to APIv2 support: + + - The new codebase is object-oriented + - The rewrite should be highly extensible + - New objects inherit rich, default logic from base classes (Resource and Collection) + - Additional business logic is added by customizing a few basic methods + - The rewrite can support APIv1 objects + - NOTE: there is an open [issue](https://github.com/claytondaley/basecrm-client/issues/10) related to authentication Some final notes: - Several forks include a pip-friendly setup and I would welcome a PR. - - A new "official-2.x" is available if anyone wants to contribute PRs trimming the experimental v2 client down to official v2 API calls. - If you require (prefer?) an asynchronous client, [TxBaseCRM](https://github.com/claytondaley/TxBaseCRM) has been created to support an effort for Twisted. BaseCRM ORM @@ -27,9 +32,9 @@ BaseCRM ORM The pattern of the low-level client (below) was selected to be as transparent as possible to the underlying API calls and timing. -Eventually, it'd be nice to provide a more ORM-like experience on top of this foundation. Database ORMs are already a complex affair. Unfortunately, building an ORM on top of a REST API introduces even more issues. - -Only after some details about the v2 API are settled (especially concurrency and sync features) will it make sense to attempt this feature. +Eventually, the goal for the client is an ORM-like experience. Unfortunately, database ORMs are already a complex affair and building an ORM on top of a REST API introduces even more issues. + +No progress will be made in this direction until some details about the v2 API are settled (especially concurrency and sync features). Low-Level API Client ==================== @@ -44,12 +49,45 @@ The BaseCRM Client includes a library of authentication objects. To create a cl from basecrm.client import BaseAPI base = BaseAPI(auth) -The client also comes with a set of resources. Resources are Python objects that contain internal descriptions of the constraints and business rules for a set of API endpoints: +NOTE: at present, the resulting client will only be able to connect to one API (v1 or v2) at a time. There is an [open issue](https://github.com/claytondaley/basecrm-client/issues/10) to resolve this, but you can create two clients as a workaround. + +The client also comes with pre-defined Resources. Resources are Python objects that contain internal descriptions of the data structure and business rules for an API endpoints: + ... + from basecrm.v2.resource import Lead, Deal + # An entity with a specific id - from basecrm.v2.entities import Contact - contact_1 = Contact(1) + lead_1 = Lead(1) + + # A new Deal + new_deal = Deal() + # Contact is a special case. If you want to create a new Contact, create a Person or Organization instead so appropriate business rules are enforced. + +Resources contain no magic so you must explicitly ask the API client to act on them when you want to exchange data with the servers: + + # Data inside Resources are updated after a successful API call + base.get(contact_1) + + base.id + +This makes the low-level API Client a very thin wrapper around the actual API calls. The syntax is friendlier, but every API call is explicit. + +Updates and deletes are similar: + + ... + + # Valid properties (keys) and types (values) are found at Class.PROPERTIES and enforced when you attempt to update the related attribute. + lead_1.first_name = 'Fred' + + # Updates are committed to the server by calling save() and supplying the Resource + base.save(lead_1) + # Create() must be called instead if a Resource does not have an ID + # Finally, a Resource with an ID (loaded or unloaded) can be submitted for deletion + base.delete(lead_1) + +To list/search resources, the API Client provides Collections that store a set of filters for a particular object: + # A list of all Deals from basecrm.v2.entities import DealSet all_deals = DealSet() @@ -58,25 +96,12 @@ The client also comes with a set of resources. Resources are Python objects tha # Valid kwargs can be found at Class.FILTERS all_organizations = basecrm.OrganizationSet() -These objects contain no magic so you must explicitly ask the API client to act on them when you want to exchange data with the servers: +Unlike Resources, limits on the page size prevent us from loading all Collection data at once. To make this explicit, data is not stored in the Collection, but is returned as a page: - # Resources are updated based on the response the API - base.get(contact_1) - # Collections describe a set of Resources page = base.get_page(all_organizations, page, per_page, order_by) -This makes the low-level API Client a very thin wrapper around the actual API calls. The syntax is friendlier, but every API call is explicit. - -Updates and deletes are similar: - - contact_1 = basecrm.Contact(1) - base.get(contact_1) - # Valid properties (keys) and types (values) are found at Class.PROPERTIES - contact_1.name = 'Fred' - base.save(contact_1) - # and when you're done - base.delete(contact_1) +Since Collections are read-only, they cannot be submitted to `create()`, `save()`, or `delete()` Ongoing Development: ==================== diff --git a/prototype.py b/prototype.py index e72ff00..9282c5f 100644 --- a/prototype.py +++ b/prototype.py @@ -189,6 +189,7 @@ def set_data(self, data): self.__dict__['data'] = self.format_data(data[self.RESPONSE_KEY]) # Mark data as loaded self.__dict__['loaded'] = True + return self # returned for setting and chaining convenience def format_data(self, data): """ @@ -198,7 +199,7 @@ def format_data(self, data): - The v2 Contact object wraps the address up into an Address object - In v1, tags are sent as comma-separated lists that should be exploded into real lists """ - return data # data is mutable, but this simplifies inline assignment + return data # returned for setting and chaining convenience def params(self): params = deepcopy(self.dirty) diff --git a/v1/entity.py b/v1/entity.py index 6246d8b..2c6b23a 100644 --- a/v1/entity.py +++ b/v1/entity.py @@ -59,7 +59,7 @@ class Contact(ResourceV1): RESPONSE_KEY = 'contact' PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ # Commented items are not listed as valid PUT/POST variables # Private items are not listed, but are almost certainly not allowed diff --git a/v2/resource.py b/v2/resource.py index 85ca4f9..35ad9db 100644 --- a/v2/resource.py +++ b/v2/resource.py @@ -4,7 +4,7 @@ import logging logger = logging.getLogger(__name__) -from prototype import Resource +from prototype import Resource, Entity __author__ = 'Clayton Daley III' __copyright__ = "Copyright 2015, Clayton Daley III" @@ -18,7 +18,7 @@ class Account(Resource): _PATH = "accounts" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_name': basestring, @@ -38,7 +38,7 @@ def __init__(self): class Address(Resource): PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ 'line1': basestring, 'city': basestring, @@ -55,7 +55,7 @@ class Contact(Resource): _PATH = "contacts" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -94,29 +94,33 @@ def __init__(self, entity_id=None): super(Contact, self).__init__(entity_id) def set_data(self, data): - for k, v in data['data'].items(): + super(Contact, self).set_data(data) + if data['data']['is_organization']: + Entity.__setattr__(self, '__class__', Organization) + else: + Entity.__setattr__(self, '__class__', Person) + return self # returned for setting and chaining convenience + + def format_data(self, data): + for k, v in data.iteritems(): if k == 'address': address = Address() address.set_data({'data': v}) # Nest back in a 'data' key to use default processor data[k] = address - if data['is_organization']: - self.__class__ = Organization - else: - self.__class__ = Person return data # data is mutable, but this simplifies chaining and inline assignment class Person(Contact): PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, 'owner_id': int, '_is_organization': bool, 'contact_id': int, - 'name': basestring, + '_name': basestring, # API will accept value, but fails to update 'name' from " " 'first_name': basestring, 'last_name': basestring, 'customer_status': basestring, @@ -158,7 +162,38 @@ def params(self): class Organization(Contact): - PROPERTIES = Contact.PROPERTIES + PROPERTIES = { + """ + Read-only attributes are preceded by an underscore + """ + '_id': int, + '_creator_id': int, + 'owner_id': int, + '_is_organization': bool, + 'contact_id': int, + 'name': basestring, # API will accept value, but fails to update 'name' from " " + '_first_name': basestring, + '_last_name': basestring, + 'customer_status': basestring, + 'prospect_status': basestring, + 'title': basestring, + 'description': basestring, + 'industry': basestring, + 'website': basestring, + 'email': basestring, + 'phone': basestring, + 'mobile': basestring, + 'fax': basestring, + 'twitter': basestring, + 'facebook': basestring, + 'linkedin': basestring, + 'skype': basestring, + 'address': Address, + 'tags': list, + 'custom_fields': dict, + '_created_at': basestring, + '_updated_at': basestring, + } def __init__(self, entity_id=None): super(Organization, self).__init__(entity_id) @@ -180,7 +215,7 @@ class Deal(Resource): _PATH = "deals" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -209,7 +244,7 @@ class DealContact(Resource): ] PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_deal': Deal, '_contact': Contact, @@ -240,7 +275,7 @@ class Lead(Resource): _PATH = "leads" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -285,7 +320,7 @@ class LossReason(Resource): _PATH = "loss_reasons" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -304,7 +339,7 @@ class Note(Resource): ] PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -323,7 +358,7 @@ class Pipeline(Resource): _PATH = "pipelines" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_name': basestring, @@ -336,7 +371,7 @@ class Source(Resource): _PATH = "sources" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -350,7 +385,7 @@ class Stage(Resource): _PATH = "stages" PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_name': basestring, @@ -373,7 +408,7 @@ class Tag(Resource): ] PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -396,7 +431,7 @@ class Task(Resource): ] PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_creator_id': int, @@ -429,7 +464,7 @@ class User(Resource): ] PROPERTIES = { """ - Read-only attributes are proceeded by an underscore + Read-only attributes are preceded by an underscore """ '_id': int, '_name': basestring,