├── .gitignore ├── .style.yapf ├── .travis.yml ├── LICENSE.txt ├── README.md ├── makefile ├── requirements-test.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── staffjoy ├── __init__.py ├── client.py ├── config.py ├── exceptions.py ├── resource.py └── resources │ ├── __init__.py │ ├── admin.py │ ├── apikey.py │ ├── chomp_task.py │ ├── cron.py │ ├── location.py │ ├── location_shift.py │ ├── location_time_off_request.py │ ├── location_timeclock.py │ ├── manager.py │ ├── mobius_task.py │ ├── organization.py │ ├── organization_worker.py │ ├── plan.py │ ├── preference.py │ ├── recurring_shift.py │ ├── role.py │ ├── schedule.py │ ├── schedule_shift.py │ ├── schedule_time_off_request.py │ ├── schedule_timeclock.py │ ├── session.py │ ├── shift.py │ ├── shift_eligible_workers.py │ ├── shift_query.py │ ├── time_off_request.py │ ├── timeclock.py │ ├── user.py │ └── worker.py └── test ├── __init__.py └── test_functional.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | 3 | .cache/ 4 | 5 | venv/ 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | 20 | # Installer logs 21 | pip-log.txt 22 | 23 | # Unit test / coverage reports 24 | .coverage 25 | .tox 26 | 27 | #Translations 28 | *.mo 29 | 30 | -------------------------------------------------------------------------------- /.style.yapf: -------------------------------------------------------------------------------- 1 | [style] 2 | based_on_style = pep8 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '2.7' 4 | - '3.5' 5 | script: 6 | - pip install -r requirements-test.txt 7 | #- make test 8 | deploy: 9 | provider: pypi 10 | user: philipithomas 11 | on: 12 | branch: master 13 | password: 14 | secure: ZnhnCCoVMwte/LzaJqtqGIwcw6rD0HeQGUGczSfioEiLEqqJswbf1Fbj/xpbifoMHxwnBuoItOInaAaERXWaV+3uM7zz9jOJtfEim9Z9zlYpAC0r4CG6qYOQiY743XnL5DLV7jzb1REr9zYMumK8jY+AaTYD3pMODolUSjwfnOGYUoTbagvzzh9DTmgAyO73Z/XByHu6vZhSWqBo0V7PKdU0V/XFhkp1yNwoxnChlOUOsmvlVhZVxR1nDvfLIEv91wHXeO1HQjgwc093JHD+vo127KqrJu37L/lz5MvMX4VvSJwnvZJvs3ZaImiDT1Y9V0Y9ymlPQDRkgS2mLfJFMVagf3nLekZdHf9mGSwTWAdDWz2ha/ptj05fIO1/Cxfo6wnsxRMBVCwR0kVrOpesOgus66I0TZczDaoir1f4hxs6HoT40fZ5XXMR2VLfJ429+Mh8kCAgl0DQZX4wQd/dYguKKPCo6zmSNuJEStHK53MHsFkFsnivPp4o2hJTq/4NcGTp8Erl8cHJX8imcDKxK20GiDatzYAbx3ogygrmM1R0O2JG2Ma1h7unp+YZ/67i7wuTi0IYfC51d4fcXWxS/zGaviidFiipLdK+SS31kBUM0dPEGN7lSEaqIqXMVh4+j/HcPvq5/LNJje9zEYsNOcCjLmjjlSFDEPlI5h1ZRis= 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2017 Staffjoy, Inc. 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # client_python 2 | 3 | [![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) 4 | 5 | A light wrapper for the [Staffjoy](https://www.staffjoy.com) API in Python. 6 | 7 | This library does not include permissions management, and it is primarily used across microservices internally. Some of its features include internal-only endpoints. 8 | 9 | ## Installation 10 | 11 | `pip install --upgrade staffjoy` 12 | 13 | ## Self-Hosted Use 14 | 15 | 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). 16 | 17 | ```python 18 | from Staffjoy import Client 19 | c = Client(key=YOUR_API_KEY, url_base="https://staffjoy.example.com/api/v2/") 20 | ``` 21 | 22 | ## Authentication 23 | 24 | Authentication keys are currently tied to an individual user's account. To issue multiple keys, we currently suggest 25 | 26 | * **Permanent**: Every Staffjoy account includes a permanent API token that can be accessed from [My Account](https://www.staffjoy.com/auth/api-key) while logged in. 27 | * **Time-based (6-hour)**: To issue an API token that is valid for 6 hours, visit [this link](https://www.staffjoy.com/auth/api-token) while logged in (note: it is JSON-encoded) 28 | * **Time-based (other lengths)**: Please email help@staffjoy.com 29 | 30 | To get your organization ID, look at the URL path when you go to the Manager app while logged in. 31 | 32 | ## Rate Limits 33 | 34 | 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). 35 | 36 | ## Usage 37 | 38 | Start with the client, then traverse the tree. 39 | 40 | ```python 41 | from Staffjoy import Client 42 | 43 | c = Client(key=YOUR_API_KEY) 44 | 45 | # To get your organization id, look at the URL path for the Manager 46 | # or email help@staffjoy.com 47 | org = c.get_organization(ORG_ID) 48 | 49 | # See the org name 50 | print(org.data.get("name)) 51 | 52 | # See all locations 53 | org.get_locations() 54 | 55 | # Add an new location 56 | loc = org.create_location(name="Staffjoy HQ", timezone="America/Los_Angeles") 57 | 58 | # Modify its name 59 | loc.patch(name="San Francisco") 60 | 61 | # See roles 62 | roles = loc.get_roles() 63 | 64 | # Create a role and add a worker for scheduling 65 | role = loc.create_role(name="Mathematicians") 66 | role.create_worker(email="dantzig@7bridg.es") 67 | 68 | # Then clean it all up (recursively deletes node children) 69 | loc.delete() 70 | 71 | ``` 72 | 73 | 74 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | fmt: 3 | yapf -r -i staffjoy/ test/ || : 4 | test: 5 | pip install -r requirements-test.txt 6 | yapf -r -d staffjoy/ test/ || (echo "Document not formatted - run 'make fmt'" && exit 1) 7 | py.test test -v 8 | 9 | 10 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | yapf==0.16.0 2 | pytest==2.8.7 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cffi==1.5.0 2 | cryptography==1.2.2 3 | enum34==1.1.2 4 | idna==2.0 5 | ipaddress==1.0.16 6 | ndg-httpsclient==0.4.0 7 | pyasn1==0.1.9 8 | pycparser==2.14 9 | pyOpenSSL==0.15.1 10 | requests==2.9.1 11 | six==1.10.0 12 | wheel==0.26.0 13 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = "0.24" 4 | setup(name="staffjoy", 5 | packages=find_packages(), 6 | version=version, 7 | description="Staffjoy API Wrapper in Python", 8 | author="Philip Thomas", 9 | author_email="philip@staffjoy.com", 10 | license="MIT", 11 | url="https://github.com/staffjoy/client_python", 12 | download_url="https://github.com/StaffJoy/client_python/archive/%s.tar.gz" % version, 13 | keywords=["staffjoy-api", "staffjoy", "staff joy"], 14 | install_requires=["requests[security]"], ) 15 | -------------------------------------------------------------------------------- /staffjoy/__init__.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.exceptions import UnauthorizedException, NotFoundException, BadRequestException 3 | from staffjoy.client import Client 4 | -------------------------------------------------------------------------------- /staffjoy/client.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.organization import Organization 3 | from staffjoy.resources.cron import Cron 4 | from staffjoy.resources.user import User 5 | from staffjoy.resources.plan import Plan 6 | from staffjoy.resources.chomp_task import ChompTask 7 | from staffjoy.resources.mobius_task import MobiusTask 8 | 9 | 10 | class Client(Resource): 11 | def get_organizations(self, limit=25, offset=0, **kwargs): 12 | return Organization.get_all( 13 | parent=self, limit=limit, offset=offset, **kwargs) 14 | 15 | def get_organization(self, id): 16 | return Organization.get(parent=self, id=id) 17 | 18 | def create_organization(self, **kwargs): 19 | return Organization.create(parent=self, **kwargs) 20 | 21 | def cron(self): 22 | """Internal only - cron job manual timer""" 23 | return Cron.get_all(parent=self) 24 | 25 | def get_users(self, limit=25, offset=0, **kwargs): 26 | 27 | # Some supported filters: filterbyUsername, filterByEmail 28 | return User.get_all(parent=self, limit=limit, offset=offset, **kwargs) 29 | 30 | def get_user(self, id): 31 | return User.get(parent=self, id=id) 32 | 33 | def get_plans(self, **kwargs): 34 | return Plan.get_all(parent=self, **kwargs) 35 | 36 | def get_plan(self, id): 37 | return Plan.get(parent=self, id=id) 38 | 39 | def get_chomp_task(self, id): 40 | # id is schedule id 41 | return ChompTask.get(parent=self) 42 | 43 | def get_chomp_tasks(self, **kwargs): 44 | return ChompTask.get(parent=self) 45 | 46 | def claim_chomp_task(self): 47 | return ChompTask.create(parent=self) 48 | 49 | def get_mobius_task(self, id): 50 | # id is schedule id 51 | return MobiusTask.get(parent=self) 52 | 53 | def get_mobius_tasks(self, **kwargs): 54 | return MobiusTask.get(parent=self) 55 | 56 | def claim_mobius_task(self): 57 | return MobiusTask.create(parent=self) 58 | -------------------------------------------------------------------------------- /staffjoy/config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class DefaultConfig: 5 | ENV = "prod" 6 | LOG_LEVEL = logging.INFO 7 | BASE = "https://suite.staffjoy.com/api/v2/" 8 | REQUEST_SLEEP = 0.5 9 | 10 | 11 | class StageConfig(DefaultConfig): 12 | ENV = "stage" 13 | BASE = "https://stage.staffjoy.com/api/v2/" 14 | 15 | 16 | class DevelopmentConfig(DefaultConfig): 17 | ENV = "dev" 18 | LOG_LEVEL = logging.DEBUG 19 | BASE = "http://suite.local/api/v2/" 20 | 21 | 22 | config_from_env = { # Determined in main.py 23 | "dev": DevelopmentConfig, 24 | "stage": StageConfig, 25 | "prod": DefaultConfig, 26 | } 27 | -------------------------------------------------------------------------------- /staffjoy/exceptions.py: -------------------------------------------------------------------------------- 1 | class UnauthorizedException(Exception): 2 | def __init__(self, code=401, response={}): 3 | 4 | self.code = code 5 | # We envelope error messages like this 6 | self.message = response.get("message", "Unknown Error") 7 | super(UnauthorizedException, self).__init__() 8 | 9 | def __str__(self): 10 | return "\n Code: {} \n Message: {} \n".format(self.code, self.message) 11 | 12 | 13 | class NotFoundException(Exception): 14 | def __init__(self, code=404, response={}): 15 | 16 | self.code = code 17 | # We envelope error messages like this 18 | self.message = response.get("message", "Unknown Error") 19 | super(NotFoundException, self).__init__() 20 | 21 | def __str__(self): 22 | return "\n Code: {} \n Message: {} \n".format(self.code, self.message) 23 | 24 | 25 | class BadRequestException(Exception): 26 | def __init__(self, code=400, response={}): 27 | 28 | self.code = code 29 | # We envelope error messages like this 30 | self.message = response.get("message", "Unknown Error") 31 | super(BadRequestException, self).__init__() 32 | 33 | def __str__(self): 34 | return "\n Code: {} \n Message: {} \n".format(self.code, self.message) 35 | -------------------------------------------------------------------------------- /staffjoy/resource.py: -------------------------------------------------------------------------------- 1 | import time 2 | from datetime import datetime 3 | from copy import copy 4 | import requests 5 | 6 | from staffjoy.config import config_from_env 7 | from staffjoy.exceptions import UnauthorizedException, NotFoundException, BadRequestException 8 | 9 | MICROSECONDS_PER_SECOND = 10**6 10 | 11 | 12 | class Resource: 13 | # Slow each request to this (bc of rate limits) 14 | REQUEST_TIME_MICROSECONDS = 0.3 * MICROSECONDS_PER_SECOND # 0.3 seconds 15 | 16 | PATH = "" # URL path added to base, including route variables 17 | ID_NAME = None # What is this ID called in the route of children? 18 | META_ENVELOPES = [] # Metadata keys for what to unpack from response 19 | ENVELOPE = "data" # We "envelope" response data in the "data" section 20 | TRUTHY_CODES = [ 21 | requests.codes.ok, requests.codes.created, requests.codes.no_content, 22 | requests.codes.accepted 23 | ] 24 | 25 | def __init__(self, 26 | key="", 27 | config=None, 28 | env="prod", 29 | url_base=None, 30 | data={}, 31 | route={}, 32 | meta={}): 33 | """Initialize the resource""" 34 | self.key = key 35 | 36 | self.config = config or config_from_env.get(env, "prod") 37 | 38 | # Used for self-hosted Staffjoy users 39 | if url_base: 40 | self.config.BASE = url_base 41 | 42 | # These should be overridden by child classes 43 | self.data = data # Data from the read method 44 | self.route = route # Route variables 45 | self.meta = meta # Meta data 46 | 47 | @classmethod 48 | def get(cls, parent=None, id=None, data=None): 49 | """Inherit info from parent and return new object""" 50 | # TODO - allow fetching of parent based on child? 51 | 52 | if parent is not None: 53 | route = copy(parent.route) 54 | else: 55 | route = {} 56 | 57 | if id is not None and cls.ID_NAME is not None: 58 | route[cls.ID_NAME] = id 59 | 60 | obj = cls(key=parent.key, route=route, config=parent.config) 61 | 62 | if data: 63 | # This is used in "get all" queries 64 | obj.data = data 65 | else: 66 | obj.fetch() 67 | 68 | return obj 69 | 70 | @classmethod 71 | def get_all(cls, parent=None, **params): 72 | 73 | if parent is not None: 74 | route = copy(parent.route) 75 | else: 76 | route = {} 77 | if cls.ID_NAME is not None: 78 | # Empty string triggers "get all resources" 79 | route[cls.ID_NAME] = "" 80 | 81 | base_obj = cls(key=parent.key, route=route, config=parent.config) 82 | """Perform a read request against the resource""" 83 | 84 | start = datetime.now() 85 | r = requests.get( 86 | base_obj._url(), auth=(base_obj.key, ""), params=params) 87 | cls._delay_for_ratelimits(start) 88 | 89 | if r.status_code not in cls.TRUTHY_CODES: 90 | return base_obj._handle_request_exception(r) 91 | 92 | response = r.json() 93 | objects_data = response.get(base_obj.ENVELOPE or base_obj, []) 94 | 95 | return_objects = [] 96 | for data in objects_data: 97 | # Note that this approach does not get meta data 98 | return_objects.append( 99 | cls.get( 100 | parent=parent, 101 | id=data.get(cls.ID_NAME, data.get("id")), 102 | data=data)) 103 | 104 | return return_objects 105 | 106 | def _url(self): 107 | """Get the URL for the resource""" 108 | if self.ID_NAME not in self.route.keys() and "id" in self.data.keys(): 109 | self.route[self.ID_NAME] = self.data["id"] 110 | return self.config.BASE + self.PATH.format(**self.route) 111 | 112 | @staticmethod 113 | def _handle_request_exception(request): 114 | """Raise the proper exception based on the response""" 115 | try: 116 | data = request.json() 117 | except: 118 | data = {} 119 | 120 | code = request.status_code 121 | if code == requests.codes.bad: 122 | raise BadRequestException(response=data) 123 | 124 | if code == requests.codes.unauthorized: 125 | raise UnauthorizedException(response=data) 126 | 127 | if code == requests.codes.not_found: 128 | raise NotFoundException(response=data) 129 | 130 | # Generic error fallback 131 | request.raise_for_status() 132 | 133 | def fetch(self): 134 | """Perform a read request against the resource""" 135 | start = datetime.now() 136 | r = requests.get(self._url(), auth=(self.key, "")) 137 | self._delay_for_ratelimits(start) 138 | 139 | if r.status_code not in self.TRUTHY_CODES: 140 | return self._handle_request_exception(r) 141 | 142 | response = r.json() 143 | if self.ENVELOPE: 144 | self.data = response.get(self.ENVELOPE, {}) 145 | else: 146 | self.data = response 147 | 148 | # Move to separate function so it can be overrridden 149 | self._process_meta(response) 150 | 151 | def _process_meta(self, response): 152 | """Process additional data sent in response""" 153 | for key in self.META_ENVELOPES: 154 | self.meta[key] = response.get(key) 155 | 156 | def delete(self): 157 | """Delete the object""" 158 | 159 | start = datetime.now() 160 | r = requests.delete(self._url(), auth=(self.key, "")) 161 | self._delay_for_ratelimits(start) 162 | 163 | if r.status_code not in self.TRUTHY_CODES: 164 | return self._handle_request_exception(r) 165 | 166 | def patch(self, **kwargs): 167 | """Change attributes of the item""" 168 | start = datetime.now() 169 | r = requests.patch(self._url(), auth=(self.key, ""), data=kwargs) 170 | self._delay_for_ratelimits(start) 171 | 172 | if r.status_code not in self.TRUTHY_CODES: 173 | return self._handle_request_exception(r) 174 | 175 | # Refetch for safety. We could modify based on response, 176 | # but I'm afraid of some edge cases and marshal functions. 177 | self.fetch() 178 | 179 | @classmethod 180 | def create(cls, parent=None, **kwargs): 181 | """Create an object and return it""" 182 | 183 | if parent is None: 184 | raise Exception("Parent class is required") 185 | 186 | route = copy(parent.route) 187 | if cls.ID_NAME is not None: 188 | route[cls.ID_NAME] = "" 189 | 190 | obj = cls(key=parent.key, route=route, config=parent.config) 191 | 192 | start = datetime.now() 193 | response = requests.post(obj._url(), auth=(obj.key, ""), data=kwargs) 194 | cls._delay_for_ratelimits(start) 195 | 196 | if response.status_code not in cls.TRUTHY_CODES: 197 | return cls._handle_request_exception(response) 198 | 199 | # No envelope on post requests 200 | data = response.json() 201 | obj.route[obj.ID_NAME] = data.get("id", data.get(obj.ID_NAME)) 202 | obj.data = data 203 | 204 | return obj 205 | 206 | def get_id(self): 207 | return self.data.get("id", self.route.get(self.ID_NAME)) 208 | 209 | @classmethod 210 | def _delay_for_ratelimits(cls, start): 211 | """If request was shorter than max request time, delay""" 212 | stop = datetime.now() 213 | duration_microseconds = (stop - start).microseconds 214 | if duration_microseconds < cls.REQUEST_TIME_MICROSECONDS: 215 | time.sleep((cls.REQUEST_TIME_MICROSECONDS - duration_microseconds) 216 | / MICROSECONDS_PER_SECOND) 217 | 218 | def __str__(self): 219 | return "{} id {}".format(self.__class__.__name__, 220 | self.route.get(self.ID_NAME)) 221 | -------------------------------------------------------------------------------- /staffjoy/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staffjoy/client_python/e8811b0c06651a15e691c96cbfd41e7da4f7f213/staffjoy/resources/__init__.py -------------------------------------------------------------------------------- /staffjoy/resources/admin.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Admin(Resource): 5 | """Organization administrators""" 6 | PATH = "organizations/{organization_id}/admins/{user_id}" 7 | ID_NAME = "user_id" 8 | -------------------------------------------------------------------------------- /staffjoy/resources/apikey.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ApiKey(Resource): 5 | """User session""" 6 | PATH = "users/{user_id}/apikeys/{apikey_id}" 7 | ID_NAME = "apikey_id" 8 | -------------------------------------------------------------------------------- /staffjoy/resources/chomp_task.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ChompTask(Resource): 5 | PATH = "internal/tasking/chomp/{schedule_id}" 6 | ENVELOPE = None 7 | ID_NAME = "schedule_id" 8 | -------------------------------------------------------------------------------- /staffjoy/resources/cron.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Cron(Resource): 5 | PATH = "internal/cron/" 6 | ID_NAME = "" 7 | ENVELOPE = None 8 | 9 | # Usage: cron.getall() 10 | -------------------------------------------------------------------------------- /staffjoy/resources/location.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.role import Role 3 | from staffjoy.resources.manager import Manager 4 | from staffjoy.resources.location_timeclock import LocationTimeclock 5 | from staffjoy.resources.location_time_off_request import LocationTimeOffRequest 6 | from staffjoy.resources.location_shift import LocationShift 7 | 8 | 9 | class Location(Resource): 10 | PATH = "organizations/{organization_id}/locations/{location_id}" 11 | ID_NAME = "location_id" 12 | 13 | def get_roles(self, **kwargs): 14 | return Role.get_all(parent=self, **kwargs) 15 | 16 | def get_role(self, id): 17 | return Role.get(parent=self, id=id) 18 | 19 | def create_role(self, **kwargs): 20 | return Role.create(parent=self, **kwargs) 21 | 22 | def get_managers(self, **kwargs): 23 | return Manager.get_all(parent=self, **kwargs) 24 | 25 | def get_manager(self, id): 26 | return Manager.get(parent=self, id=id) 27 | 28 | def create_manager(self, **kwargs): 29 | """Typically just pass email""" 30 | return Manager.create(parent=self, **kwargs) 31 | 32 | def get_timeclocks(self, **kwargs): 33 | return LocationTimeclock.get_all(parent=self, **kwargs) 34 | 35 | def get_time_off_requests(self, **kwargs): 36 | return LocationTimeOffRequest.get_all(parent=self, **kwargs) 37 | 38 | def get_shifts(self, **kwargs): 39 | return LocationShift.get_all(parent=self, **kwargs) 40 | -------------------------------------------------------------------------------- /staffjoy/resources/location_shift.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class LocationShift(Resource): 5 | """this is only a get collection endpoint""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/shifts/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/location_time_off_request.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class LocationTimeOffRequest(Resource): 5 | """this is only a get collection endpoint""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/timeoffrequests/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/location_timeclock.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class LocationTimeclock(Resource): 5 | """this is only a get collection endpoint""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/timeclocks/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/manager.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Manager(Resource): 5 | """Location managers""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/managers/{user_id}" 7 | ID_NAME = "user_id" 8 | -------------------------------------------------------------------------------- /staffjoy/resources/mobius_task.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class MobiusTask(Resource): 5 | PATH = "internal/tasking/mobius/{schedule_id}" 6 | ID_NAME = "schedule_id" 7 | ENVELOPE = None 8 | -------------------------------------------------------------------------------- /staffjoy/resources/organization.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.location import Location 3 | from staffjoy.resources.admin import Admin 4 | from staffjoy.resources.organization_worker import OrganizationWorker 5 | 6 | 7 | class Organization(Resource): 8 | PATH = "organizations/{organization_id}" 9 | ID_NAME = "organization_id" 10 | 11 | def get_locations(self, **kwargs): 12 | return Location.get_all(parent=self, **kwargs) 13 | 14 | def get_location(self, id): 15 | return Location.get(parent=self, id=id) 16 | 17 | def create_location(self, **kwargs): 18 | return Location.create(parent=self, **kwargs) 19 | 20 | def get_admins(self): 21 | return Admin.get_all(parent=self) 22 | 23 | def get_admin(self, id): 24 | return Admin.get(parent=self, id=id) 25 | 26 | def create_admin(self, **kwargs): 27 | """Typically just pass email""" 28 | return Admin.create(parent=self, **kwargs) 29 | 30 | def get_workers(self, **kwargs): 31 | return OrganizationWorker.get_all(parent=self, **kwargs) 32 | -------------------------------------------------------------------------------- /staffjoy/resources/organization_worker.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class OrganizationWorker(Resource): 5 | """Organization workers - this endpoint is only a get collection""" 6 | PATH = "organizations/{organization_id}/workers/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/plan.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Plan(Resource): 5 | PATH = "plans/{plan_id}" 6 | ID_NAME = "plan_id" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/preference.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Preference(Resource): 5 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/schedules/{schedule_id}/preferences/{user_id}" 6 | ID_NAME = "user_id" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/recurring_shift.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class RecurringShift(Resource): 5 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/recurringshifts/{recurring_shift_id}" 6 | ID_NAME = "recurring_shift_id" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/role.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.worker import Worker 3 | from staffjoy.resources.schedule import Schedule 4 | from staffjoy.resources.shift import Shift 5 | from staffjoy.resources.shift_query import ShiftQuery 6 | from staffjoy.resources.recurring_shift import RecurringShift 7 | 8 | 9 | class Role(Resource): 10 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}" 11 | ID_NAME = "role_id" 12 | 13 | def get_workers(self, **kwargs): 14 | return Worker.get_all(parent=self, **kwargs) 15 | 16 | def get_worker(self, id=id): 17 | return Worker.get(parent=self, id=id) 18 | 19 | def create_worker(self, **kwargs): 20 | return Worker.create(parent=self, **kwargs) 21 | 22 | def get_schedules(self, **kwargs): 23 | return Schedule.get_all(parent=self, **kwargs) 24 | 25 | def get_schedule(self, id): 26 | return Schedule.get(parent=self, id=id) 27 | 28 | def get_shifts(self, **kwargs): 29 | return Shift.get_all(parent=self, **kwargs) 30 | 31 | def get_shift(self, id): 32 | return Shift.get(parent=self, id=id) 33 | 34 | def create_shift(self, **kwargs): 35 | return Shift.create(parent=self, **kwargs) 36 | 37 | def get_shift_query(self, **kwargs): 38 | return ShiftQuery.get_all(parent=self, **kwargs) 39 | 40 | def get_recurring_shifts(self, **kwargs): 41 | return RecurringShift.get_all(parent=self, **kwargs) 42 | 43 | def get_recurring_shift(self, id): 44 | return RecurringShift.get(parent=self, id=id) 45 | 46 | def create_recurring_shift(self, **kwargs): 47 | return RecurringShift.create(parent=self, **kwargs) 48 | -------------------------------------------------------------------------------- /staffjoy/resources/schedule.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.preference import Preference 3 | from staffjoy.resources.schedule_shift import ScheduleShift 4 | from staffjoy.resources.schedule_timeclock import ScheduleTimeclock 5 | from staffjoy.resources.schedule_time_off_request import ScheduleTimeOffRequest 6 | 7 | 8 | class Schedule(Resource): 9 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/schedules/{schedule_id}" 10 | ID_NAME = "schedule_id" 11 | 12 | def get_preferences(self, **kwargs): 13 | return Preference.get_all(parent=self, **kwargs) 14 | 15 | def get_preference(self, id): 16 | """ Get a worker's preference for a given week""" 17 | return Preference.get(parent=self, id=id) 18 | 19 | def create_preference(self, **kwargs): 20 | return Preference.create(parent=self, **kwargs) 21 | 22 | def get_schedule_shifts(self, **kwargs): 23 | return ScheduleShift.get_all(parent=self, **kwargs) 24 | 25 | def get_schedule_timeclocks(self, **kwargs): 26 | return ScheduleTimeclock.get_all(parent=self, **kwargs) 27 | 28 | def get_schedule_time_off_requests(self, **kwargs): 29 | return ScheduleTimeOffRequest.get_all(parent=self, **kwargs) 30 | -------------------------------------------------------------------------------- /staffjoy/resources/schedule_shift.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ScheduleShift(Resource): 5 | """this is only a get collection endpoint""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/schedules/{schedule_id}/shifts/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/schedule_time_off_request.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ScheduleTimeOffRequest(Resource): 5 | """this is only a get collection endpoint""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/schedules/{schedule_id}/timeoffrequests/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/schedule_timeclock.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ScheduleTimeclock(Resource): 5 | """this is only a get collection endpoint""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/schedules/{schedule_id}/timeclocks/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/session.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Session(Resource): 5 | """User session""" 6 | PATH = "users/{user_id}/sessions/{session_id}" 7 | ID_NAME = "session_id" 8 | -------------------------------------------------------------------------------- /staffjoy/resources/shift.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.shift_eligible_workers import ShiftEligibleWorker 3 | 4 | 5 | class Shift(Resource): 6 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/shifts/{shift_id}" 7 | ID_NAME = "shift_id" 8 | 9 | def get_eligible_workers(self, **kwargs): 10 | return ShiftEligibleWorker.get_all(self, **kwargs) 11 | -------------------------------------------------------------------------------- /staffjoy/resources/shift_eligible_workers.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ShiftEligibleWorker(Resource): 5 | """Returns workers that can claim a given shift""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/shifts/{shift_id}/users/{user_id}" 7 | ID_NAME = "user_id" 8 | -------------------------------------------------------------------------------- /staffjoy/resources/shift_query.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class ShiftQuery(Resource): 5 | """Returns workers that can claim a potential shift""" 6 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/shiftquery/" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/time_off_request.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class TimeOffRequest(Resource): 5 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/users/{user_id}/timeoffrequests/{time_off_request_id}" 6 | ID_NAME = "time_off_request_id" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/timeclock.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | 3 | 4 | class Timeclock(Resource): 5 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/users/{user_id}/timeclocks/{timeclock_id}" 6 | ID_NAME = "timeclock_id" 7 | -------------------------------------------------------------------------------- /staffjoy/resources/user.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.session import Session 3 | from staffjoy.resources.apikey import ApiKey 4 | 5 | 6 | class User(Resource): 7 | PATH = "users/{user_id}" 8 | ID_NAME = "user_id" 9 | 10 | def get_sessions(self): 11 | return Session.get_all(parent=self) 12 | 13 | def get_session(self, id): 14 | return Session.get(parent=self, id=id) 15 | 16 | def get_apikeys(self): 17 | return ApiKey.get_all(parent=self) 18 | 19 | def get_apikey(self, id): 20 | return ApiKey.get(parent=self, id=id) 21 | -------------------------------------------------------------------------------- /staffjoy/resources/worker.py: -------------------------------------------------------------------------------- 1 | from staffjoy.resource import Resource 2 | from staffjoy.resources.timeclock import Timeclock 3 | from staffjoy.resources.time_off_request import TimeOffRequest 4 | 5 | 6 | class Worker(Resource): 7 | """Organization administrators""" 8 | PATH = "organizations/{organization_id}/locations/{location_id}/roles/{role_id}/users/{user_id}" 9 | ID_NAME = "user_id" 10 | 11 | def get_timeclocks(self, **kwargs): 12 | return Timeclock.get_all(parent=self, **kwargs) 13 | 14 | def get_timeclock(self, id): 15 | return Timeclock.get(parent=self, id=id) 16 | 17 | def create_timeclock(self, **kwargs): 18 | return Timeclock.create(parent=self, **kwargs) 19 | 20 | def get_time_off_requests(self, **kwargs): 21 | return TimeOffRequest.get_all(parent=self, **kwargs) 22 | 23 | def get_time_off_request(self, id): 24 | return TimeOffRequest.get(parent=self, id=id) 25 | 26 | def create_time_off_request(self, **kwargs): 27 | return TimeOffRequest.create(parent=self, **kwargs) 28 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | # Set up a logger 5 | logger = logging.getLogger() 6 | logger.setLevel(logging.DEBUG) 7 | logger.addHandler(logging.StreamHandler(sys.stdout)) 8 | -------------------------------------------------------------------------------- /test/test_functional.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from staffjoy import Client, UnauthorizedException 6 | 7 | from . import logger 8 | """ 9 | This test file is intended for use in continuous integration. It runs 10 | against the Staging environment of Staffjoy in a dedicated functional 11 | testing organization. We will not be giving public access to Staffjoy 12 | stage, but you can modify this script to run against your own org. 13 | For a developer access, please email help@staffjoy.com 14 | """ 15 | 16 | TEST_ORG = 18 17 | ENV = "stage" 18 | KEY = os.environ.get("STAFFJOY_STAGE_API_KEY") 19 | TEST_WORKER = "feynman@7bridg.es" 20 | 21 | 22 | def test_org_crud(): 23 | c = Client(key=KEY, env=ENV) 24 | 25 | # Just some basic stuff 26 | assert len(c.get_plans()) > 0 27 | 28 | logger.debug("Fetching organization") 29 | o = c.get_organization(TEST_ORG) 30 | 31 | assert o.get_id() == TEST_ORG 32 | 33 | location_count = len(o.get_locations()) 34 | 35 | logger.debug("Changing organization name") 36 | o.patch(name="[In Progress] Continuous integration test") 37 | 38 | logger.debug("Creating a location") 39 | l = o.create_location(name="El Farolito", timezone="America/Los_Angeles") 40 | l_id = l.get_id() 41 | logger.debug("Location id {}".format(l_id)) 42 | 43 | assert l.data.get("name") == "El Farolito" 44 | logger.debug("Changing location name") 45 | l.patch(name="La Taqueria") 46 | 47 | logger.debug("Checking that location is created") 48 | new_location_count = len(o.get_locations()) 49 | assert new_location_count == (location_count + 1) 50 | del l 51 | 52 | logger.debug("Fetching location by ID") 53 | l = o.get_location(l_id) 54 | assert l.data.get("name") == "La Taqueria" 55 | 56 | logger.debug("Testing role crud") 57 | r = l.create_role(name="Kitchen") 58 | r.patch(name="Cocina") 59 | logger.debug("Adding worker") 60 | r.get_workers() 61 | r.create_worker( 62 | email=TEST_WORKER, 63 | min_hours_per_workweek=30, 64 | max_hours_per_workweek=40) 65 | 66 | logger.debug("Deleting worker") 67 | r.delete() 68 | 69 | logger.debug("Deleting location") 70 | l.delete() 71 | del l 72 | logger.debug("Making sure location has been archived") 73 | 74 | loc = o.get_location(l_id) 75 | assert loc.data.get("archived") 76 | 77 | logger.debug("Finishing up") 78 | o.patch(name="Continuous integration test") 79 | all_locations = o.get_locations() 80 | for location in all_locations: 81 | if not location.data.get("archived"): 82 | location.delete() 83 | 84 | for location in o.get_locations(): 85 | assert location.data.get("archived") 86 | --------------------------------------------------------------------------------