breezeway 0.0.1a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anthony DeGarimore
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.3
2
+ Name: breezeway
3
+ Version: 0.0.1a1
4
+ Summary:
5
+ Requires-Python: >=3.8,<4.0
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: Programming Language :: Python :: 3.8
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
@@ -0,0 +1,9 @@
1
+ from .breezeway_client import BreezewayClient, AsyncBreezewayClient
2
+ from .models.company import Department
3
+
4
+ __all__ = ['BreezewayClient', 'AsyncBreezewayClient']
5
+
6
+ # Package metadata
7
+ __author__ = 'Anthony DeGarimore'
8
+ __email__ = 'Anthony@DeGarimore.com'
9
+ __licence__ = 'MIT'
@@ -0,0 +1,80 @@
1
+ from abc import ABC, abstractmethod
2
+ from os import getenv
3
+ from typing import List, Optional
4
+
5
+ import httpx
6
+
7
+ from .models.auth import JWTAuth
8
+ from .models.company import Company, Subdepartment, Template
9
+
10
+
11
+ class BaseBreezewayClient(ABC):
12
+ HEADERS = {'accept': 'application/json'}
13
+
14
+ def __init__(self, client_id: str, client_secret: str, base_url: str, company_id: Optional[int] = None):
15
+ base_url = base_url or 'https://api.breezeway.io'
16
+ client_id = client_id or getenv('BREEZEWAY_CLIENT_ID')
17
+ client_secret = client_secret or getenv('BREEZEWAY_CLIENT_SECRET')
18
+ if not client_id or not client_secret:
19
+ raise ValueError('client_id and client_secret are required either as parameters or environment variables '
20
+ 'BREEZEWAY_CLIENT_ID and BREEZEWAY_CLIENT_SECRET')
21
+ self.base_url: str = base_url.rstrip('/')
22
+ self._company_id: Optional[int] = int(company_id) if company_id else company_id
23
+ self.auth: httpx.Auth = JWTAuth(self.base_url, client_id, client_secret)
24
+
25
+ @abstractmethod
26
+ def _request(self, method: str, endpoint: str, payload: dict = None) -> dict:
27
+ pass
28
+
29
+ @property
30
+ def company_id(self) -> int:
31
+ if self._company_id:
32
+ return self._company_id
33
+ companies = self.companies()
34
+ if not companies:
35
+ raise RuntimeError('No companies found')
36
+ elif len(companies) > 1:
37
+ raise RuntimeError('Multiple companies found. '
38
+ 'You must specify a company id when initializing the client')
39
+ self._company_id = companies[0].id
40
+ return self._company_id
41
+
42
+ @company_id.setter
43
+ def company_id(self, value: int):
44
+ self._company_id = value
45
+
46
+ def companies(self) -> List[Company]:
47
+ endpoint = '/public/inventory/v1/companies'
48
+ return [Company.from_json(company) for company in self._request('GET', endpoint)]
49
+
50
+ def templates(self) -> List[Template]:
51
+ endpoint = 'public/inventory/v1/companies/templates'
52
+ payload = {'company_id': self.company_id}
53
+ return [Template.from_json(template) for template in self._request('GET', endpoint, payload)]
54
+
55
+ def subdepartments(self) -> List[Subdepartment]:
56
+ endpoint = 'public/inventory/v1/companies/subdepartments'
57
+ payload = {'company_id': self.company_id}
58
+ return [Subdepartment.from_json(subdepartment) for subdepartment in self._request('GET', endpoint, payload)]
59
+
60
+
61
+ class BreezewayClient(BaseBreezewayClient):
62
+ def __init__(self, client_id=None, client_secret=None, base_url=None, company_id: Optional[int] = None):
63
+ super().__init__(client_id, client_secret, base_url, company_id)
64
+ self.client = httpx.Client(auth=self.auth, base_url=self.base_url, headers=self.HEADERS)
65
+
66
+ def _request(self, method: str, endpoint: str, payload: dict = None) -> dict:
67
+ resp = self.client.request(method, endpoint, json=payload)
68
+ resp.read()
69
+ return resp.json()
70
+
71
+
72
+ class AsyncBreezewayClient(BaseBreezewayClient):
73
+ def __init__(self, client_id=None, client_secret=None, base_url=None, company_id: Optional[int] = None):
74
+ super().__init__(client_id, client_secret, base_url, company_id)
75
+ self.client = httpx.AsyncClient(auth=self.auth, base_url=self.base_url, headers=self.HEADERS)
76
+
77
+ async def _request(self, method: str, endpoint: str, payload: dict = None) -> dict:
78
+ resp = await self.client.request(method, endpoint, json=payload)
79
+ await resp.aread()
80
+ return resp.json()
File without changes
@@ -0,0 +1,67 @@
1
+ import logging
2
+ import time
3
+ import typing
4
+
5
+ import httpx
6
+ from httpx import Request, Response
7
+
8
+
9
+ class JWTAuth(httpx.Auth):
10
+ HEADERS = {'accept': 'application/json'}
11
+
12
+ def __init__(self, base_url: str, client_id: str, client_secret: str):
13
+ self.client_id = client_id
14
+ self.client_secret = client_secret
15
+ self.base_url = base_url
16
+ self._access_token = None
17
+ self._refresh_token = None
18
+ self._token_expires_at = 0
19
+
20
+ def build_authentication_request(self) -> httpx.Request:
21
+ url = self.base_url + '/public/auth/v1/'
22
+ payload = {
23
+ 'client_id': self.client_id,
24
+ 'client_secret': self.client_secret
25
+ }
26
+ return httpx.Request("POST", url, headers=self.HEADERS, json=payload)
27
+
28
+ def build_refresh_token_request(self) -> httpx.Request:
29
+ url = self.base_url + '/public/auth/v1/refresh'
30
+ headers = self.HEADERS | {'authorization': f'JWT {self._refresh_token}'}
31
+ return httpx.Request("POST", url, headers=headers)
32
+
33
+ def _update_tokens(self, response: httpx.Response):
34
+ response.raise_for_status()
35
+ body = response.json()
36
+ if not body or 'error' in body:
37
+ logging.error(f"Authentication failed\n\t{body['error'] if 'error' in body else body}")
38
+ raise RuntimeError('Authentication failed')
39
+ self._token_expires_at = time.time() + 86400
40
+ self._access_token = body['access_token']
41
+ self._refresh_token = body['refresh_token']
42
+
43
+ def sync_auth_flow(self, request: Request) -> typing.Generator[Request, Response, None]:
44
+ if self._token_expires_at < time.time(): # if token is expired
45
+ if self._refresh_token:
46
+ response = yield self.build_refresh_token_request()
47
+ response.read()
48
+ self._update_tokens(response)
49
+ else:
50
+ response = yield self.build_authentication_request()
51
+ response.read()
52
+ self._update_tokens(response)
53
+ request.headers['authorization'] = f'JWT {self._access_token}'
54
+ yield request
55
+
56
+ async def async_auth_flow(self, request: Request) -> typing.AsyncGenerator[Request, Response]:
57
+ if self._token_expires_at < time.time(): # if token is expired
58
+ if self._refresh_token:
59
+ response = yield self.build_refresh_token_request()
60
+ await response.aread()
61
+ self._update_tokens(response)
62
+ else:
63
+ response = yield self.build_authentication_request()
64
+ await response.aread()
65
+ self._update_tokens(response)
66
+ request.headers['authorization'] = f'JWT {self._access_token}'
67
+ yield request
@@ -0,0 +1,26 @@
1
+ import json
2
+ import logging
3
+ from dataclasses import fields
4
+ from typing import Type, TypeVar
5
+
6
+ T = TypeVar('T', bound='BaseBreezewayModel')
7
+
8
+
9
+ class BaseBreezewayModel:
10
+
11
+ @classmethod
12
+ def from_json(cls: Type[T], json_data: str | dict) -> T:
13
+ if isinstance(json_data, str):
14
+ json_data = json.loads(json_data)
15
+ field_names = {field.name for field in fields(cls)}
16
+ extra_keys = set(json_data.keys()) - field_names
17
+ if extra_keys:
18
+ logging.warning(f'Ignoring extra JSON values in {str(T)}: {extra_keys}')
19
+ filtered_data = {k: v for k, v in json_data.items() if k in field_names}
20
+ instance = cls(**filtered_data)
21
+ instance.convert_data_types() # Call method to convert data types after JSON import
22
+ return instance
23
+
24
+ def convert_data_types(self):
25
+ """Method to convert data types after JSON import as needed. Add your implementation here."""
26
+ pass
@@ -0,0 +1,42 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from breezeway.models.base import BaseBreezewayModel
5
+
6
+
7
+ class Department(Enum):
8
+ HOUSEKEEPING = 'housekeeping'
9
+ INSPECTION = 'inspection'
10
+ MAINTENANCE = 'maintenance'
11
+
12
+ @property
13
+ def name(self) -> str:
14
+ return {
15
+ Department.HOUSEKEEPING: 'Cleaning',
16
+ Department.INSPECTION: 'Inspection',
17
+ Department.MAINTENANCE: 'Maintenance'
18
+ }[self]
19
+
20
+
21
+ @dataclass
22
+ class Company(BaseBreezewayModel):
23
+ id: int
24
+ name: str
25
+ reference_company_id: str | None = None
26
+
27
+
28
+ @dataclass
29
+ class Subdepartment(BaseBreezewayModel):
30
+ id: int
31
+ name: str
32
+
33
+
34
+ @dataclass
35
+ class Template(BaseBreezewayModel):
36
+ id: int
37
+ name: str
38
+ department: Department
39
+
40
+ def convert_data_types(self):
41
+ if not isinstance(self.department, Department):
42
+ self.department = Department(self.department)
@@ -0,0 +1,10 @@
1
+ [tool.poetry]
2
+ name = "breezeway"
3
+ version = "0.0.1a1"
4
+
5
+ [tool.poetry.dependencies]
6
+ python = "^3.8"
7
+ httpx = "^0.28.1"
8
+
9
+ [build-system]
10
+ requires = ["poetry-core"]