· 8 years ago · Nov 29, 2017, 08:14 AM
1# Python bindings to Upwork API
2# python-upwork version 0.5
3# (C) 2010-2015 Upwork
4
5import os
6import json
7import logging
8import urllib3
9
10from urllib3 import Retry
11
12from upwork import ca_certs_locater
13from upwork.oauth import OAuth
14from upwork.http import raise_http_error
15from upwork.utils import decimal_default
16from upwork.exceptions import IncorrectJsonResponseError
17
18
19__all__ = ["Client"]
20
21
22logger = logging.getLogger('python-upwork')
23
24if os.environ.get("PYTHON_UPWORK_DEBUG", False):
25 if os.environ.get("PYTHON_UPWORK_DEBUG_FILE", False):
26 fh = logging.FileHandler(filename=os.environ["PYTHON_UPWORK_DEBUG_FILE"]
27 )
28 fh.setLevel(logging.DEBUG)
29 logger.addHandler(fh)
30 else:
31 ch = logging.StreamHandler()
32 ch.setLevel(logging.DEBUG)
33 logger.addHandler(ch)
34else:
35 ch = logging.StreamHandler()
36 ch.setLevel(logging.CRITICAL)
37 logger.addHandler(ch)
38
39
40class Client(object):
41 """
42 Main API client with oAuth v1 authorization.
43
44 *Parameters:*
45 :public_key: Public API key
46
47 :secret_key: API key secret
48
49 :oauth_access_token: oAuth access token public key
50
51 :oauth_access_token_secret: oAuth access token secret key
52
53 :fmt: (optional, default ``json``)
54 API response format.
55 Currently only ``'json'`` is supported
56
57 :finreport: (optional, default ``True``)
58 Whether to attach
59 :py:mod:`upwork.routers.finreport` router
60
61 :hr: (optional, default ``True``)
62 Whether to attach
63 :py:mod:`upwork.routers.hr` router
64
65 :messages: (optional, default ``True``)
66 Whether to attach
67 :py:mod:`upwork.routers.messages` router
68
69 :offers: (optional, default ``True``)
70 Whether to attach
71 :py:mod:`upwork.routers.offers` router
72
73 :provider: (optional, default ``True``)
74 Whether to attach
75 :py:mod:`upwork.routers.provider` router
76
77 :task: (optional, default ``True``)
78 Whether to attach
79 :py:mod:`upwork.routers.task` router
80
81 :team: (optional, default ``True``)
82 Whether to attach
83 :py:mod:`upwork.routers.team` router
84
85 :timereport: (optional, default ``True``)
86 Whether to attach
87 :py:mod:`upwork.routers.timereport` router
88
89 :job: (optional, default ``True``)
90 Whether to attach
91 :py:mod:`upwork.routers.job` router
92
93 :timeout: (optional, default ``3 secs``)
94 Socket operations timeout.
95 """
96
97 def __init__(self, public_key, secret_key,
98 oauth_access_token=None, oauth_access_token_secret=None,
99 fmt='json', finreport=True, hr=True, messages=True,
100 offers=True, provider=True, task=True, team=True,
101 timereport=True, job=True, timeout=3):
102
103 self.public_key = public_key
104 self.secret_key = secret_key
105 self.fmt = fmt
106
107 # Catch the warning about
108 # """
109 # SecurityWarning: Certificate has no `subjectAltName`,
110 # falling back to check for a `commonName` for now.
111 # This feature is being removed by major browsers
112 # and deprecated by RFC 2818.
113 # (See https://github.com/shazow/urllib3/issues/497 for details.)
114 # """
115 # The warning will appear only in logs
116 logging.captureWarnings(True)
117 self.http = urllib3.PoolManager(
118 cert_reqs='CERT_REQUIRED',
119 ca_certs=ca_certs_locater.get(),
120 timeout=int(timeout),
121 retries=Retry(2, backoff_factor=0.5)
122 )
123
124 self.oauth_access_token = oauth_access_token
125 self.oauth_access_token_secret = oauth_access_token_secret
126
127 #Namespaces
128 self.auth = OAuth(self)
129
130 if finreport:
131 from upwork.routers.finreport import Finreports
132 self.finreport = Finreports(self)
133
134 if hr:
135 from upwork.routers.hr import HR_V1, HR, HR_V3, HR_V4
136 self.hr_v1 = HR_V1(self)
137 self.hr = HR(self)
138 self.hr_v3 = HR_V3(self)
139 self.hr_v4 = HR_V4(self)
140
141 if messages:
142 from upwork.routers.messages import Messages
143 self.messages = Messages(self)
144
145 if offers:
146 from upwork.routers.offers import Offers
147 self.offers = Offers(self)
148
149 if provider:
150 from upwork.routers.provider import Provider, Provider_V2
151 self.provider = Provider(self)
152 self.provider_v2 = Provider_V2(self)
153
154 if task:
155 from upwork.routers.task import Task, Task_V2
156 self.task = Task(self)
157 self.task_v2 = Task_V2(self)
158
159 if team:
160 from upwork.routers.team import Team, Team_V2
161 self.team = Team(self)
162 self.team_v2 = Team_V2(self)
163
164 if timereport:
165 from upwork.routers.timereport import TimeReport
166 self.timereport = TimeReport(self)
167
168 if job:
169 from upwork.routers.job import Job
170 self.job = Job(self)
171
172 #Shortcuts for HTTP methods
173 def get(self, url, data=None):
174 return self.read(url, data, method='GET', fmt=self.fmt)
175
176 def post(self, url, data=None):
177 return self.read(url, data, method='POST', fmt=self.fmt)
178
179 def put(self, url, data=None):
180 return self.read(url, data, method='PUT', fmt=self.fmt)
181
182 def delete(self, url, data=None):
183 return self.read(url, data, method='DELETE', fmt=self.fmt)
184
185 # The method that actually makes HTTP requests
186 def urlopen(self, url, data=None, method='GET', headers=None):
187 """Perform oAuth v1 signed HTTP request.
188
189 *Parameters:*
190 :url: Target url
191
192 :data: Dictionary with parameters
193
194 :method: (optional, default ``GET``)
195 HTTP method, possible values:
196 * ``GET``
197 * ``POST``
198 * ``PUT``
199 * ``DELETE``
200
201 :headers: (optional, default ``{}``)
202 Dictionary with header values
203
204 """
205
206 if headers is None:
207 headers = {}
208
209 self.last_method = method
210 self.last_url = url
211 self.last_data = data
212
213 # TODO: Headers are not supported fully yet
214 # instead we pass oauth parameters in querystring
215 if method in ('PUT', 'DELETE'):
216 post_data = self.auth.get_oauth_params(
217 url, self.oauth_access_token,
218 self.oauth_access_token_secret,
219 {}, method) # don't need parameters in url
220 else:
221 if data is None:
222 data = {}
223 post_data = self.auth.get_oauth_params(
224 url, self.oauth_access_token,
225 self.oauth_access_token_secret,
226 data, method)
227
228 if method == 'GET':
229 url = '{0}?{1}'.format(url, post_data)
230 return self.http.urlopen(method, url)
231 elif method == 'POST':
232 return self.http.urlopen(
233 method, url, body=post_data,
234 headers={'Content-Type':
235 'application/x-www-form-urlencoded;charset=UTF-8'})
236 elif method in ('PUT', 'DELETE'):
237 url = '{0}?{1}'.format(url, post_data)
238 headers['Content-Type'] = 'application/json'
239 if data is not None:
240 data_json = json.dumps(data)
241 else:
242 data_json = ''
243 return self.http.urlopen(
244 method, url, body=data_json, headers=headers)
245
246 else:
247 raise Exception('Wrong http method: {0}. Supported'
248 'methods are: '
249 'GET, POST, PUT, DELETE'.format(method))
250
251 def read(self, url, data=None, method='GET', fmt='json'):
252 """
253 Returns parsed Python object or raises an error.
254
255 *Parameters:*
256 :url: Target url
257
258 :data: Dictionary with parameters
259
260 :method: (optional, default ``GET``)
261 HTTP method, possible values:
262 * ``GET``
263 * ``POST``
264 * ``PUT``
265 * ``DELETE``
266
267 :fmt: (optional, default ``json``)
268 API response format.
269 Currently only ``'json'`` is supported
270
271 """
272 assert fmt == 'json', "Only JSON format is supported at the moment"
273
274 if '/gds/' not in url:
275 url = '{0}.{1}'.format(url, fmt)
276
277 logger = logging.getLogger('python-upwork')
278
279 logger.debug('Prepairing to make Upwork call')
280 logger.debug('URL: {0}'.format(url))
281 try:
282 logger.debug('Data: {0}'.format(
283 json.dumps(data, default=decimal_default)))
284 except TypeError:
285 logger.debug('Data: {0}'.format(str(data)))
286 logger.debug('Method: {0}'.format(method))
287 response = self.urlopen(url, data, method)
288
289 if response.status != 200:
290 logger.debug('Error: {0}'.format(response))
291 raise_http_error(url, response)
292
293 result = response.data
294 logger.debug('Response: {0}'.format(result))
295
296 if fmt == 'json':
297 try:
298 result = json.loads(result.decode("utf-8")) #this line
299 except ValueError:
300 # Not a valid json string
301 logger.debug('Response is not a valid json string')
302 raise IncorrectJsonResponseError(
303 json.dumps({'status': response.status, 'body': result},
304 default=decimal_default)
305 )
306 return result
307
308
309if __name__ == "__main__":
310 import doctest
311 doctest.testmod()