end-point-blank-py 0.2.1__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.
Files changed (62) hide show
  1. end_point_blank_py-0.2.1/LICENSE +16 -0
  2. end_point_blank_py-0.2.1/PKG-INFO +36 -0
  3. end_point_blank_py-0.2.1/README.md +402 -0
  4. end_point_blank_py-0.2.1/pyproject.toml +33 -0
  5. end_point_blank_py-0.2.1/setup.cfg +4 -0
  6. end_point_blank_py-0.2.1/src/end_point_blank/__init__.py +106 -0
  7. end_point_blank_py-0.2.1/src/end_point_blank/authorization.py +42 -0
  8. end_point_blank_py-0.2.1/src/end_point_blank/commands/__init__.py +0 -0
  9. end_point_blank_py-0.2.1/src/end_point_blank/commands/_http.py +75 -0
  10. end_point_blank_py-0.2.1/src/end_point_blank/commands/authentication_cache.py +107 -0
  11. end_point_blank_py-0.2.1/src/end_point_blank/commands/basic_authenticate.py +66 -0
  12. end_point_blank_py-0.2.1/src/end_point_blank/commands/bearer_generate.py +24 -0
  13. end_point_blank_py-0.2.1/src/end_point_blank/commands/endpoint_authorize.py +91 -0
  14. end_point_blank_py-0.2.1/src/end_point_blank/commands/endpoint_update.py +75 -0
  15. end_point_blank_py-0.2.1/src/end_point_blank/commands/generate_access_token.py +44 -0
  16. end_point_blank_py-0.2.1/src/end_point_blank/commands/route_pattern_finder.py +42 -0
  17. end_point_blank_py-0.2.1/src/end_point_blank/commands/version_finder.py +81 -0
  18. end_point_blank_py-0.2.1/src/end_point_blank/configuration.py +164 -0
  19. end_point_blank_py-0.2.1/src/end_point_blank/django/__init__.py +12 -0
  20. end_point_blank_py-0.2.1/src/end_point_blank/django/decorators.py +89 -0
  21. end_point_blank_py-0.2.1/src/end_point_blank/django/endpoint_registrar.py +142 -0
  22. end_point_blank_py-0.2.1/src/end_point_blank/django/middleware.py +111 -0
  23. end_point_blank_py-0.2.1/src/end_point_blank/django/versioned.py +26 -0
  24. end_point_blank_py-0.2.1/src/end_point_blank/fast_json_truncator.py +56 -0
  25. end_point_blank_py-0.2.1/src/end_point_blank/flask/__init__.py +6 -0
  26. end_point_blank_py-0.2.1/src/end_point_blank/flask/authenticated.py +52 -0
  27. end_point_blank_py-0.2.1/src/end_point_blank/flask/authorized.py +61 -0
  28. end_point_blank_py-0.2.1/src/end_point_blank/flask/endpoint_registrar.py +59 -0
  29. end_point_blank_py-0.2.1/src/end_point_blank/flask/versioned.py +38 -0
  30. end_point_blank_py-0.2.1/src/end_point_blank/log_entry.py +33 -0
  31. end_point_blank_py-0.2.1/src/end_point_blank/masking.py +349 -0
  32. end_point_blank_py-0.2.1/src/end_point_blank/middleware/__init__.py +0 -0
  33. end_point_blank_py-0.2.1/src/end_point_blank/middleware/report_interaction.py +112 -0
  34. end_point_blank_py-0.2.1/src/end_point_blank/request_store.py +45 -0
  35. end_point_blank_py-0.2.1/src/end_point_blank/session_configuration.py +40 -0
  36. end_point_blank_py-0.2.1/src/end_point_blank/string_truncator.py +37 -0
  37. end_point_blank_py-0.2.1/src/end_point_blank/tokens/__init__.py +0 -0
  38. end_point_blank_py-0.2.1/src/end_point_blank/tokens/access_tokens.py +102 -0
  39. end_point_blank_py-0.2.1/src/end_point_blank/unauthorized_error.py +7 -0
  40. end_point_blank_py-0.2.1/src/end_point_blank/writers/__init__.py +0 -0
  41. end_point_blank_py-0.2.1/src/end_point_blank/writers/delayed_writer.py +63 -0
  42. end_point_blank_py-0.2.1/src/end_point_blank/writers/direct_writer.py +40 -0
  43. end_point_blank_py-0.2.1/src/end_point_blank/writers/exception_writer.py +78 -0
  44. end_point_blank_py-0.2.1/src/end_point_blank/writers/log_writer.py +83 -0
  45. end_point_blank_py-0.2.1/src/end_point_blank/writers/request_writer.py +107 -0
  46. end_point_blank_py-0.2.1/src/end_point_blank/writers/response_writer.py +85 -0
  47. end_point_blank_py-0.2.1/src/end_point_blank/xml_truncator.py +90 -0
  48. end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/PKG-INFO +36 -0
  49. end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/SOURCES.txt +60 -0
  50. end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/dependency_links.txt +1 -0
  51. end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/requires.txt +13 -0
  52. end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/top_level.txt +1 -0
  53. end_point_blank_py-0.2.1/tests/test_authorization.py +32 -0
  54. end_point_blank_py-0.2.1/tests/test_configuration.py +134 -0
  55. end_point_blank_py-0.2.1/tests/test_delayed_writer.py +43 -0
  56. end_point_blank_py-0.2.1/tests/test_exception_writer_stacktrace.py +36 -0
  57. end_point_blank_py-0.2.1/tests/test_masking_apply.py +66 -0
  58. end_point_blank_py-0.2.1/tests/test_masking_backrefs.py +33 -0
  59. end_point_blank_py-0.2.1/tests/test_masking_jsonpath.py +41 -0
  60. end_point_blank_py-0.2.1/tests/test_middleware.py +81 -0
  61. end_point_blank_py-0.2.1/tests/test_request_store.py +43 -0
  62. end_point_blank_py-0.2.1/tests/test_writer_masking.py +18 -0
@@ -0,0 +1,16 @@
1
+ Copyright (c) 2026 EndPointBlank. All rights reserved.
2
+
3
+ This software and its associated documentation (the "Software") are the
4
+ proprietary and confidential property of EndPointBlank. The Software is licensed,
5
+ not sold, and its use is governed by a separate written agreement between you and
6
+ EndPointBlank. No rights are granted except as expressly set out in that agreement.
7
+
8
+ Without the prior written permission of EndPointBlank, you may not copy, modify,
9
+ merge, publish, distribute, sublicense, or sell copies of the Software, in whole
10
+ or in part, by any means.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
14
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL ENDPOINTBLANK BE
15
+ LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE
16
+ SOFTWARE.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: end-point-blank-py
3
+ Version: 0.2.1
4
+ Summary: Python library for EndPointBlank API integration - endpoint tracking, authorization, and error reporting
5
+ Author-email: "Robert A. Lasch" <rlasch@gmail.com>
6
+ License: Copyright (c) 2026 EndPointBlank. All rights reserved.
7
+
8
+ This software and its associated documentation (the "Software") are the
9
+ proprietary and confidential property of EndPointBlank. The Software is licensed,
10
+ not sold, and its use is governed by a separate written agreement between you and
11
+ EndPointBlank. No rights are granted except as expressly set out in that agreement.
12
+
13
+ Without the prior written permission of EndPointBlank, you may not copy, modify,
14
+ merge, publish, distribute, sublicense, or sell copies of the Software, in whole
15
+ or in part, by any means.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
19
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL ENDPOINTBLANK BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE
21
+ SOFTWARE.
22
+
23
+ Classifier: License :: Other/Proprietary License
24
+ Requires-Python: >=3.10
25
+ License-File: LICENSE
26
+ Requires-Dist: requests>=2.28
27
+ Provides-Extra: flask
28
+ Requires-Dist: flask>=2.3; extra == "flask"
29
+ Provides-Extra: django
30
+ Requires-Dist: django>=4.2; extra == "django"
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=7.4; extra == "dev"
33
+ Requires-Dist: pytest-mock>=3.11; extra == "dev"
34
+ Requires-Dist: responses>=0.23; extra == "dev"
35
+ Requires-Dist: flask>=2.3; extra == "dev"
36
+ Dynamic: license-file
@@ -0,0 +1,402 @@
1
+ # EndPointBlank (Python)
2
+
3
+ EndPointBlank client for Python (Django / Flask / any WSGI app) — endpoint tracking, request
4
+ authorization, error/request/response/log reporting, and client-side data masking.
5
+
6
+ ## Installation
7
+
8
+ The package is **not yet published to PyPI**. Install it from source or as a git dependency.
9
+
10
+ ```sh
11
+ pip install end-point-blank-py
12
+ ```
13
+
14
+ Until it's published, install directly from git:
15
+
16
+ ```sh
17
+ pip install "git+https://github.com/EndPointBlank/end_point_blank_py.git"
18
+ ```
19
+
20
+ or add it to your `pyproject.toml` / `requirements.txt` as a VCS dependency:
21
+
22
+ ```
23
+ end-point-blank-py @ git+https://github.com/EndPointBlank/end_point_blank_py.git
24
+ ```
25
+
26
+ Framework extras are available if you want Flask/Django installed alongside it:
27
+
28
+ ```sh
29
+ pip install "end-point-blank-py[flask]"
30
+ pip install "end-point-blank-py[django]"
31
+ ```
32
+
33
+ Requires **Python >= 3.10**.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ import end_point_blank as epb
39
+
40
+ epb.configure(
41
+ client_id="your-client-id",
42
+ client_secret="your-client-secret",
43
+ app_name="my-app",
44
+ environment="production",
45
+ )
46
+
47
+ # Wrap any WSGI app to report requests/responses/errors automatically.
48
+ from end_point_blank.middleware import ReportInteractionMiddleware
49
+
50
+ app = ReportInteractionMiddleware(app)
51
+ ```
52
+
53
+ That's it — every request/response passing through the wrapped app is now reported to
54
+ EndPointBlank, and unhandled exceptions are captured and sent as error reports.
55
+
56
+ ## Configuration
57
+
58
+ Call `end_point_blank.configure(...)` once at startup (e.g. app factory / settings module).
59
+ Only the parameters you pass are updated; everything else keeps its current value.
60
+
61
+ A subset of settings also fall back to `ENDPOINTBLANK_*` environment variables so the library
62
+ works with zero code changes in 12-factor deployments (e.g. container environments where secrets
63
+ are injected as env vars). **Precedence for those settings is:**
64
+
65
+ ```
66
+ explicit configure(...) value > ENDPOINTBLANK_* environment variable > built-in default
67
+ ```
68
+
69
+ Precedence is resolved at *read* time (not at import time), so setting the env var after the
70
+ process has started but before the first call still takes effect as long as `configure()` wasn't
71
+ called with an explicit value for that setting.
72
+
73
+ | `configure()` keyword | Env var fallback | Default | Notes |
74
+ |---|---|---|---|
75
+ | `client_id` | `ENDPOINTBLANK_CLIENT_ID` | `None` | Your EndPointBlank client ID. |
76
+ | `client_secret` | `ENDPOINTBLANK_CLIENT_SECRET` | `None` | Your EndPointBlank client secret. |
77
+ | `base_url` | `ENDPOINTBLANK_BASE_URL` | `https://in.endpointblank.com` | Control-plane API (authorize/authenticate, access tokens, endpoint updates, endpoint errors). |
78
+ | — *(no `configure()` kwarg)* | `ENDPOINTBLANK_LOG_BASE_URL` | `https://log.endpointblank.com` | Log-plane API (requests/responses/logs/application errors). Set via env var or `Configuration().log_base_url = ...` directly — not exposed as a `configure()` parameter. |
79
+ | `app_name` | `ENDPOINTBLANK_APP_NAME` | `None` | Application name reported to the API. |
80
+ | `environment` | `ENDPOINTBLANK_ENV` | `None` | Runtime environment name (e.g. `"production"`). |
81
+ | `worker_count` | — | `4` | Number of background worker threads used when `log_mode=LogMode.DELAYED`. |
82
+ | `log_mode` | — | `LogMode.DIRECT` | `LogMode.DIRECT` sends synchronously; `LogMode.DELAYED` queues onto background workers. |
83
+ | `version_finder` | — | `None` | Optional callable `(environ) -> str \| None` for custom API-version detection, overriding the built-in header/query/path lookup. |
84
+ | `application_version` | — | `None` | Overrides the app version sent in endpoint updates. |
85
+ | `token_ttl` | — | `None` | Optional access-token TTL (seconds) sent to the token endpoint. |
86
+ | `cache_ttl` | — | `300` | Seconds a successful authorization result is cached (keyed on client-auth + path + method). |
87
+ | `masking_rules` | — | `[]` | List of masking rule dicts. See [Data masking](#data-masking). |
88
+ | `mask_hook` | — | `None` | Optional callable `(payload, record_type) -> payload` run after rule-based masking. |
89
+
90
+ `Configuration` is a singleton — `Configuration()` always returns the same instance, and you can
91
+ also read/assign its attributes directly (`Configuration().log_base_url = "..."`) instead of going
92
+ through `configure()`.
93
+
94
+ ### `configure(...)` example
95
+
96
+ ```python
97
+ import end_point_blank as epb
98
+ from end_point_blank.configuration import LogMode
99
+
100
+ epb.configure(
101
+ client_id="your-client-id",
102
+ client_secret="your-client-secret",
103
+ base_url="https://in.endpointblank.com",
104
+ app_name="my-app",
105
+ environment="production",
106
+ log_mode=LogMode.DELAYED, # send reports from background worker threads
107
+ worker_count=8,
108
+ cache_ttl=600,
109
+ )
110
+ ```
111
+
112
+ ### 12-factor / env-var example
113
+
114
+ With these set in the environment, you can call `epb.configure()` with no arguments (or only the
115
+ values not covered by env vars, e.g. `log_mode`):
116
+
117
+ ```sh
118
+ export ENDPOINTBLANK_CLIENT_ID="your-client-id"
119
+ export ENDPOINTBLANK_CLIENT_SECRET="your-client-secret"
120
+ export ENDPOINTBLANK_BASE_URL="https://in.endpointblank.com"
121
+ export ENDPOINTBLANK_LOG_BASE_URL="https://log.endpointblank.com"
122
+ export ENDPOINTBLANK_APP_NAME="my-app"
123
+ export ENDPOINTBLANK_ENV="production"
124
+ ```
125
+
126
+ ```python
127
+ import end_point_blank as epb
128
+
129
+ epb.configure() # picks up all ENDPOINTBLANK_* env vars above
130
+ ```
131
+
132
+ ## Usage
133
+
134
+ ### Authorization
135
+
136
+ `end_point_blank.authorization.Authorization` builds the `Authorization` header used on every
137
+ outbound call to the EndPointBlank API: a cached `Bearer` token if one exists for the target
138
+ hostname, otherwise HTTP Basic auth from `client_id`/`client_secret`.
139
+
140
+ ```python
141
+ from end_point_blank.authorization import Authorization
142
+
143
+ header_value = Authorization.header() # "Basic <base64(client_id:client_secret)>"
144
+ header_value = Authorization.header(hostname="api.example.com") # "Bearer <token>" if cached
145
+ ```
146
+
147
+ Route/endpoint authorization itself is enforced via the Flask/Django decorators below
148
+ (`authenticated`, `authorized`), which call the `commands.basic_authenticate.BasicAuthenticate` and
149
+ `commands.endpoint_authorize.EndpointAuthorize` commands under the hood and raise
150
+ `end_point_blank.unauthorized_error.UnauthorizedError` on a non-201 response:
151
+
152
+ ```python
153
+ from flask import Flask
154
+ from end_point_blank.flask import authenticated, authorized
155
+
156
+ app = Flask(__name__)
157
+
158
+ @app.route("/protected")
159
+ @authenticated
160
+ def protected_view():
161
+ return "Hello, authenticated user!"
162
+
163
+ @app.route("/sensitive")
164
+ @authorized
165
+ def sensitive_view():
166
+ return "Hello, authorized user!"
167
+ ```
168
+
169
+ Successful authorization results are cached in-process for `cache_ttl` seconds (default 300) to
170
+ avoid a network round trip on every request.
171
+
172
+ ### Error, request/response, and log reporting
173
+
174
+ `ReportInteractionMiddleware` (WSGI) or its Django equivalent automatically:
175
+
176
+ - stores the current request `environ` in a thread-local (`RequestStore`), keyed by a UUID taken
177
+ from `X-Request-Id` if present, else a generated one;
178
+ - sends the request payload via `RequestWriter` before the app runs;
179
+ - sends the response payload via `ResponseWriter` after the app runs (in a `finally`, so it fires
180
+ even on error);
181
+ - sends unhandled exceptions via `ExceptionWriter` (re-raising `UnauthorizedError` without
182
+ reporting it, since unauthorized access is expected/normal traffic).
183
+
184
+ ```python
185
+ from end_point_blank.middleware import ReportInteractionMiddleware
186
+
187
+ app = ReportInteractionMiddleware(app) # wraps any WSGI callable
188
+ ```
189
+
190
+ You can also call the writers directly, e.g. from a background job or a non-HTTP context:
191
+
192
+ ```python
193
+ from end_point_blank.writers.exception_writer import ExceptionWriter
194
+ from end_point_blank.writers.log_writer import LogWriter
195
+
196
+ try:
197
+ risky_call()
198
+ except Exception as exc:
199
+ ExceptionWriter.write(exc)
200
+ raise
201
+
202
+ LogWriter.info("Payment processed", {"amount": 42})
203
+ LogWriter.warn("Slow query", {"duration_ms": 812})
204
+ LogWriter.error("Database timeout")
205
+ LogWriter.fatal("Out of memory")
206
+ ```
207
+
208
+ Reports are sent synchronously (`LogMode.DIRECT`, the default) or queued onto background worker
209
+ threads (`LogMode.DELAYED`, `worker_count` workers) depending on configuration. All writer failures
210
+ are caught and logged internally (via the standard `logging` module) — they never raise into your
211
+ application.
212
+
213
+ ### Endpoint registration (Flask / Django)
214
+
215
+ Publish your route list (and any declared API versions) to EndPointBlank so it can associate
216
+ incoming traffic with known endpoints:
217
+
218
+ ```python
219
+ # Flask — call once after the app is fully configured.
220
+ from end_point_blank.flask import register_flask_endpoints, versioned
221
+
222
+ @app.route("/api/v1/users")
223
+ @versioned(["v1", "v2"], state="Current")
224
+ def list_users():
225
+ return []
226
+
227
+ with app.app_context():
228
+ register_flask_endpoints(app)
229
+ ```
230
+
231
+ ```python
232
+ # Django — call once in AppConfig.ready().
233
+ from end_point_blank.django import register_django_endpoints, versioned
234
+
235
+ @versioned(["v1"], state="Deprecated")
236
+ def user_list(request):
237
+ ...
238
+
239
+ class MyAppConfig(AppConfig):
240
+ def ready(self):
241
+ register_django_endpoints()
242
+ ```
243
+
244
+ `@versioned(versions, state="__default__")` can be stacked multiple times on the same view to
245
+ declare more than one lifecycle state (e.g. `"Current"` vs. `"Deprecated"`).
246
+
247
+ ### Data masking
248
+
249
+ Mask sensitive data **before it leaves your app**. Configure an ordered list of rules; each rule
250
+ targets one field and masks by a JSONPath, a regex, or both. (The server-side intake also masks
251
+ independently, so this is defense in depth, not a replacement for it.)
252
+
253
+ ```python
254
+ import end_point_blank as epb
255
+
256
+ epb.configure(
257
+ masking_rules=[
258
+ # Replace any "ssn" field at any depth in the request body.
259
+ {"target": "request_body", "path": "$..ssn", "replacement_value": "***"},
260
+ # Keep first/last 4 of a card number in error messages via backreferences.
261
+ {"target": "error_message", "regex": r"(\d{4})-\d{4}-\d{4}-(\d{4})", "replacement_value": "$1-****-****-$2"},
262
+ ],
263
+ # Optional: runs after the rules; last chance to transform the payload.
264
+ mask_hook=lambda payload, record_type: payload,
265
+ )
266
+ ```
267
+
268
+ Rules are plain dicts.
269
+
270
+ **Rule fields**
271
+
272
+ - `target` — exactly one of `"request_body"`, `"request_headers"`, `"path"`, `"response_body"`,
273
+ `"error_message"`.
274
+ - `path` — an optional JSONPath (supported subset: `$`, `.name`, `['name']`, `[n]`, `.*` / `[*]`,
275
+ and `..name` for recursive descent). Keys are case-sensitive.
276
+ - `regex` — an optional regular expression.
277
+ - `replacement_value` — the replacement string (default `"..."`).
278
+ - `enabled` — optional bool (default `True`); set `False` to keep a rule defined but skip it.
279
+
280
+ **Semantics — path scopes, regex matches within.** With only a `path`, the selected node is
281
+ replaced entirely. With only a `regex`, every matching string is replaced. With both, the regex is
282
+ applied only within the path-selected node(s). When a `regex` is present, `replacement_value`
283
+ supports backreferences: `$1`, `$2`, … insert capture groups (`$0` the whole match; `$$` for a
284
+ literal `$`; an out-of-range or non-participating group expands to `""`).
285
+
286
+ Masking never raises: an uncompilable regex, a blank/malformed/unsupported path, a non-JSON body,
287
+ or a missing/`None` field all degrade to a no-op. Stacktraces and log messages are never masked.
288
+
289
+ ## Framework integration
290
+
291
+ ### WSGI (any framework)
292
+
293
+ ```python
294
+ from end_point_blank.middleware import ReportInteractionMiddleware
295
+
296
+ app = ReportInteractionMiddleware(app)
297
+ ```
298
+
299
+ ### Flask
300
+
301
+ ```python
302
+ from flask import Flask
303
+ from end_point_blank.middleware import ReportInteractionMiddleware
304
+ from end_point_blank.flask import authenticated, authorized, versioned, register_flask_endpoints
305
+
306
+ app = Flask(__name__)
307
+ app.wsgi_app = ReportInteractionMiddleware(app.wsgi_app)
308
+
309
+ @app.route("/api/v1/users")
310
+ @versioned(["v1"], state="Current")
311
+ @authorized
312
+ def list_users():
313
+ return []
314
+
315
+ with app.app_context():
316
+ register_flask_endpoints(app)
317
+ ```
318
+
319
+ ### Django
320
+
321
+ ```python
322
+ # settings.py
323
+ MIDDLEWARE = [
324
+ "end_point_blank.django.ReportInteractionMiddleware",
325
+ # ... your other middleware
326
+ ]
327
+ ```
328
+
329
+ ```python
330
+ # views.py
331
+ from end_point_blank.django import authenticated, authorized, versioned
332
+
333
+ @versioned(["v1", "v2"], state="Current")
334
+ @authorized
335
+ def user_list(request):
336
+ ...
337
+ ```
338
+
339
+ ```python
340
+ # apps.py
341
+ from django.apps import AppConfig
342
+ from end_point_blank.django import register_django_endpoints
343
+
344
+ class MyAppConfig(AppConfig):
345
+ def ready(self):
346
+ register_django_endpoints()
347
+ ```
348
+
349
+ The Django middleware also implements `process_exception`, so errors are still reported even when
350
+ an outer error-rendering middleware converts the exception into a normal response before it would
351
+ otherwise reach `ReportInteractionMiddleware.__call__`'s exception handler.
352
+
353
+ ## Development
354
+
355
+ Clone the repo, then create a virtualenv and install the package in editable mode with dev
356
+ dependencies (the bundled `build.sh` does this for you):
357
+
358
+ ```sh
359
+ ./build.sh
360
+ # or manually:
361
+ python3 -m venv .venv
362
+ .venv/bin/pip install -e ".[dev]"
363
+ ```
364
+
365
+ Run the test suite (the bundled `test.sh` uses `.venv` automatically if present):
366
+
367
+ ```sh
368
+ ./test.sh
369
+ # or:
370
+ python -m pytest
371
+ ```
372
+
373
+ ### Layout
374
+
375
+ ```
376
+ src/end_point_blank/
377
+ ├── __init__.py # configure(...) + public API surface
378
+ ├── configuration.py # Configuration singleton + LogMode
379
+ ├── authorization.py # Authorization header builder
380
+ ├── masking.py # Client-side masking engine (JSONPath subset + regex)
381
+ ├── request_store.py # Thread-local current-request store
382
+ ├── unauthorized_error.py # UnauthorizedError
383
+ ├── log_entry.py # LogEntry value object
384
+ ├── middleware/ # WSGI middleware (ReportInteractionMiddleware)
385
+ ├── writers/ # RequestWriter, ResponseWriter, ExceptionWriter, LogWriter,
386
+ │ # DirectWriter / DelayedWriter transports
387
+ ├── commands/ # HTTP command objects: authorize, authenticate, endpoint
388
+ │ # update, access-token generation, version/route-pattern finders
389
+ ├── tokens/ # Access-token cache
390
+ ├── flask/ # authenticated/authorized/versioned decorators + endpoint registrar
391
+ └── django/ # middleware, decorators, versioned, endpoint registrar
392
+ tests/ # pytest suite mirroring the src/ layout
393
+ ```
394
+
395
+ ## License
396
+
397
+ No `LICENSE` file is currently included in this repository. All rights reserved by the author
398
+ (Robert A. Lasch) until a license is added.
399
+
400
+ ## Links
401
+
402
+ - Repository: https://github.com/EndPointBlank/end_point_blank_py
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "end-point-blank-py"
7
+ version = "0.2.1"
8
+ description = "Python library for EndPointBlank API integration - endpoint tracking, authorization, and error reporting"
9
+ authors = [{ name = "Robert A. Lasch", email = "rlasch@gmail.com" }]
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ classifiers = [
13
+ "License :: Other/Proprietary License",
14
+ ]
15
+ dependencies = [
16
+ "requests>=2.28",
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ flask = ["flask>=2.3"]
21
+ django = ["django>=4.2"]
22
+ dev = [
23
+ "pytest>=7.4",
24
+ "pytest-mock>=3.11",
25
+ "responses>=0.23",
26
+ "flask>=2.3",
27
+ ]
28
+
29
+ [tool.setuptools.packages.find]
30
+ where = ["src"]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,106 @@
1
+ """
2
+ EndPointBlank Python Library
3
+ ============================
4
+
5
+ Endpoint tracking, authorization, and error reporting for Python web applications.
6
+
7
+ Quick start::
8
+
9
+ import end_point_blank as epb
10
+
11
+ epb.configure(
12
+ client_id="your-client-id",
13
+ client_secret="your-client-secret",
14
+ app_name="my-app",
15
+ environment="production",
16
+ )
17
+
18
+ # WSGI middleware (works with any WSGI app)
19
+ from end_point_blank.middleware import ReportInteractionMiddleware
20
+ app = ReportInteractionMiddleware(app)
21
+
22
+ # Flask decorators
23
+ from end_point_blank.flask import authenticated, authorized, versioned
24
+
25
+ # Django middleware: add "end_point_blank.django.ReportInteractionMiddleware"
26
+ # to settings.MIDDLEWARE
27
+
28
+ """
29
+
30
+ from .configuration import Configuration, LogMode
31
+ from .unauthorized_error import UnauthorizedError
32
+
33
+ VERSION = "0.2.1"
34
+
35
+
36
+ def configure(
37
+ *,
38
+ client_id: str = None,
39
+ client_secret: str = None,
40
+ base_url: str = None,
41
+ environment: str = None,
42
+ app_name: str = None,
43
+ worker_count: int = None,
44
+ log_mode: LogMode = None,
45
+ version_finder=None,
46
+ application_version: str = None,
47
+ token_ttl: int = None,
48
+ cache_ttl: int = None,
49
+ masking_rules=None,
50
+ mask_hook=None,
51
+ ) -> None:
52
+ """
53
+ Configure the EndPointBlank library.
54
+
55
+ All parameters are optional; only supplied values are updated.
56
+
57
+ :param client_id: Your EndPointBlank client ID.
58
+ :param client_secret: Your EndPointBlank client secret.
59
+ :param base_url: Override the API base URL (default: ``https://endpointblank.com/api``).
60
+ :param environment: Runtime environment name (e.g. ``"production"``).
61
+ :param app_name: Application name reported to the API.
62
+ :param worker_count: Number of background worker threads for delayed writing (default: 4).
63
+ :param log_mode: :class:`~end_point_blank.configuration.LogMode` — ``DIRECT`` or ``DELAYED``.
64
+ :param version_finder: Optional callable ``(environ) -> str | None`` for custom version detection.
65
+ :param application_version: Override application version sent in endpoint updates.
66
+ :param token_ttl: Optional access token TTL in seconds sent to the token endpoint.
67
+ :param cache_ttl: Credential cache TTL in seconds (default: 300).
68
+ :param masking_rules: List of masking rule dicts (``target``/``path``/``regex``/``replacement_value``).
69
+ :param mask_hook: Optional callable ``(payload, record_type) -> payload`` run after rule-based masking.
70
+ """
71
+ config = Configuration()
72
+ if client_id is not None:
73
+ config.client_id = client_id
74
+ if client_secret is not None:
75
+ config.client_secret = client_secret
76
+ if base_url is not None:
77
+ config.base_url = base_url
78
+ if environment is not None:
79
+ config.environment = environment
80
+ if app_name is not None:
81
+ config.app_name = app_name
82
+ if worker_count is not None:
83
+ config.worker_count = worker_count
84
+ if log_mode is not None:
85
+ config.log_mode = log_mode
86
+ if version_finder is not None:
87
+ config.version_finder = version_finder
88
+ if application_version is not None:
89
+ config.application_version = application_version
90
+ if token_ttl is not None:
91
+ config.token_ttl = token_ttl
92
+ if cache_ttl is not None:
93
+ config.cache_ttl = cache_ttl
94
+ if masking_rules is not None:
95
+ config.masking_rules = masking_rules
96
+ if mask_hook is not None:
97
+ config.mask_hook = mask_hook
98
+
99
+
100
+ __all__ = [
101
+ "configure",
102
+ "Configuration",
103
+ "LogMode",
104
+ "UnauthorizedError",
105
+ "VERSION",
106
+ ]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from typing import Optional
5
+
6
+ from .configuration import Configuration
7
+
8
+
9
+ class Authorization:
10
+ """
11
+ Generates HTTP authorization headers for EndPointBlank API calls.
12
+
13
+ If a valid Bearer token exists for the given hostname it returns a
14
+ ``Bearer <token>`` header; otherwise falls back to HTTP Basic auth
15
+ using the configured ``client_id`` and ``client_secret``.
16
+
17
+ Equivalent to the Ruby gem's ``EndPointBlank::Authorization``.
18
+ """
19
+
20
+ @classmethod
21
+ def header(cls, hostname: Optional[str] = None) -> str:
22
+ """
23
+ Returns a formatted authorization header value.
24
+
25
+ :param hostname: If provided and a valid token is cached for this
26
+ hostname, returns a Bearer token header.
27
+ :returns: ``"Bearer <token>"`` or ``"Basic <credentials>"``
28
+ """
29
+ if hostname:
30
+ # Import here to avoid circular dependency
31
+ from .tokens.access_tokens import AccessTokens
32
+ token = AccessTokens().token(hostname)
33
+ if token:
34
+ return f"Bearer {token}"
35
+ return f"Basic {cls.basic_credentials()}"
36
+
37
+ @staticmethod
38
+ def basic_credentials() -> str:
39
+ """Returns the Base64-encoded ``client_id:client_secret`` string."""
40
+ config = Configuration()
41
+ raw = f"{config.client_id}:{config.client_secret}"
42
+ return base64.b64encode(raw.encode()).decode()