apidepth 0.1.0__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.
- apidepth/__init__.py +220 -0
- apidepth/collector.py +606 -0
- apidepth/configuration.py +95 -0
- apidepth/event.py +60 -0
- apidepth/instrumentation.py +442 -0
- apidepth/integrations/__init__.py +0 -0
- apidepth/integrations/django.py +106 -0
- apidepth/integrations/flask.py +118 -0
- apidepth/py.typed +0 -0
- apidepth/rate_limit_headers.py +245 -0
- apidepth/registry_loader.py +448 -0
- apidepth/vendor_registry.py +319 -0
- apidepth/version.py +9 -0
- apidepth-0.1.0.dist-info/METADATA +282 -0
- apidepth-0.1.0.dist-info/RECORD +17 -0
- apidepth-0.1.0.dist-info/WHEEL +4 -0
- apidepth-0.1.0.dist-info/licenses/LICENSE +21 -0
apidepth/__init__.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Apidepth Python SDK.
|
|
2
|
+
|
|
3
|
+
Tracks outbound API latency, error rates, and rate-limit quota across
|
|
4
|
+
third-party vendors — Stripe, OpenAI, Anthropic, Twilio, GitHub, and more.
|
|
5
|
+
Requires zero changes to existing HTTP call sites.
|
|
6
|
+
|
|
7
|
+
Quick start (non-framework)::
|
|
8
|
+
|
|
9
|
+
import apidepth
|
|
10
|
+
|
|
11
|
+
apidepth.configure(
|
|
12
|
+
api_key="apd_live_...",
|
|
13
|
+
environment="production",
|
|
14
|
+
)
|
|
15
|
+
apidepth.instrument() # patches requests and httpx if installed
|
|
16
|
+
|
|
17
|
+
For **Django**, add ``"apidepth.integrations.django"`` to ``INSTALLED_APPS``
|
|
18
|
+
and set ``APIDEPTH = {"api_key": ...}`` in ``settings.py``.
|
|
19
|
+
|
|
20
|
+
For **Flask**, use ``apidepth.integrations.flask.Apidepth(app)``.
|
|
21
|
+
|
|
22
|
+
Module-level globals
|
|
23
|
+
--------------------
|
|
24
|
+
``_configuration``
|
|
25
|
+
Lazily created :class:`~apidepth.configuration.Configuration` singleton.
|
|
26
|
+
Always access via :func:`get_configuration`.
|
|
27
|
+
|
|
28
|
+
``_logger``
|
|
29
|
+
The ``logging.Logger`` used by all SDK components. Defaults to the
|
|
30
|
+
standard ``"apidepth"`` logger. Override with :func:`set_logger` to
|
|
31
|
+
route SDK log output through a framework logger (Django, Flask).
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import logging
|
|
36
|
+
import platform
|
|
37
|
+
import sys
|
|
38
|
+
from typing import Any, Dict, Optional
|
|
39
|
+
|
|
40
|
+
from apidepth.version import VERSION
|
|
41
|
+
from apidepth.configuration import Configuration
|
|
42
|
+
|
|
43
|
+
__version__ = VERSION
|
|
44
|
+
|
|
45
|
+
_configuration: Optional[Configuration] = None
|
|
46
|
+
_configuration_lock = __import__("threading").Lock()
|
|
47
|
+
_logger: Optional[logging.Logger] = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Public API
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
def configure(**kwargs: Any) -> Configuration:
|
|
55
|
+
"""Set one or more configuration options and return the singleton.
|
|
56
|
+
|
|
57
|
+
Must be called before :func:`instrument` and before any instrumented HTTP
|
|
58
|
+
requests are made. Framework integrations call this automatically inside
|
|
59
|
+
their boot hooks, so manual calls are only needed for non-framework use.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
**kwargs: Any attribute defined on
|
|
63
|
+
:class:`~apidepth.configuration.Configuration`. Common options::
|
|
64
|
+
|
|
65
|
+
apidepth.configure(
|
|
66
|
+
api_key=os.environ["APIDEPTH_API_KEY"],
|
|
67
|
+
environment="production",
|
|
68
|
+
sample_rate=0.5,
|
|
69
|
+
ignored_hosts=["internal.example.com"],
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The updated :class:`~apidepth.configuration.Configuration` singleton.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If an unknown configuration key is passed.
|
|
77
|
+
"""
|
|
78
|
+
from apidepth.configuration import Configuration as _Cfg
|
|
79
|
+
_valid = {k for k in vars(_Cfg()) if not k.startswith("_")}
|
|
80
|
+
unknown = set(kwargs) - _valid
|
|
81
|
+
if unknown:
|
|
82
|
+
raise TypeError(
|
|
83
|
+
f"apidepth.configure() got unexpected keyword argument(s): "
|
|
84
|
+
f"{', '.join(sorted(unknown))}. "
|
|
85
|
+
f"Valid options: {', '.join(sorted(_valid))}."
|
|
86
|
+
)
|
|
87
|
+
config = get_configuration()
|
|
88
|
+
for key, value in kwargs.items():
|
|
89
|
+
setattr(config, key, value)
|
|
90
|
+
return config
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_configuration() -> Configuration:
|
|
94
|
+
"""Return the process-wide :class:`~apidepth.configuration.Configuration` singleton.
|
|
95
|
+
|
|
96
|
+
Creates the singleton with default values on the first call. Thread-safe.
|
|
97
|
+
"""
|
|
98
|
+
global _configuration
|
|
99
|
+
if _configuration is None:
|
|
100
|
+
with _configuration_lock:
|
|
101
|
+
if _configuration is None:
|
|
102
|
+
_configuration = Configuration()
|
|
103
|
+
return _configuration
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_logger() -> logging.Logger:
|
|
107
|
+
"""Return the SDK logger, creating the default one if none has been set.
|
|
108
|
+
|
|
109
|
+
The default logger is ``logging.getLogger("apidepth")``. Framework
|
|
110
|
+
integrations replace it with the framework's own logger via
|
|
111
|
+
:func:`set_logger` so all SDK output flows through the framework's
|
|
112
|
+
logging pipeline.
|
|
113
|
+
"""
|
|
114
|
+
global _logger
|
|
115
|
+
if _logger is None:
|
|
116
|
+
_logger = logging.getLogger("apidepth")
|
|
117
|
+
return _logger
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def set_logger(logger: logging.Logger) -> None:
|
|
121
|
+
"""Replace the SDK logger.
|
|
122
|
+
|
|
123
|
+
Call this inside a framework boot hook before any SDK log output is
|
|
124
|
+
produced. After this call all SDK modules that use
|
|
125
|
+
``logging.getLogger("apidepth")`` will pick up the new logger because
|
|
126
|
+
the Python logging system resolves loggers by name at emit time.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
logger: The replacement :class:`logging.Logger`.
|
|
130
|
+
"""
|
|
131
|
+
global _logger
|
|
132
|
+
_logger = logger
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def instrument() -> None:
|
|
136
|
+
"""Patch installed HTTP client libraries (*requests*, *httpx*).
|
|
137
|
+
|
|
138
|
+
Safe to call multiple times — subsequent calls after the first are
|
|
139
|
+
no-ops. Must be called after :func:`configure`. Framework integrations
|
|
140
|
+
(Django, Flask) call this automatically.
|
|
141
|
+
|
|
142
|
+
After this call, all outbound HTTP requests made through *requests* or
|
|
143
|
+
*httpx* to recognised vendor hostnames are automatically captured and
|
|
144
|
+
enqueued for the next batch flush.
|
|
145
|
+
"""
|
|
146
|
+
from apidepth.instrumentation import instrument as _instrument
|
|
147
|
+
_instrument()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def sdk_metadata() -> Dict[str, Any]:
|
|
151
|
+
"""Return a metadata dict included in every batch payload.
|
|
152
|
+
|
|
153
|
+
The metadata lets the collector correlate data-quality issues with
|
|
154
|
+
specific SDK versions, Python runtimes, and app servers without
|
|
155
|
+
requiring a support ticket. Mirrors the Ruby gem's ``sdk_metadata``
|
|
156
|
+
which includes ``rails_version`` and ``app_server``.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A dict with keys ``name``, ``version``, ``python_version``,
|
|
160
|
+
``python_platform``, and ``app_server``.
|
|
161
|
+
"""
|
|
162
|
+
return {
|
|
163
|
+
"name": "apidepth-python",
|
|
164
|
+
"version": VERSION,
|
|
165
|
+
"python_version": sys.version.split()[0],
|
|
166
|
+
"python_platform": platform.platform(),
|
|
167
|
+
"app_server": _detect_app_server(),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _detect_app_server() -> str:
|
|
172
|
+
"""Detect the WSGI/ASGI server by inspecting already-loaded modules.
|
|
173
|
+
|
|
174
|
+
Uses ``sys.modules`` rather than attempting new imports so that the
|
|
175
|
+
detection is zero-cost and does not force-load server libraries as a
|
|
176
|
+
side-effect. This mirrors the Ruby gem's ``detect_app_server`` which
|
|
177
|
+
uses ``defined?(Puma)`` / ``defined?(Unicorn)`` / ``defined?(PhusionPassenger)``.
|
|
178
|
+
|
|
179
|
+
Servers checked (in priority order):
|
|
180
|
+
|
|
181
|
+
* ``gunicorn`` — most common WSGI server
|
|
182
|
+
* ``uwsgi`` — uWSGI (the ``uwsgi`` C extension module)
|
|
183
|
+
* ``waitress`` — pure-Python WSGI server
|
|
184
|
+
* ``uvicorn`` — ASGI server (FastAPI, Starlette)
|
|
185
|
+
* ``hypercorn`` — ASGI server
|
|
186
|
+
* ``daphne`` — ASGI server (Django Channels)
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
The lowercase server name, or ``"unknown"`` if none is detected.
|
|
190
|
+
"""
|
|
191
|
+
mods = sys.modules
|
|
192
|
+
if "gunicorn" in mods:
|
|
193
|
+
return "gunicorn"
|
|
194
|
+
if "uwsgi" in mods:
|
|
195
|
+
return "uwsgi"
|
|
196
|
+
if "waitress" in mods:
|
|
197
|
+
return "waitress"
|
|
198
|
+
if "uvicorn" in mods:
|
|
199
|
+
return "uvicorn"
|
|
200
|
+
if "hypercorn" in mods:
|
|
201
|
+
return "hypercorn"
|
|
202
|
+
if "daphne" in mods:
|
|
203
|
+
return "daphne"
|
|
204
|
+
return "unknown"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def sanitize_log(s: str) -> str:
|
|
208
|
+
"""Strip CR, LF, and TAB from *s* and truncate to 200 characters.
|
|
209
|
+
|
|
210
|
+
Used throughout the SDK before interpolating untrusted strings (vendor
|
|
211
|
+
names, error messages, hostnames from registry data) into log output.
|
|
212
|
+
Prevents log-injection attacks (CVE-2025-27111 class).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
s: Any string value. Non-string inputs are coerced via ``str()``.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The sanitised, truncated string.
|
|
219
|
+
"""
|
|
220
|
+
return str(s).translate(str.maketrans("\r\n\t", " "))[:200]
|