deceit 1.0__tar.gz → 1.2__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deceit
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: deceit
5
5
  Home-page: https://github.com/cscshared/deceit
6
6
  Author: dev
@@ -26,6 +26,8 @@ License-File: LICENSE
26
26
  Requires-Dist: requests
27
27
  Requires-Dist: requests_oauthlib
28
28
  Requires-Dist: pytz
29
+ Requires-Dist: httpx
30
+ Requires-Dist: urllib3
29
31
  Provides-Extra: dev
30
32
  Requires-Dist: pytest; extra == "dev"
31
33
  Requires-Dist: coverage; extra == "dev"
@@ -39,6 +41,9 @@ Requires-Dist: pytest-docker; extra == "dev"
39
41
  Requires-Dist: waddle; extra == "dev"
40
42
  Requires-Dist: convocations; extra == "dev"
41
43
  Requires-Dist: pytest-cov; extra == "dev"
44
+ Requires-Dist: pytest-asyncio; extra == "dev"
45
+ Requires-Dist: pytest-durations; extra == "dev"
46
+ Requires-Dist: pytest-httpx; extra == "dev"
42
47
  Dynamic: author
43
48
  Dynamic: author-email
44
49
  Dynamic: classifier
@@ -58,8 +63,8 @@ Dynamic: summary
58
63
 
59
64
  boilerplate requests code for creating simple api clients. includes
60
65
  a standard requests retry adapter for retrying errors 429, 502, 503, and 504,
61
- and a base api exceptions class that can be used for api-specific error
62
- handling. includes hooks that can be used to add request and response
66
+ and a base api exceptions class that can be used for api-specific error
67
+ handling. includes hooks that can be used to add request and response
63
68
  logging to a database if needed for debugging / traceability.
64
69
 
65
70
  named after a group of lapwings. pax avium.
@@ -85,29 +90,29 @@ class AirflowApiClient(ApiClient):
85
90
 
86
91
  def connections(self):
87
92
  return self.get('connections')
88
-
93
+
89
94
  ```
90
95
 
91
96
  ## anchoring off of base url
92
97
 
93
- if you provide a `base_url` to the constructor of `ApiClient`, all
98
+ if you provide a `base_url` to the constructor of `ApiClient`, all
94
99
  calls to the `ApiClient.send` function will be anchored to the `base_url`.
95
100
  In the `AirflowApiClient` example above, the `get` to the `connections`
96
101
  endpoint will use `posixpath.join` to construct the full url, e.g.,
97
- `http://localhost:8080/api/v1/connections`. It is important to note
98
- that deceit uses `posixpath.join` not `urllib.parse.urljoin`.
102
+ `http://localhost:8080/api/v1/connections`. It is important to note
103
+ that deceit uses `posixpath.join` not `urllib.parse.urljoin`.
99
104
  So make sure not to prefix anchored routes with `/`.
100
105
 
101
106
  ## presend and postsend
102
107
 
103
- The `deceit`-ful `ApiClient` includes hooks for doing `presend` and
104
- `postsend` actions. If you subclass `ApiClient` and override `presend`,
105
- you can perform actions such as logging the `requests.PreparedRequest`
108
+ The `deceit`-ful `ApiClient` includes hooks for doing `presend` and
109
+ `postsend` actions. If you subclass `ApiClient` and override `presend`,
110
+ you can perform actions such as logging the `requests.PreparedRequest`
106
111
  or adding an hmac signature. If you subclass `ApiClient` and override
107
- `postsend` you can add additional post-request, pre-exception handling,
108
- such as logging the request / response cycle. `presend` takes
112
+ `postsend` you can add additional post-request, pre-exception handling,
113
+ such as logging the request / response cycle. `presend` takes
109
114
  one parameter, the `requests.PreparedRequest`, while `postsend` takes
110
- two parameters, the `requests.PreparedRequest` and the
115
+ two parameters, the `requests.PreparedRequest` and the
111
116
  `requests.models.Response`.
112
117
 
113
118
  ## timeout
@@ -119,9 +124,12 @@ the `default_timeout` parameter to the `ApiClient` constructor.
119
124
 
120
125
  ### prerequisites
121
126
 
122
- * python3.9 or python3.10
123
- * docker-compose
127
+ * python3.10+
128
+ * docker-compose -- in the unit tests, we use docker compose to bring up
129
+ airflow to have an api that we can hit.
124
130
  * internet connection
131
+ * having `uv` installed will make things faster, since the standard
132
+ venv setup uses `uv`. you can use `make uv` to install uv.
125
133
 
126
134
  ### getting started
127
135
 
@@ -129,6 +137,7 @@ standard avian setup using `make`
129
137
 
130
138
  ```bash
131
139
  cd /path/to/deceit
140
+ make uv
132
141
  make setup
133
142
  make test
134
143
  ```
@@ -4,8 +4,8 @@
4
4
 
5
5
  boilerplate requests code for creating simple api clients. includes
6
6
  a standard requests retry adapter for retrying errors 429, 502, 503, and 504,
7
- and a base api exceptions class that can be used for api-specific error
8
- handling. includes hooks that can be used to add request and response
7
+ and a base api exceptions class that can be used for api-specific error
8
+ handling. includes hooks that can be used to add request and response
9
9
  logging to a database if needed for debugging / traceability.
10
10
 
11
11
  named after a group of lapwings. pax avium.
@@ -31,29 +31,29 @@ class AirflowApiClient(ApiClient):
31
31
 
32
32
  def connections(self):
33
33
  return self.get('connections')
34
-
34
+
35
35
  ```
36
36
 
37
37
  ## anchoring off of base url
38
38
 
39
- if you provide a `base_url` to the constructor of `ApiClient`, all
39
+ if you provide a `base_url` to the constructor of `ApiClient`, all
40
40
  calls to the `ApiClient.send` function will be anchored to the `base_url`.
41
41
  In the `AirflowApiClient` example above, the `get` to the `connections`
42
42
  endpoint will use `posixpath.join` to construct the full url, e.g.,
43
- `http://localhost:8080/api/v1/connections`. It is important to note
44
- that deceit uses `posixpath.join` not `urllib.parse.urljoin`.
43
+ `http://localhost:8080/api/v1/connections`. It is important to note
44
+ that deceit uses `posixpath.join` not `urllib.parse.urljoin`.
45
45
  So make sure not to prefix anchored routes with `/`.
46
46
 
47
47
  ## presend and postsend
48
48
 
49
- The `deceit`-ful `ApiClient` includes hooks for doing `presend` and
50
- `postsend` actions. If you subclass `ApiClient` and override `presend`,
51
- you can perform actions such as logging the `requests.PreparedRequest`
49
+ The `deceit`-ful `ApiClient` includes hooks for doing `presend` and
50
+ `postsend` actions. If you subclass `ApiClient` and override `presend`,
51
+ you can perform actions such as logging the `requests.PreparedRequest`
52
52
  or adding an hmac signature. If you subclass `ApiClient` and override
53
- `postsend` you can add additional post-request, pre-exception handling,
54
- such as logging the request / response cycle. `presend` takes
53
+ `postsend` you can add additional post-request, pre-exception handling,
54
+ such as logging the request / response cycle. `presend` takes
55
55
  one parameter, the `requests.PreparedRequest`, while `postsend` takes
56
- two parameters, the `requests.PreparedRequest` and the
56
+ two parameters, the `requests.PreparedRequest` and the
57
57
  `requests.models.Response`.
58
58
 
59
59
  ## timeout
@@ -65,9 +65,12 @@ the `default_timeout` parameter to the `ApiClient` constructor.
65
65
 
66
66
  ### prerequisites
67
67
 
68
- * python3.9 or python3.10
69
- * docker-compose
68
+ * python3.10+
69
+ * docker-compose -- in the unit tests, we use docker compose to bring up
70
+ airflow to have an api that we can hit.
70
71
  * internet connection
72
+ * having `uv` installed will make things faster, since the standard
73
+ venv setup uses `uv`. you can use `make uv` to install uv.
71
74
 
72
75
  ### getting started
73
76
 
@@ -75,6 +78,7 @@ standard avian setup using `make`
75
78
 
76
79
  ```bash
77
80
  cd /path/to/deceit
81
+ make uv
78
82
  make setup
79
83
  make test
80
84
  ```
@@ -0,0 +1,160 @@
1
+ import base64
2
+ import json
3
+ import logging
4
+ import posixpath
5
+ from typing import Optional, Dict, Any
6
+ import httpx
7
+ from .encoders import JsonEncoder
8
+ from .exceptions import ApiException
9
+
10
+
11
+ class ApiClient:
12
+ def __init__(self, *args, base_url=None, default_timeout=None,
13
+ exception_class=None, user=None, password=None, **kwargs):
14
+ self.base_url = base_url
15
+ self.default_timeout = default_timeout
16
+ self.log = logging.getLogger(__name__)
17
+ self.exception_class = exception_class or ApiException
18
+ self.m_session: Optional[httpx.AsyncClient] = None
19
+ self.user = user
20
+ self.password = password
21
+
22
+ @property
23
+ def session(self) -> httpx.AsyncClient:
24
+ if self.m_session is None or self.m_session.is_closed:
25
+ self.m_session = httpx.AsyncClient()
26
+ return self.m_session
27
+
28
+ async def close(self):
29
+ if self.m_session and not self.m_session.is_closed:
30
+ await self.m_session.aclose()
31
+
32
+ async def __aenter__(self):
33
+ return self
34
+
35
+ async def __aexit__(self, *exc_info):
36
+ await self.close()
37
+
38
+ async def presend(self, request: httpx.Request): # pragma: no cover
39
+ """override to log requests"""
40
+ pass
41
+
42
+ async def postsend(self, request: httpx.Request, response: httpx.Response): # pragma: no cover
43
+ """override to log responses"""
44
+ pass
45
+
46
+ def headers(self, *args, **kwargs):
47
+ headers = {}
48
+ if self.user and self.password:
49
+ credentials = base64.b64encode(
50
+ f'{self.user}:{self.password}'.encode()
51
+ ).decode()
52
+ headers['Authorization'] = f'Basic {credentials}'
53
+ return headers
54
+
55
+ def get_url(self, route: str) -> str:
56
+ if route.startswith('https:') or route.startswith('http:'):
57
+ return route
58
+ base_url = self.base_url
59
+ return posixpath.join(base_url, route) if base_url else route
60
+
61
+ async def send(
62
+ self,
63
+ method: str,
64
+ route: str,
65
+ params: Optional[Dict[str, str]] = None,
66
+ form_data: Optional[Dict[str, str]] = None,
67
+ json_data: Optional[Dict[Any, Any]] = None,
68
+ raw: bool = False,
69
+ **kwargs) -> Any:
70
+ """base send function that initiates an async http request
71
+ :param method: the type of request such as `get`, `post`, etc.
72
+ :param route: the anchored route to use, remember that routes are
73
+ prefixed to the `base_url`.
74
+ :param params: a dictionary of query parameters to be included in the url
75
+ :param form_data: a dictionary of values that will be sent in the request
76
+ body as form encoded values
77
+ :param json_data: a dictionary of values that will be sent in the
78
+ request json-encoded
79
+ :param raw: a flag that, if specified, will not cause the raising
80
+ of an `ApiException`. Use this flag when you need additional
81
+ values out of the response or want to handle errors differently
82
+ than the default.
83
+ """
84
+ headers = self.headers()
85
+ headers.update(kwargs.pop('headers', None) or {})
86
+ timeout_val = kwargs.pop('timeout', None) or self.default_timeout
87
+ url = self.get_url(route)
88
+ build_kwargs = {}
89
+ if timeout_val is not None:
90
+ build_kwargs['timeout'] = httpx.Timeout(timeout_val)
91
+
92
+ if json_data is not None:
93
+ build_kwargs['content'] = json.dumps(json_data, cls=JsonEncoder)
94
+ headers.setdefault('content-type', 'application/json')
95
+ elif form_data is not None:
96
+ build_kwargs['data'] = form_data
97
+
98
+
99
+ request = self.session.build_request(
100
+ method, url, headers=headers, params=params,
101
+ **build_kwargs, **kwargs)
102
+ await self.presend(request)
103
+ response = await self.session.send(request)
104
+ await self.postsend(request, response)
105
+
106
+ if raw:
107
+ return response
108
+ result = await self.handle_response(response)
109
+ if result:
110
+ return result
111
+ return response
112
+
113
+ async def handle_response(self, response: httpx.Response) -> Optional[Dict[Any, Any]]:
114
+ """handles responses by returning the json dict or raising an
115
+ `ApiException` for non 200-series responses
116
+
117
+ :param response: the response received from the api
118
+ :return: the json dict if we received a successful response or None
119
+ """
120
+ if response.status_code // 100 != 2:
121
+ content = response.content
122
+ text = response.text
123
+ try:
124
+ data = json.loads(text)
125
+ except (json.JSONDecodeError, ValueError):
126
+ data = None
127
+ raise self.exception_class(
128
+ response.status_code,
129
+ content,
130
+ text,
131
+ data,
132
+ dict(response.headers),
133
+ )
134
+ try:
135
+ return response.json()
136
+ except (json.JSONDecodeError, ValueError):
137
+ return None
138
+
139
+ async def get(self, route, params=None, **kwargs):
140
+ return await self.send('get', route, params=params, **kwargs)
141
+
142
+ async def post(self, route, form_data=None, json_data=None, **kwargs):
143
+ return await self.send(
144
+ 'post', route, form_data=form_data, json_data=json_data, **kwargs)
145
+
146
+ async def put(self, route, form_data=None, json_data=None, **kwargs):
147
+ return await self.send(
148
+ 'put', route, form_data=form_data, json_data=json_data, **kwargs)
149
+
150
+ async def delete(self, route, params=None, form_data=None,
151
+ json_data=None, **kwargs):
152
+ return await self.send(
153
+ 'delete', route, params=params, form_data=form_data,
154
+ json_data=json_data, **kwargs)
155
+
156
+ async def patch(self, route, params=None, form_data=None,
157
+ json_data=None, **kwargs):
158
+ return await self.send(
159
+ 'patch', route, params=params, form_data=form_data,
160
+ json_data=json_data, **kwargs)
@@ -18,4 +18,4 @@ def make_json_serializable(value, fn=lambda x: x):
18
18
 
19
19
  class JsonEncoder(json.JSONEncoder):
20
20
  def default(self, o): # pylint: disable=method-hidden
21
- return make_json_serializable(o, super(JsonEncoder, self).default)
21
+ return make_json_serializable(o, super().default)
@@ -29,7 +29,7 @@ class ApiException(Exception):
29
29
  if self.headers:
30
30
  st = json.dumps(self.headers, indent=2)
31
31
  st_headers = f'[{klass}] [{self.status_code}] / headers => {st}'
32
- p.text(f'[{klass}] [{self.status_code}] => {body}' + st_headers)
32
+ p.text(f'[{klass}] [{self.status_code}] => {body}\n{st_headers}')
33
33
  p.text()
34
34
 
35
35
  @classmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deceit
3
- Version: 1.0
3
+ Version: 1.2
4
4
  Summary: deceit
5
5
  Home-page: https://github.com/cscshared/deceit
6
6
  Author: dev
@@ -26,6 +26,8 @@ License-File: LICENSE
26
26
  Requires-Dist: requests
27
27
  Requires-Dist: requests_oauthlib
28
28
  Requires-Dist: pytz
29
+ Requires-Dist: httpx
30
+ Requires-Dist: urllib3
29
31
  Provides-Extra: dev
30
32
  Requires-Dist: pytest; extra == "dev"
31
33
  Requires-Dist: coverage; extra == "dev"
@@ -39,6 +41,9 @@ Requires-Dist: pytest-docker; extra == "dev"
39
41
  Requires-Dist: waddle; extra == "dev"
40
42
  Requires-Dist: convocations; extra == "dev"
41
43
  Requires-Dist: pytest-cov; extra == "dev"
44
+ Requires-Dist: pytest-asyncio; extra == "dev"
45
+ Requires-Dist: pytest-durations; extra == "dev"
46
+ Requires-Dist: pytest-httpx; extra == "dev"
42
47
  Dynamic: author
43
48
  Dynamic: author-email
44
49
  Dynamic: classifier
@@ -58,8 +63,8 @@ Dynamic: summary
58
63
 
59
64
  boilerplate requests code for creating simple api clients. includes
60
65
  a standard requests retry adapter for retrying errors 429, 502, 503, and 504,
61
- and a base api exceptions class that can be used for api-specific error
62
- handling. includes hooks that can be used to add request and response
66
+ and a base api exceptions class that can be used for api-specific error
67
+ handling. includes hooks that can be used to add request and response
63
68
  logging to a database if needed for debugging / traceability.
64
69
 
65
70
  named after a group of lapwings. pax avium.
@@ -85,29 +90,29 @@ class AirflowApiClient(ApiClient):
85
90
 
86
91
  def connections(self):
87
92
  return self.get('connections')
88
-
93
+
89
94
  ```
90
95
 
91
96
  ## anchoring off of base url
92
97
 
93
- if you provide a `base_url` to the constructor of `ApiClient`, all
98
+ if you provide a `base_url` to the constructor of `ApiClient`, all
94
99
  calls to the `ApiClient.send` function will be anchored to the `base_url`.
95
100
  In the `AirflowApiClient` example above, the `get` to the `connections`
96
101
  endpoint will use `posixpath.join` to construct the full url, e.g.,
97
- `http://localhost:8080/api/v1/connections`. It is important to note
98
- that deceit uses `posixpath.join` not `urllib.parse.urljoin`.
102
+ `http://localhost:8080/api/v1/connections`. It is important to note
103
+ that deceit uses `posixpath.join` not `urllib.parse.urljoin`.
99
104
  So make sure not to prefix anchored routes with `/`.
100
105
 
101
106
  ## presend and postsend
102
107
 
103
- The `deceit`-ful `ApiClient` includes hooks for doing `presend` and
104
- `postsend` actions. If you subclass `ApiClient` and override `presend`,
105
- you can perform actions such as logging the `requests.PreparedRequest`
108
+ The `deceit`-ful `ApiClient` includes hooks for doing `presend` and
109
+ `postsend` actions. If you subclass `ApiClient` and override `presend`,
110
+ you can perform actions such as logging the `requests.PreparedRequest`
106
111
  or adding an hmac signature. If you subclass `ApiClient` and override
107
- `postsend` you can add additional post-request, pre-exception handling,
108
- such as logging the request / response cycle. `presend` takes
112
+ `postsend` you can add additional post-request, pre-exception handling,
113
+ such as logging the request / response cycle. `presend` takes
109
114
  one parameter, the `requests.PreparedRequest`, while `postsend` takes
110
- two parameters, the `requests.PreparedRequest` and the
115
+ two parameters, the `requests.PreparedRequest` and the
111
116
  `requests.models.Response`.
112
117
 
113
118
  ## timeout
@@ -119,9 +124,12 @@ the `default_timeout` parameter to the `ApiClient` constructor.
119
124
 
120
125
  ### prerequisites
121
126
 
122
- * python3.9 or python3.10
123
- * docker-compose
127
+ * python3.10+
128
+ * docker-compose -- in the unit tests, we use docker compose to bring up
129
+ airflow to have an api that we can hit.
124
130
  * internet connection
131
+ * having `uv` installed will make things faster, since the standard
132
+ venv setup uses `uv`. you can use `make uv` to install uv.
125
133
 
126
134
  ### getting started
127
135
 
@@ -129,6 +137,7 @@ standard avian setup using `make`
129
137
 
130
138
  ```bash
131
139
  cd /path/to/deceit
140
+ make uv
132
141
  make setup
133
142
  make test
134
143
  ```
@@ -7,6 +7,7 @@ setup.py
7
7
  deceit/__init__.py
8
8
  deceit/adapters.py
9
9
  deceit/api_client.py
10
+ deceit/async_api_client.py
10
11
  deceit/encoders.py
11
12
  deceit/exceptions.py
12
13
  deceit.egg-info/PKG-INFO
@@ -1,6 +1,8 @@
1
1
  requests
2
2
  requests_oauthlib
3
3
  pytz
4
+ httpx
5
+ urllib3
4
6
 
5
7
  [dev]
6
8
  pytest
@@ -15,3 +17,6 @@ pytest-docker
15
17
  waddle
16
18
  convocations
17
19
  pytest-cov
20
+ pytest-asyncio
21
+ pytest-durations
22
+ pytest-httpx
@@ -10,3 +10,6 @@ pytest-docker
10
10
  waddle
11
11
  convocations
12
12
  pytest-cov
13
+ pytest-asyncio
14
+ pytest-durations
15
+ pytest-httpx
@@ -6,7 +6,7 @@ from setuptools import setup
6
6
  # Version info -- read without importing
7
7
  _locals = {}
8
8
 
9
- version = '1.0'
9
+ version = '1.2'
10
10
 
11
11
  # PyYAML ships a split Python 2/3 codebase. Unfortunately, some pip versions
12
12
  # attempt to interpret both halves of PyYAML, yielding SyntaxErrors. Thus, we
@@ -68,6 +68,8 @@ setup(
68
68
  'requests',
69
69
  'requests_oauthlib',
70
70
  'pytz',
71
+ 'httpx',
72
+ 'urllib3',
71
73
  ],
72
74
  extras_require=extras,
73
75
  )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes