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.
- end_point_blank_py-0.2.1/LICENSE +16 -0
- end_point_blank_py-0.2.1/PKG-INFO +36 -0
- end_point_blank_py-0.2.1/README.md +402 -0
- end_point_blank_py-0.2.1/pyproject.toml +33 -0
- end_point_blank_py-0.2.1/setup.cfg +4 -0
- end_point_blank_py-0.2.1/src/end_point_blank/__init__.py +106 -0
- end_point_blank_py-0.2.1/src/end_point_blank/authorization.py +42 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/__init__.py +0 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/_http.py +75 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/authentication_cache.py +107 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/basic_authenticate.py +66 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/bearer_generate.py +24 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/endpoint_authorize.py +91 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/endpoint_update.py +75 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/generate_access_token.py +44 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/route_pattern_finder.py +42 -0
- end_point_blank_py-0.2.1/src/end_point_blank/commands/version_finder.py +81 -0
- end_point_blank_py-0.2.1/src/end_point_blank/configuration.py +164 -0
- end_point_blank_py-0.2.1/src/end_point_blank/django/__init__.py +12 -0
- end_point_blank_py-0.2.1/src/end_point_blank/django/decorators.py +89 -0
- end_point_blank_py-0.2.1/src/end_point_blank/django/endpoint_registrar.py +142 -0
- end_point_blank_py-0.2.1/src/end_point_blank/django/middleware.py +111 -0
- end_point_blank_py-0.2.1/src/end_point_blank/django/versioned.py +26 -0
- end_point_blank_py-0.2.1/src/end_point_blank/fast_json_truncator.py +56 -0
- end_point_blank_py-0.2.1/src/end_point_blank/flask/__init__.py +6 -0
- end_point_blank_py-0.2.1/src/end_point_blank/flask/authenticated.py +52 -0
- end_point_blank_py-0.2.1/src/end_point_blank/flask/authorized.py +61 -0
- end_point_blank_py-0.2.1/src/end_point_blank/flask/endpoint_registrar.py +59 -0
- end_point_blank_py-0.2.1/src/end_point_blank/flask/versioned.py +38 -0
- end_point_blank_py-0.2.1/src/end_point_blank/log_entry.py +33 -0
- end_point_blank_py-0.2.1/src/end_point_blank/masking.py +349 -0
- end_point_blank_py-0.2.1/src/end_point_blank/middleware/__init__.py +0 -0
- end_point_blank_py-0.2.1/src/end_point_blank/middleware/report_interaction.py +112 -0
- end_point_blank_py-0.2.1/src/end_point_blank/request_store.py +45 -0
- end_point_blank_py-0.2.1/src/end_point_blank/session_configuration.py +40 -0
- end_point_blank_py-0.2.1/src/end_point_blank/string_truncator.py +37 -0
- end_point_blank_py-0.2.1/src/end_point_blank/tokens/__init__.py +0 -0
- end_point_blank_py-0.2.1/src/end_point_blank/tokens/access_tokens.py +102 -0
- end_point_blank_py-0.2.1/src/end_point_blank/unauthorized_error.py +7 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/__init__.py +0 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/delayed_writer.py +63 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/direct_writer.py +40 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/exception_writer.py +78 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/log_writer.py +83 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/request_writer.py +107 -0
- end_point_blank_py-0.2.1/src/end_point_blank/writers/response_writer.py +85 -0
- end_point_blank_py-0.2.1/src/end_point_blank/xml_truncator.py +90 -0
- end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/PKG-INFO +36 -0
- end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/SOURCES.txt +60 -0
- end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/dependency_links.txt +1 -0
- end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/requires.txt +13 -0
- end_point_blank_py-0.2.1/src/end_point_blank_py.egg-info/top_level.txt +1 -0
- end_point_blank_py-0.2.1/tests/test_authorization.py +32 -0
- end_point_blank_py-0.2.1/tests/test_configuration.py +134 -0
- end_point_blank_py-0.2.1/tests/test_delayed_writer.py +43 -0
- end_point_blank_py-0.2.1/tests/test_exception_writer_stacktrace.py +36 -0
- end_point_blank_py-0.2.1/tests/test_masking_apply.py +66 -0
- end_point_blank_py-0.2.1/tests/test_masking_backrefs.py +33 -0
- end_point_blank_py-0.2.1/tests/test_masking_jsonpath.py +41 -0
- end_point_blank_py-0.2.1/tests/test_middleware.py +81 -0
- end_point_blank_py-0.2.1/tests/test_request_store.py +43 -0
- 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,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()
|
|
File without changes
|