From 1f4767573becdc01c454cf6b38912cb173d0d5af Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Thu, 30 Jun 2016 16:23:42 -0700 Subject: [PATCH 01/10] speed improvement! --- staffjoy/resource.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/staffjoy/resource.py b/staffjoy/resource.py index 2bfe5a6..1da3aba 100644 --- a/staffjoy/resource.py +++ b/staffjoy/resource.py @@ -1,14 +1,16 @@ import time +from datetime import datetime from copy import copy import requests from staffjoy.config import config_from_env from staffjoy.exceptions import UnauthorizedException, NotFoundException, BadRequestException +MICROSECONDS_PER_SECOND = 10**6 class Resource: - # Seconds to sleep between requests (bc of rate limits) - REQUEST_SLEEP = 0.5 + # Slow each request to this (bc of rate limits) + REQUEST_TIME_MICROSECONDS = 0.3 * MICROSECONDS_PER_SECOND # 0.3 seconds PATH = "" # URL path added to base, including route variables ID_NAME = None # What is this ID called in the route of children? @@ -71,10 +73,11 @@ def get_all(cls, parent=None, **params): base_obj = cls(key=parent.key, route=route, config=parent.config) """Perform a read request against the resource""" + start = datetime.now() r = requests.get(base_obj._url(), auth=(base_obj.key, ""), params=params) - time.sleep(cls.REQUEST_SLEEP) + cls._delay_for_ratelimits(start) if r.status_code not in cls.TRUTHY_CODES: return base_obj._handle_request_exception(r) @@ -121,8 +124,9 @@ def _handle_request_exception(request): def fetch(self): """Perform a read request against the resource""" + start = datetime.now() r = requests.get(self._url(), auth=(self.key, "")) - time.sleep(self.REQUEST_SLEEP) + self._delay_for_ratelimits(start) if r.status_code not in self.TRUTHY_CODES: return self._handle_request_exception(r) @@ -144,16 +148,18 @@ def _process_meta(self, response): def delete(self): """Delete the object""" + start = datetime.now() r = requests.delete(self._url(), auth=(self.key, "")) - time.sleep(self.REQUEST_SLEEP) + self._delay_for_ratelimits(start) if r.status_code not in self.TRUTHY_CODES: return self._handle_request_exception(r) def patch(self, **kwargs): """Change attributes of the item""" + start = datetime.now() r = requests.patch(self._url(), auth=(self.key, ""), data=kwargs) - time.sleep(self.REQUEST_SLEEP) + self._delay_for_ratelimits(start) if r.status_code not in self.TRUTHY_CODES: return self._handle_request_exception(r) @@ -175,8 +181,9 @@ def create(cls, parent=None, **kwargs): obj = cls(key=parent.key, route=route, config=parent.config) + start = datetime.now() response = requests.post(obj._url(), auth=(obj.key, ""), data=kwargs) - time.sleep(cls.REQUEST_SLEEP) + cls._delay_for_ratelimits(start) if response.status_code not in cls.TRUTHY_CODES: return cls._handle_request_exception(response) @@ -191,6 +198,14 @@ def create(cls, parent=None, **kwargs): def get_id(self): return self.data.get("id", self.route.get(self.ID_NAME)) + @classmethod + def _delay_for_ratelimits(cls, start): + """If request was shorter than max request time, delay""" + stop = datetime.now() + duration_microseconds = (stop-start).microseconds + if duration_microseconds < cls.REQUEST_TIME_MICROSECONDS: + time.sleep((cls.REQUEST_TIME_MICROSECONDS - duration_microseconds) / MICROSECONDS_PER_SECOND) + def __str__(self): return "{} id {}".format(self.__class__.__name__, self.route.get(self.ID_NAME)) From 58c079ae25e1b2c7d8f35426dc43a6070506b297 Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Fri, 1 Jul 2016 12:26:35 -0700 Subject: [PATCH 02/10] update yapf version --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index b014aa0..ea72794 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,2 @@ -yapf==0.6.2 +yapf==0.10.0 pytest==2.8.7 From 971e3d8ddc8733ac665c8c5054f2593bfb99425c Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Fri, 1 Jul 2016 12:26:58 -0700 Subject: [PATCH 03/10] final format change --- staffjoy/resource.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/staffjoy/resource.py b/staffjoy/resource.py index 1da3aba..4367426 100644 --- a/staffjoy/resource.py +++ b/staffjoy/resource.py @@ -8,9 +8,10 @@ MICROSECONDS_PER_SECOND = 10**6 + class Resource: # Slow each request to this (bc of rate limits) - REQUEST_TIME_MICROSECONDS = 0.3 * MICROSECONDS_PER_SECOND # 0.3 seconds + REQUEST_TIME_MICROSECONDS = 0.3 * MICROSECONDS_PER_SECOND # 0.3 seconds PATH = "" # URL path added to base, including route variables ID_NAME = None # What is this ID called in the route of children? @@ -202,9 +203,10 @@ def get_id(self): def _delay_for_ratelimits(cls, start): """If request was shorter than max request time, delay""" stop = datetime.now() - duration_microseconds = (stop-start).microseconds + duration_microseconds = (stop - start).microseconds if duration_microseconds < cls.REQUEST_TIME_MICROSECONDS: - time.sleep((cls.REQUEST_TIME_MICROSECONDS - duration_microseconds) / MICROSECONDS_PER_SECOND) + time.sleep((cls.REQUEST_TIME_MICROSECONDS - duration_microseconds) + / MICROSECONDS_PER_SECOND) def __str__(self): return "{} id {}".format(self.__class__.__name__, From de15bd229f0fab798f1d5c12da8385c08d4e3610 Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Thu, 23 Feb 2017 21:07:29 -0800 Subject: [PATCH 04/10] update dev, bump release, update formatting --- LICENSE.txt | 3 ++- README.md | 10 +++------- requirements-test.txt | 2 +- setup.py | 4 ++-- staffjoy/client.py | 6 ++---- staffjoy/config.py | 2 +- staffjoy/resource.py | 20 +++++++++++--------- test/test_functional.py | 7 ++++--- 8 files changed, 26 insertions(+), 28 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 354b463..41e964a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,5 +1,6 @@ MIT License -Copyright (c) 2016 Staffjoy, Inc. + +Copyright (c) 2016-2017 Staffjoy, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 88077bb..b77a22d 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ A light wrapper for the [Staffjoy](https://www.staffjoy.com) API in Python. This library does not include permissions management, and it is primarily used across microservices internally. Some of its features include internal-only endpoints. -[![Build Status](https://travis-ci.org/Staffjoy/client_python.svg?branch=master)](https://travis-ci.org/Staffjoy/client_python) +[![Build Status](https://travis-ci.org/Staffjoy/client_python.svg?branch=master)](https://travis-ci.org/Staffjoy/client_python) [![Moonlight](https://img.shields.io/badge/Contractors-1-brightgreen.svg)](https://moonlightwork.com/staffjoy) ## Installation -`pip install staffjoy` +`pip install --upgrade staffjoy` ## Authentication @@ -22,11 +22,7 @@ To get your organization ID, look at the URL path when you go to the Manager app ## Rate Limits -This client sleeps for .5 seconds after every request. Thus, in a single thread, requests are limited to 120 per minute. This is done to avoid rate limiting. Staffjoy's API currently rate limits to 300 requests per second across keys and IPs. Thus, by using this library, you should never encounter a rate limit (assuming one executing thread per IP address). - -## Updates - -If you use this library, please subscribe to the [Staffjoy API Updates Google Group](https://groups.google.com/forum/#!forum/staffjoy-api-updates) for important notifications about changes and deprecations. +This client sleeps after every request in order to limit requests to 120 per minute. This is done to avoid rate limiting. Staffjoy's API currently rate limits to 300 requests per second across keys and IPs. Thus, by using this library, you should never encounter a rate limit (assuming one executing thread per IP address). ## Usage diff --git a/requirements-test.txt b/requirements-test.txt index b014aa0..ee7ab8b 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,2 +1,2 @@ -yapf==0.6.2 +yapf==0.16.0 pytest==2.8.7 diff --git a/setup.py b/setup.py index f617fb9..af54dac 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ from setuptools import setup, find_packages -version = "0.22" +version = "0.23" setup(name="staffjoy", packages=find_packages(), version=version, description="Staffjoy API Wrapper in Python", author="Philip Thomas", - author_email="help@staffjoy.com", + author_email="philip@staffjoy.com", license="MIT", url="https://github.com/staffjoy/client_python", download_url="https://github.com/StaffJoy/client_python/archive/%s.tar.gz" % version, diff --git a/staffjoy/client.py b/staffjoy/client.py index bbc7045..5789d78 100644 --- a/staffjoy/client.py +++ b/staffjoy/client.py @@ -9,10 +9,8 @@ class Client(Resource): def get_organizations(self, limit=25, offset=0, **kwargs): - return Organization.get_all(parent=self, - limit=limit, - offset=offset, - **kwargs) + return Organization.get_all( + parent=self, limit=limit, offset=offset, **kwargs) def get_organization(self, id): return Organization.get(parent=self, id=id) diff --git a/staffjoy/config.py b/staffjoy/config.py index a13d9cd..be1ba82 100644 --- a/staffjoy/config.py +++ b/staffjoy/config.py @@ -16,7 +16,7 @@ class StageConfig(DefaultConfig): class DevelopmentConfig(DefaultConfig): ENV = "dev" LOG_LEVEL = logging.DEBUG - BASE = "http://dev.staffjoy.com/api/v2/" + BASE = "http://suite.local/api/v2/" config_from_env = { # Determined in main.py diff --git a/staffjoy/resource.py b/staffjoy/resource.py index 2bfe5a6..89be6ad 100644 --- a/staffjoy/resource.py +++ b/staffjoy/resource.py @@ -14,8 +14,10 @@ class Resource: ID_NAME = None # What is this ID called in the route of children? META_ENVELOPES = [] # Metadata keys for what to unpack from response ENVELOPE = "data" # We "envelope" response data in the "data" section - TRUTHY_CODES = [requests.codes.ok, requests.codes.created, - requests.codes.no_content, requests.codes.accepted] + TRUTHY_CODES = [ + requests.codes.ok, requests.codes.created, requests.codes.no_content, + requests.codes.accepted + ] def __init__(self, key="", @@ -71,9 +73,8 @@ def get_all(cls, parent=None, **params): base_obj = cls(key=parent.key, route=route, config=parent.config) """Perform a read request against the resource""" - r = requests.get(base_obj._url(), - auth=(base_obj.key, ""), - params=params) + r = requests.get( + base_obj._url(), auth=(base_obj.key, ""), params=params) time.sleep(cls.REQUEST_SLEEP) if r.status_code not in cls.TRUTHY_CODES: @@ -85,10 +86,11 @@ def get_all(cls, parent=None, **params): return_objects = [] for data in objects_data: # Note that this approach does not get meta data - return_objects.append(cls.get(parent=parent, - id=data.get(cls.ID_NAME, data.get( - "id")), - data=data)) + return_objects.append( + cls.get( + parent=parent, + id=data.get(cls.ID_NAME, data.get("id")), + data=data)) return return_objects diff --git a/test/test_functional.py b/test/test_functional.py index 9d7aaa2..782a703 100644 --- a/test/test_functional.py +++ b/test/test_functional.py @@ -58,9 +58,10 @@ def test_org_crud(): r.patch(name="Cocina") logger.debug("Adding worker") r.get_workers() - r.create_worker(email=TEST_WORKER, - min_hours_per_workweek=30, - max_hours_per_workweek=40) + r.create_worker( + email=TEST_WORKER, + min_hours_per_workweek=30, + max_hours_per_workweek=40) logger.debug("Deleting worker") r.delete() From 88ff40950bf0c307825acbe68263a438df75f214 Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Thu, 23 Feb 2017 21:21:42 -0800 Subject: [PATCH 05/10] update fmt --- staffjoy/resource.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/staffjoy/resource.py b/staffjoy/resource.py index 0242f9c..9a35737 100644 --- a/staffjoy/resource.py +++ b/staffjoy/resource.py @@ -77,9 +77,8 @@ def get_all(cls, parent=None, **params): """Perform a read request against the resource""" start = datetime.now() - r = requests.get(base_obj._url(), - auth=(base_obj.key, ""), - params=params) + r = requests.get( + base_obj._url(), auth=(base_obj.key, ""), params=params) cls._delay_for_ratelimits(start) if r.status_code not in cls.TRUTHY_CODES: From 161a2f66eb45ce1474b56e95911c08322c3411af Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Mon, 3 Apr 2017 15:36:46 -0700 Subject: [PATCH 06/10] allow custom domains --- README.md | 9 +++++++++ staffjoy/resource.py | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/README.md b/README.md index b77a22d..9870d89 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,15 @@ This library does not include permissions management, and it is primarily used a `pip install --upgrade staffjoy` +## Self-Hosted Use + +If you are self-hosting Staffjoy on a custom domain, please pass a `url_base` to the client. It defaults to `https://suite.staffjoy.com/api/v2/"`. (Trailing slash may matter). + +```python +from Staffjoy import Client +c = Client(key=YOUR_API_KEY, url_base="https://staffjoy.example.com/api/v2/") +``` + ## Authentication Authentication keys are currently tied to an individual user's account. To issue multiple keys, we currently suggest diff --git a/staffjoy/resource.py b/staffjoy/resource.py index 9a35737..7e96284 100644 --- a/staffjoy/resource.py +++ b/staffjoy/resource.py @@ -26,6 +26,7 @@ def __init__(self, key="", config=None, env="prod", + url_base=None, data={}, route={}, meta={}): @@ -34,6 +35,10 @@ def __init__(self, self.config = config or config_from_env.get(env, "prod") + # Used for self-hosted Staffjoy users + if url_base: + self.config.BASE = url_base + # These should be overridden by child classes self.data = data # Data from the read method self.route = route # Route variables From 0b50354356c038cf94df8a107e429d85b0a27783 Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Mon, 3 Apr 2017 15:44:48 -0700 Subject: [PATCH 07/10] bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index af54dac..f68aad3 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = "0.23" +version = "0.24" setup(name="staffjoy", packages=find_packages(), version=version, From 7ecd925bface243ed289a9a96b86afa066643eac Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Mon, 3 Apr 2017 18:42:08 -0700 Subject: [PATCH 08/10] disable tests --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8f989ff..bee3ba9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ python: - '3.5' script: - pip install -r requirements-test.txt -- make test +#- make test deploy: provider: pypi user: philipithomas From c231f0695dad1564de3af024bceec985d920d967 Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Mon, 26 Jun 2017 15:33:42 -0500 Subject: [PATCH 09/10] Update moonlight badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9870d89..d4da5b9 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A light wrapper for the [Staffjoy](https://www.staffjoy.com) API in Python. This library does not include permissions management, and it is primarily used across microservices internally. Some of its features include internal-only endpoints. -[![Build Status](https://travis-ci.org/Staffjoy/client_python.svg?branch=master)](https://travis-ci.org/Staffjoy/client_python) [![Moonlight](https://img.shields.io/badge/Contractors-1-brightgreen.svg)](https://moonlightwork.com/staffjoy) +[![Build Status](https://travis-ci.org/Staffjoy/client_python.svg?branch=master)](https://travis-ci.org/Staffjoy/client_python) [![Moonlight contractors](https://img.shields.io/badge/contractors-1147-brightgreen.svg)](https://moonlightwork.com/for/staffjoy) ## Installation From e8811b0c06651a15e691c96cbfd41e7da4f7f213 Mon Sep 17 00:00:00 2001 From: "Philip I. Thomas" Date: Wed, 28 Mar 2018 18:56:03 -0300 Subject: [PATCH 10/10] Update moonlight badge --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d4da5b9..e104455 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # client_python +[![Build Status](https://travis-ci.org/Staffjoy/client_python.svg?branch=master)](https://travis-ci.org/Staffjoy/client_python) [![Moonlight contractors](https://www.moonlightwork.com/shields/python.svg)](https://www.moonlightwork.com/for/python?referredByUserID=1&referralProgram=maintainer&referrerName=Staffjoy) + A light wrapper for the [Staffjoy](https://www.staffjoy.com) API in Python. This library does not include permissions management, and it is primarily used across microservices internally. Some of its features include internal-only endpoints. -[![Build Status](https://travis-ci.org/Staffjoy/client_python.svg?branch=master)](https://travis-ci.org/Staffjoy/client_python) [![Moonlight contractors](https://img.shields.io/badge/contractors-1147-brightgreen.svg)](https://moonlightwork.com/for/staffjoy) - ## Installation `pip install --upgrade staffjoy`