Skip to content

Commit

Permalink
- typo
Browse files Browse the repository at this point in the history
 - Person and Organization needed custom PROPERTIES to handle different readonly fields
  • Loading branch information
claytondaley committed Apr 21, 2015
1 parent dfa50f4 commit dbd1f95
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 56 deletions.
87 changes: 56 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,37 @@ 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
===========

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
====================
Expand All @@ -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()
Expand All @@ -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:
====================
Expand Down
3 changes: 2 additions & 1 deletion prototype.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion v1/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
81 changes: 58 additions & 23 deletions v2/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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> <Last>"
'first_name': basestring,
'last_name': basestring,
'customer_status': basestring,
Expand Down Expand Up @@ -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> <Last>"
'_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)
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit dbd1f95

Please sign in to comment.