Skip to content

Commit 7bdca91

Browse files
author
Arthur Debert
committed
Initial commit, basic resource structure works, Deal resource is simple but clean
0 parents  commit 7bdca91

18 files changed

+619
-0
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dist
2+
*.pyc
3+
/build/
4+
/pipedrive_py.egg-info/
5+
/runme.py
6+
.idea

.idea/uiDesigner.xml

+124
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AUTHORS.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Arthur Debert <[email protected]>Topic :: Software Development :: Libraries :: Python Modules

HISTORY.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
I needed this, so I made this.

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Pipedrive Python
2+
================
3+
4+
A simple python lib for interacting with the pipedrive.com api.

pipedrive/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from base import *
2+
from deal import *
3+
from user import *
4+
from activity import *

pipedrive/activity.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# encoding:utf-8
2+
from schematics.exceptions import ValidationError
3+
from base import BaseResource
4+
from schematics.models import Model
5+
from schematics.types import (
6+
StringType, IntType, DateTimeType
7+
)
8+
from schematics.types.compound import ListType
9+
10+
11+
class DealStatusType(ListType):
12+
OPEN = 'open'
13+
WON = 'won'
14+
LOST = 'lost'
15+
DELETED = 'deleted'
16+
VALID_STATUSES = (
17+
OPEN, WON, LOST, DELETED
18+
)
19+
20+
def validate_choices(self, items):
21+
errors = []
22+
for item in items:
23+
try:
24+
self.field.validate(item)
25+
if item not in DealStatusType.VALID_STATUSES:
26+
errors += ["%s is not a valid deal status" % item]
27+
except ValidationError as exc:
28+
errors += exc.messages
29+
30+
if errors:
31+
raise ValidationError(errors)
32+
33+
34+
class Activity(Model):
35+
subject = StringType(required=True)
36+
type = StringType(required=False)
37+
duration = IntType(required=False)
38+
user_id = IntType(required=False)
39+
deal_id = IntType(required=False)
40+
person_id = IntType(required=False)
41+
org_id = IntType(required=False)
42+
note = StringType(required=False)
43+
due_date = DateTimeType(required=False)
44+
45+
46+
class DealResource(BaseResource):
47+
API_ACESSOR_NAME = 'deal'
48+
LIST_REQ_PATH = '/deals'
49+
DETAIL_REQ_PATH = '/deals/{id}'
50+
51+
def detail(self, resource_ids):
52+
response = self._detail(resource_ids)
53+
return Activity(raw_input(response.json))
54+
55+
def create(self, deal):
56+
response = self._create(data=deal.to_native())
57+
return Activity(raw_input(response.json))
58+
59+
def list(self):
60+
response = self._list()
61+
return response.json

pipedrive/base.py

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# encoding:utf-8
2+
from copy import deepcopy
3+
import datetime
4+
import requests
5+
6+
from logging import getLogger
7+
from schematics.models import Model
8+
from schematics.types import BooleanType, DateType
9+
from schematics.types.compound import ListType, DictType, ModelType
10+
11+
logger = getLogger('pipedrive.api')
12+
13+
BASE_URL = 'https://api.pipedrive.com/v1'
14+
15+
16+
class PipedriveAPI(object):
17+
resource_registry = {}
18+
19+
def __init__(self, api_token):
20+
self.api_token = api_token
21+
22+
def __getattr__(self, item):
23+
try:
24+
return PipedriveAPI.resource_registry[item](self)
25+
except KeyError:
26+
raise AttributeError('No resource is registered under that name.')
27+
28+
def send_request(self, method, path, params=None, data=None):
29+
params = params or {}
30+
params['api_token'] = self.api_token
31+
url = BASE_URL + path
32+
try:
33+
return requests.request(method, url, params=params, data=data)
34+
except:
35+
logger.exception("Request failed")
36+
37+
@staticmethod
38+
def register_resource(resource_class):
39+
PipedriveAPI.resource_registry[
40+
resource_class.API_ACESSOR_NAME] = resource_class
41+
42+
43+
class BaseResource(object):
44+
"""Common ground for all api resources.
45+
46+
Attributes:
47+
API_ACESSOR_NAME(str): The property name that this resource will be
48+
accessible from the Api object (i.e. "sms" ) api.sms.liist()
49+
LIST_REQ_PATH(str): The request path component for the list view
50+
(listing and creation)
51+
DETAIL_REQ_PATH(str): The request path component for the detail view
52+
(deletion, updating and detail)
53+
"""
54+
55+
API_ACESSOR_NAME = ''
56+
LIST_REQ_PATH = None
57+
DETAIL_REQ_PATH = None
58+
59+
def __init__(self, api):
60+
self.api = api
61+
setattr(self.api, self.API_ACESSOR_NAME, self)
62+
63+
def send_request(self, method, path, params, data):
64+
response = self.api.send_request(method, path, params, data)
65+
if 200 <= response.status_code < 400:
66+
return response
67+
self.process_error(response)
68+
69+
def process_success(self, data):
70+
pass
71+
72+
def process_error(self, response):
73+
pass
74+
75+
def _create(self, params=None, data=None):
76+
return self.send_request('POST', self.LIST_REQ_PATH, params, data)
77+
78+
def _list(self, params=None, data=None):
79+
return self.send_request('GET', self.LIST_REQ_PATH, params, data)
80+
81+
def _delete(self, resource_ids, params=None, data=None):
82+
url = self.DETAIL_REQ_PATH % resource_ids
83+
return self.send_request('DELETE', url, params=None, data=None)
84+
85+
def _update(self, resource_ids, params=None, data=None):
86+
url = self.DETAIL_REQ_PATH % resource_ids
87+
return self.send_request('PUT', url, params=None, data=None)
88+
89+
def _detail(self, resource_ids, params=None, data=None):
90+
url = self.DETAIL_REQ_PATH % resource_ids
91+
return self.send_request('GET', url, params, data)
92+
93+
94+
class CollectionResponse(Model):
95+
items = []
96+
success = BooleanType()
97+
98+
def __init__(self, response, model_class):
99+
super(CollectionResponse, self).__init__()
100+
serialized = response.json()
101+
items = serialized['data'] or []
102+
self.items = [dict_to_model(one, model_class) for one in items]
103+
104+
105+
def dict_to_model(data, model_class):
106+
"""Converts the json response to a full fledge model
107+
The schematics model constructor is strict. If it sees keys that it
108+
doesn't know about it will raise an exception. This is a problem, both
109+
because we won't model all of the data at first, but also because the
110+
lib would break on new fields being returned.
111+
Therefore we inspect the model class and remove all keys not present
112+
before constructing the model.
113+
We also convert complex types to new models.
114+
Args:
115+
data(dict): The json response data as returned from the API.
116+
model_class(Model): The schematics model to instantiate
117+
Returns:
118+
Model: WIth the populated data
119+
"""
120+
data = deepcopy(data)
121+
model_keys = set(model_class.__dict__['_fields'].keys())
122+
# This is a major clusterfuck. Pipedrive will return, for example
123+
# under the "user_id" key, the user dict. There fore we check if we
124+
# need to convert this
125+
for key in model_keys:
126+
if isinstance(model_class._fields[key], ModelType):
127+
response_key = key + "_id"
128+
if response_key in data:
129+
value = dict_to_model(data[response_key],
130+
getattr(model_class, key).model_class)
131+
del data[response_key]
132+
data[key] = value
133+
134+
safe_keys = set(data.keys()).intersection(model_keys)
135+
safe_data = {key: data[key] for key in safe_keys}
136+
return model_class(raw_data=safe_data)
137+
138+
139+
class PipedriveDateTime(DateType):
140+
def to_native(self, value, context=None):
141+
return datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S")

0 commit comments

Comments
 (0)