apitally 0.3.0__py3-none-any.whl → 0.3.2__py3-none-any.whl

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.
apitally/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.0"
1
+ __version__ = "0.3.2"
@@ -16,14 +16,16 @@ from apitally.client.base import (
16
16
  ApitallyClientBase,
17
17
  ApitallyKeyCacheBase,
18
18
  )
19
+ from apitally.client.logging import get_logger
19
20
 
20
21
 
21
- logger = logging.getLogger(__name__)
22
+ logger = get_logger(__name__)
22
23
  retry = partial(
23
24
  backoff.on_exception,
24
25
  backoff.expo,
25
26
  httpx.HTTPError,
26
27
  max_tries=3,
28
+ logger=logger,
27
29
  giveup_log_level=logging.WARNING,
28
30
  )
29
31
 
@@ -74,6 +76,13 @@ class ApitallyClient(ApitallyClientBase):
74
76
  def stop_sync_loop(self) -> None:
75
77
  self._stop_sync_loop = True
76
78
 
79
+ async def handle_shutdown(self) -> None:
80
+ if self._sync_loop_task is not None:
81
+ self._sync_loop_task.cancel()
82
+ # Send any remaining requests data before exiting
83
+ async with self.get_http_client() as client:
84
+ await self.send_requests_data(client)
85
+
77
86
  def set_app_info(self, app_info: Dict[str, Any]) -> None:
78
87
  self._app_info_sent = False
79
88
  self._app_info_payload = self.get_info_payload(app_info)
@@ -109,7 +118,7 @@ class ApitallyClient(ApitallyClientBase):
109
118
  self.handle_keys_response(response_data)
110
119
  self._keys_updated_at = time.time()
111
120
  elif self.key_registry.salt is None: # pragma: no cover
112
- logger.error("Initial Apitally API key sync failed")
121
+ logger.critical("Initial Apitally API key sync failed")
113
122
  # Exit because the application will not be able to authenticate requests
114
123
  sys.exit(1)
115
124
  elif (self._keys_updated_at is not None and time.time() - self._keys_updated_at > MAX_QUEUE_TIME) or (
@@ -119,6 +128,7 @@ class ApitallyClient(ApitallyClientBase):
119
128
 
120
129
  @retry(raise_on_giveup=False)
121
130
  async def _send_app_info(self, client: httpx.AsyncClient, payload: Dict[str, Any]) -> None:
131
+ logger.debug("Sending app info")
122
132
  response = await client.post(url="/info", json=payload, timeout=REQUEST_TIMEOUT)
123
133
  if response.status_code == 404 and "Client ID" in response.text:
124
134
  self.stop_sync_loop()
@@ -130,11 +140,13 @@ class ApitallyClient(ApitallyClientBase):
130
140
 
131
141
  @retry()
132
142
  async def _send_requests_data(self, client: httpx.AsyncClient, payload: Dict[str, Any]) -> None:
143
+ logger.debug("Sending requests data")
133
144
  response = await client.post(url="/requests", json=payload)
134
145
  response.raise_for_status()
135
146
 
136
147
  @retry(raise_on_giveup=False)
137
148
  async def _get_keys(self, client: httpx.AsyncClient) -> Dict[str, Any]:
149
+ logger.debug("Updating API keys")
138
150
  response = await client.get(url="/keys")
139
151
  response.raise_for_status()
140
152
  return response.json()
apitally/client/base.py CHANGED
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- import logging
5
4
  import os
6
5
  import re
7
6
  import threading
@@ -15,8 +14,10 @@ from math import floor
15
14
  from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union, cast
16
15
  from uuid import UUID, uuid4
17
16
 
17
+ from apitally.client.logging import get_logger
18
18
 
19
- logger = logging.getLogger(__name__)
19
+
20
+ logger = get_logger(__name__)
20
21
 
21
22
  HUB_BASE_URL = os.getenv("APITALLY_HUB_BASE_URL") or "https://hub.apitally.io"
22
23
  HUB_VERSION = "v1"
@@ -0,0 +1,17 @@
1
+ import logging
2
+ import os
3
+
4
+
5
+ debug = os.getenv("APITALLY_DEBUG", "false").lower() in {"true", "yes", "y", "1"}
6
+ root_logger = logging.getLogger("apitally")
7
+
8
+ if debug:
9
+ root_logger.setLevel(logging.DEBUG)
10
+ console_handler = logging.StreamHandler()
11
+ formatter = logging.Formatter("%(asctime)s [%(name)s] %(levelname)s: %(message)s")
12
+ console_handler.setFormatter(formatter)
13
+ root_logger.addHandler(console_handler)
14
+
15
+
16
+ def get_logger(name: str) -> logging.Logger:
17
+ return logging.getLogger(name)
@@ -17,14 +17,16 @@ from apitally.client.base import (
17
17
  ApitallyClientBase,
18
18
  ApitallyKeyCacheBase,
19
19
  )
20
+ from apitally.client.logging import get_logger
20
21
 
21
22
 
22
- logger = logging.getLogger(__name__)
23
+ logger = get_logger(__name__)
23
24
  retry = partial(
24
25
  backoff.on_exception,
25
26
  backoff.expo,
26
27
  requests.RequestException,
27
28
  max_tries=3,
29
+ logger=logger,
28
30
  giveup_log_level=logging.WARNING,
29
31
  )
30
32
 
@@ -68,24 +70,31 @@ class ApitallyClient(ApitallyClientBase):
68
70
  def start_sync_loop(self) -> None:
69
71
  self._stop_sync_loop.clear()
70
72
  if self._thread is None or not self._thread.is_alive():
71
- self._thread = Thread(target=self._run_sync_loop)
73
+ self._thread = Thread(target=self._run_sync_loop, daemon=True)
72
74
  self._thread.start()
73
75
  register_exit(self.stop_sync_loop)
74
76
 
75
77
  def _run_sync_loop(self) -> None:
76
- first_iteration = True
77
- while not self._stop_sync_loop.is_set():
78
- try:
79
- with requests.Session() as session:
80
- if self.sync_api_keys:
81
- self.get_keys(session)
82
- if not self._app_info_sent and not first_iteration:
83
- self.send_app_info(session)
84
- self.send_requests_data(session)
85
- time.sleep(self.sync_interval)
86
- except Exception as e: # pragma: no cover
87
- logger.exception(e)
88
- first_iteration = False
78
+ try:
79
+ last_sync_time = 0.0
80
+ while not self._stop_sync_loop.is_set():
81
+ try:
82
+ now = time.time()
83
+ if (now - last_sync_time) > self.sync_interval:
84
+ with requests.Session() as session:
85
+ if self.sync_api_keys:
86
+ self.get_keys(session)
87
+ if not self._app_info_sent and last_sync_time > 0: # not on first sync
88
+ self.send_app_info(session)
89
+ self.send_requests_data(session)
90
+ last_sync_time = now
91
+ time.sleep(1)
92
+ except Exception as e: # pragma: no cover
93
+ logger.exception(e)
94
+ finally:
95
+ # Send any remaining requests data before exiting
96
+ with requests.Session() as session:
97
+ self.send_requests_data(session)
89
98
 
90
99
  def stop_sync_loop(self) -> None:
91
100
  self._stop_sync_loop.set()
@@ -125,7 +134,7 @@ class ApitallyClient(ApitallyClientBase):
125
134
  self.handle_keys_response(response_data)
126
135
  self._keys_updated_at = time.time()
127
136
  elif self.key_registry.salt is None: # pragma: no cover
128
- logger.error("Initial Apitally API key sync failed")
137
+ logger.critical("Initial Apitally API key sync failed")
129
138
  # Exit because the application will not be able to authenticate requests
130
139
  sys.exit(1)
131
140
  elif (self._keys_updated_at is not None and time.time() - self._keys_updated_at > MAX_QUEUE_TIME) or (
@@ -135,6 +144,7 @@ class ApitallyClient(ApitallyClientBase):
135
144
 
136
145
  @retry(raise_on_giveup=False)
137
146
  def _send_app_info(self, session: requests.Session, payload: Dict[str, Any]) -> None:
147
+ logger.debug("Sending app info")
138
148
  response = session.post(url=f"{self.hub_url}/info", json=payload, timeout=REQUEST_TIMEOUT)
139
149
  if response.status_code == 404 and "Client ID" in response.text:
140
150
  self.stop_sync_loop()
@@ -146,11 +156,13 @@ class ApitallyClient(ApitallyClientBase):
146
156
 
147
157
  @retry()
148
158
  def _send_requests_data(self, session: requests.Session, payload: Dict[str, Any]) -> None:
159
+ logger.debug("Sending requests data")
149
160
  response = session.post(url=f"{self.hub_url}/requests", json=payload, timeout=REQUEST_TIMEOUT)
150
161
  response.raise_for_status()
151
162
 
152
163
  @retry(raise_on_giveup=False)
153
164
  def _get_keys(self, session: requests.Session) -> Dict[str, Any]:
165
+ logger.debug("Updating API keys")
154
166
  response = session.get(url=f"{self.hub_url}/keys", timeout=REQUEST_TIMEOUT)
155
167
  response.raise_for_status()
156
168
  return response.json()
apitally/starlette.py CHANGED
@@ -5,7 +5,17 @@ import json
5
5
  import sys
6
6
  import time
7
7
  from importlib.metadata import PackageNotFoundError, version
8
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Type
8
+ from typing import (
9
+ TYPE_CHECKING,
10
+ Any,
11
+ Callable,
12
+ Dict,
13
+ List,
14
+ Optional,
15
+ Tuple,
16
+ Type,
17
+ Union,
18
+ )
9
19
 
10
20
  from httpx import HTTPStatusError
11
21
  from starlette.authentication import (
@@ -59,6 +69,7 @@ class ApitallyMiddleware(BaseHTTPMiddleware):
59
69
  )
60
70
  self.client.start_sync_loop()
61
71
  self.delayed_set_app_info(app_version, openapi_url)
72
+ _register_shutdown_handler(app, self.client.handle_shutdown)
62
73
  super().__init__(app)
63
74
 
64
75
  def delayed_set_app_info(self, app_version: Optional[str] = None, openapi_url: Optional[str] = None) -> None:
@@ -221,7 +232,7 @@ def _get_endpoint_info(app: ASGIApp) -> List[EndpointInfo]:
221
232
  return schemas.get_endpoints(routes)
222
233
 
223
234
 
224
- def _get_routes(app: ASGIApp) -> List[BaseRoute]:
235
+ def _get_routes(app: Union[ASGIApp, Router]) -> List[BaseRoute]:
225
236
  if isinstance(app, Router):
226
237
  return app.routes
227
238
  elif hasattr(app, "app"):
@@ -229,6 +240,13 @@ def _get_routes(app: ASGIApp) -> List[BaseRoute]:
229
240
  return [] # pragma: no cover
230
241
 
231
242
 
243
+ def _register_shutdown_handler(app: Union[ASGIApp, Router], shutdown_handler: Callable[[], Any]) -> None:
244
+ if isinstance(app, Router):
245
+ app.add_event_handler("shutdown", shutdown_handler)
246
+ elif hasattr(app, "app"):
247
+ _register_shutdown_handler(app.app, shutdown_handler)
248
+
249
+
232
250
  def _get_versions(app_version: Optional[str]) -> Dict[str, str]:
233
251
  versions = {
234
252
  "python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: apitally
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Apitally client library for Python
5
5
  Home-page: https://docs.apitally.io
6
6
  License: MIT
@@ -30,7 +30,7 @@ Requires-Dist: fastapi (>=0.87.0) ; extra == "fastapi"
30
30
  Requires-Dist: flask (>=2.0.0) ; extra == "flask"
31
31
  Requires-Dist: httpx (>=0.22.0) ; extra == "fastapi" or extra == "starlette"
32
32
  Requires-Dist: requests (>=2.26.0) ; extra == "django-rest-framework" or extra == "django-ninja" or extra == "flask"
33
- Requires-Dist: starlette (>=0.21.0) ; extra == "fastapi" or extra == "starlette"
33
+ Requires-Dist: starlette (>=0.21.0,<1.0.0) ; extra == "fastapi" or extra == "starlette"
34
34
  Project-URL: Documentation, https://docs.apitally.io
35
35
  Project-URL: Repository, https://github.com/apitally/python-client
36
36
  Description-Content-Type: text/markdown
@@ -45,7 +45,7 @@ Description-Content-Type: text/markdown
45
45
 
46
46
  <p align="center"><b>Your refreshingly simple REST API companion.</b></p>
47
47
 
48
- <p align="center"><i>Apitally offers API traffic monitoring and integrated API key management that is extremely easy to set up and use with new and existing API projects. No assumptions made about your infrastructure, no extra tools for you to host and maintain.</i></p>
48
+ <p align="center"><i>Apitally offers busy engineering teams a simple and affordable API monitoring and API key management solution that is easy to set up and use with new and existing API projects.</i></p>
49
49
 
50
50
  <p align="center">🔗 <b><a href="https://apitally.io" target="_blank">apitally.io</a></b></p>
51
51
 
@@ -53,13 +53,13 @@ Description-Content-Type: text/markdown
53
53
 
54
54
  ---
55
55
 
56
- # Apitally client for Python
56
+ # Apitally client library for Python
57
57
 
58
58
  [![Tests](https://github.com/apitally/python-client/actions/workflows/tests.yaml/badge.svg?event=push)](https://github.com/apitally/python-client/actions)
59
59
  [![Codecov](https://codecov.io/gh/apitally/python-client/graph/badge.svg?token=UNLYBY4Y3V)](https://codecov.io/gh/apitally/python-client)
60
60
  [![PyPI](https://img.shields.io/pypi/v/apitally?logo=pypi&logoColor=white&color=%23006dad)](https://pypi.org/project/apitally/)
61
61
 
62
- This client library currently supports the following frameworks:
62
+ This client library for Apitally currently supports the following Python web frameworks:
63
63
 
64
64
  - [FastAPI](https://docs.apitally.io/frameworks/fastapi)
65
65
  - [Starlette](https://docs.apitally.io/frameworks/starlette)
@@ -67,6 +67,14 @@ This client library currently supports the following frameworks:
67
67
  - [Django Ninja](https://docs.apitally.io/frameworks/django-ninja)
68
68
  - [Django REST Framework](https://docs.apitally.io/frameworks/django-rest-framework)
69
69
 
70
+ Learn more about Apitally on our 🌎 [website](https://apitally.io) or check out the 📚 [documentation](https://docs.apitally.io).
71
+
72
+ ## Key features
73
+
74
+ - Middleware for different frameworks to capture metadata about API endpoints, requests and responses (no sensitive data is captured)
75
+ - Non-blocking clients that aggregate and send captured data to Apitally and optionally synchronize API key hashes in 1 minute intervals
76
+ - Functions to easily secure endpoints with API key authentication and permission checks
77
+
70
78
  ## Install
71
79
 
72
80
  Use `pip` to install and provide your framework of choice as an extra, for example:
@@ -79,10 +87,12 @@ The available extras are: `fastapi`, `starlette`, `flask`, `django_ninja` and `d
79
87
 
80
88
  ## Usage
81
89
 
82
- Below are basic usage examples for each supported framework. For further instructions and examples, including how to identify consumers and use API key authentication, check out our [documentation](https://docs.apitally.io/).
90
+ Our [setup guides](https://docs.apitally.io/quickstart) include all the details you need to get started.
83
91
 
84
92
  ### FastAPI
85
93
 
94
+ This is an example of how to add the Apitally middleware to a FastAPI application. For further instructions, see our [setup guide for FastAPI](https://docs.apitally.io/frameworks/fastapi).
95
+
86
96
  ```python
87
97
  from fastapi import FastAPI
88
98
  from apitally.fastapi import ApitallyMiddleware
@@ -97,6 +107,8 @@ app.add_middleware(
97
107
 
98
108
  ### Starlette
99
109
 
110
+ This is an example of how to add the Apitally middleware to a Starlette application. For further instructions, see our [setup guide for Starlette](https://docs.apitally.io/frameworks/starlette).
111
+
100
112
  ```python
101
113
  from starlette.applications import Starlette
102
114
  from apitally.starlette import ApitallyMiddleware
@@ -111,6 +123,8 @@ app.add_middleware(
111
123
 
112
124
  ### Flask
113
125
 
126
+ This is an example of how to add the Apitally middleware to a Flask application. For further instructions, see our [setup guide for Flask](https://docs.apitally.io/frameworks/flask).
127
+
114
128
  ```python
115
129
  from flask import Flask
116
130
  from apitally.flask import ApitallyMiddleware
@@ -125,6 +139,8 @@ app.wsgi_app = ApitallyMiddleware(
125
139
 
126
140
  ### Django Ninja
127
141
 
142
+ This is an example of how to add the Apitally middleware to a Django Ninja application. For further instructions, see our [setup guide for Django Ninja](https://docs.apitally.io/frameworks/django-ninja).
143
+
128
144
  In your Django `settings.py` file:
129
145
 
130
146
  ```python
@@ -140,6 +156,8 @@ APITALLY_MIDDLEWARE = {
140
156
 
141
157
  ### Django REST Framework
142
158
 
159
+ This is an example of how to add the Apitally middleware to a Django REST Framework application. For further instructions, see our [setup guide for Django REST Framework](https://docs.apitally.io/frameworks/django-rest-framework).
160
+
143
161
  In your Django `settings.py` file:
144
162
 
145
163
  ```python
@@ -155,7 +173,7 @@ APITALLY_MIDDLEWARE = {
155
173
 
156
174
  ## Getting help
157
175
 
158
- If you need help please join our [Apitally community on Slack](https://apitally-community.slack.com/) or [create a new discussion](https://github.com/orgs/apitally/discussions/categories/q-a) on GitHub.
176
+ If you need help please [create a new discussion](https://github.com/orgs/apitally/discussions/categories/q-a) on GitHub.
159
177
 
160
178
  ## License
161
179
 
@@ -0,0 +1,17 @@
1
+ apitally/__init__.py,sha256=vNiWJ14r_cw5t_7UDqDQIVZvladKFGyHH2avsLpN7Vg,22
2
+ apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ apitally/client/asyncio.py,sha256=uq1mK3LHSxhpvMY6FT-k9qzWced-v-bmuweBjXMBqkQ,5925
4
+ apitally/client/base.py,sha256=L_9mbabVDikBG0fYOUu-l1y0VjdsZ2qLu9VSmcQveTM,10622
5
+ apitally/client/logging.py,sha256=QMsKIIAFo92PNBUleeTgsrsQa7SEal-oJa1oOHUr1wI,507
6
+ apitally/client/threading.py,sha256=89C6RdyUTTo5dg_ZAG1qIp9cNzzfAa7zEYejyV_gw00,6471
7
+ apitally/django.py,sha256=U7lyjG97FrGGgA6_NHk3qHvWT69D2e5-37rtrS5PQDU,10251
8
+ apitally/django_ninja.py,sha256=TFltgr03FzTnl83sUAXJj7R32u_g9DTZ9p-HuVKs4ZE,2785
9
+ apitally/django_rest_framework.py,sha256=UmJvxxiKGRdaILSbg6jJY_cvAl-mpuPY1pM0FoQ4bg0,1587
10
+ apitally/fastapi.py,sha256=YjnrRis8UG2M6Q3lkwizbtDXU7nPfCA4mebxG8XwveY,3334
11
+ apitally/flask.py,sha256=7RI1odLYPRmlkQc-OU1HZJoHZxx2n249ftgsklpx2bM,6559
12
+ apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ apitally/starlette.py,sha256=C5Mz77BV33jp3HpmDko5Bjl2t7lxq_WUC6Nj3pcX1AU,9771
14
+ apitally-0.3.2.dist-info/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
15
+ apitally-0.3.2.dist-info/METADATA,sha256=DR6-sdXPUywSoGGFf4M0OiDmoszGRW_BKqCuACh2ekc,6745
16
+ apitally-0.3.2.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
17
+ apitally-0.3.2.dist-info/RECORD,,
@@ -1,16 +0,0 @@
1
- apitally/__init__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
2
- apitally/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- apitally/client/asyncio.py,sha256=JR3WLUN9dfvIhLhYA2gNL3v1DqAxLHbUWqko5Z5CKHs,5440
4
- apitally/client/base.py,sha256=dtFSHYTxpAFWqLcCSehzFL0qEWmNMjpBJQGDQMBxfQ4,10596
5
- apitally/client/threading.py,sha256=Y90IdkFwu8ysSzKDOFgPnZYYaEFVvZNGzF4uTMoODhU,5890
6
- apitally/django.py,sha256=U7lyjG97FrGGgA6_NHk3qHvWT69D2e5-37rtrS5PQDU,10251
7
- apitally/django_ninja.py,sha256=TFltgr03FzTnl83sUAXJj7R32u_g9DTZ9p-HuVKs4ZE,2785
8
- apitally/django_rest_framework.py,sha256=UmJvxxiKGRdaILSbg6jJY_cvAl-mpuPY1pM0FoQ4bg0,1587
9
- apitally/fastapi.py,sha256=YjnrRis8UG2M6Q3lkwizbtDXU7nPfCA4mebxG8XwveY,3334
10
- apitally/flask.py,sha256=7RI1odLYPRmlkQc-OU1HZJoHZxx2n249ftgsklpx2bM,6559
11
- apitally/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- apitally/starlette.py,sha256=XOTpCe2Oiv7IvjljVx14w-hTnNzOllxnUpRXxCfeABY,9347
13
- apitally-0.3.0.dist-info/LICENSE,sha256=vbLzC-4TddtXX-_AFEBKMYWRlxC_MN0g66QhPxo8PgY,1065
14
- apitally-0.3.0.dist-info/METADATA,sha256=wsjv5c_GoE2bRby8FWekm4IpLk6IURgHlAFbkSRoh1Q,5453
15
- apitally-0.3.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
16
- apitally-0.3.0.dist-info/RECORD,,