pyapiary 2.0.0__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 (54) hide show
  1. pyapiary-2.0.0/PKG-INFO +435 -0
  2. pyapiary-2.0.0/README.md +409 -0
  3. pyapiary-2.0.0/pyproject.toml +36 -0
  4. pyapiary-2.0.0/src/pyapiary/__init__.py +31 -0
  5. pyapiary-2.0.0/src/pyapiary/api_connectors/__init__.py +0 -0
  6. pyapiary-2.0.0/src/pyapiary/api_connectors/broker.py +398 -0
  7. pyapiary-2.0.0/src/pyapiary/api_connectors/flashpoint.py +195 -0
  8. pyapiary-2.0.0/src/pyapiary/api_connectors/generic.py +105 -0
  9. pyapiary-2.0.0/src/pyapiary/api_connectors/ipqs.py +68 -0
  10. pyapiary-2.0.0/src/pyapiary/api_connectors/spycloud.py +207 -0
  11. pyapiary-2.0.0/src/pyapiary/api_connectors/twilio.py +114 -0
  12. pyapiary-2.0.0/src/pyapiary/api_connectors/urlscan.py +148 -0
  13. pyapiary-2.0.0/src/pyapiary/dbms_connectors/__init__.py +0 -0
  14. pyapiary-2.0.0/src/pyapiary/dbms_connectors/elasticsearch.py +143 -0
  15. pyapiary-2.0.0/src/pyapiary/dbms_connectors/mongo.py +390 -0
  16. pyapiary-2.0.0/src/pyapiary/dbms_connectors/mongo_async.py +323 -0
  17. pyapiary-2.0.0/src/pyapiary/dbms_connectors/odbc.py +110 -0
  18. pyapiary-2.0.0/src/pyapiary/dbms_connectors/splunk.py +131 -0
  19. pyapiary-2.0.0/src/pyapiary/helpers.py +102 -0
  20. pyapiary-2.0.0/src/pyapiary/tests/__init__.py +0 -0
  21. pyapiary-2.0.0/src/pyapiary/tests/conftest.py +47 -0
  22. pyapiary-2.0.0/src/pyapiary/tests/test_broker/test_integration_broker.py +14 -0
  23. pyapiary-2.0.0/src/pyapiary/tests/test_broker/test_unit_asyncbroker.py +17 -0
  24. pyapiary-2.0.0/src/pyapiary/tests/test_broker/test_unit_broker.py +67 -0
  25. pyapiary-2.0.0/src/pyapiary/tests/test_elasticsearch/test_unit_elasticsearch.py +67 -0
  26. pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml +66 -0
  27. pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/test_integration_flashpoint.py +11 -0
  28. pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/test_unit_async_flashpoint.py +49 -0
  29. pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/test_unit_flashpoint.py +45 -0
  30. pyapiary-2.0.0/src/pyapiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml +87 -0
  31. pyapiary-2.0.0/src/pyapiary/tests/test_generic/test_integration_generic_connector.py +12 -0
  32. pyapiary-2.0.0/src/pyapiary/tests/test_generic/test_unit_async_generic_connector.py +54 -0
  33. pyapiary-2.0.0/src/pyapiary/tests/test_generic/test_unit_generic_connector.py +34 -0
  34. pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/__init__.py +0 -0
  35. pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml +64 -0
  36. pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py +13 -0
  37. pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py +53 -0
  38. pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py +45 -0
  39. pyapiary-2.0.0/src/pyapiary/tests/test_mongodb/test_unit_async_mongo.py +109 -0
  40. pyapiary-2.0.0/src/pyapiary/tests/test_mongodb/test_unit_mongo.py +219 -0
  41. pyapiary-2.0.0/src/pyapiary/tests/test_odbc/test_unit_odbc.py +82 -0
  42. pyapiary-2.0.0/src/pyapiary/tests/test_splunk/test_unit_splunk.py +56 -0
  43. pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml +1870 -0
  44. pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/test_integration_spycloud.py +12 -0
  45. pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/test_unit_async_spycloud.py +44 -0
  46. pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/test_unit_spycloud.py +46 -0
  47. pyapiary-2.0.0/src/pyapiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml +68 -0
  48. pyapiary-2.0.0/src/pyapiary/tests/test_twilio/test_integration_twilio.py +14 -0
  49. pyapiary-2.0.0/src/pyapiary/tests/test_twilio/test_unit_async_twilio.py +34 -0
  50. pyapiary-2.0.0/src/pyapiary/tests/test_twilio/test_unit_twilio.py +45 -0
  51. pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml +279 -0
  52. pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/test_integration_urlscan.py +12 -0
  53. pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/test_unit_async_urlscan.py +49 -0
  54. pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/test_unit_urlscan.py +39 -0
@@ -0,0 +1,435 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyapiary
3
+ Version: 2.0.0
4
+ Summary: A simple, lightweight set of connectors and functions to various APIs and DBMSs, controlled by a central broker.
5
+ Author: Rob D'Aveta
6
+ Author-email: rob.daveta@gmail.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Provides-Extra: odbc
15
+ Requires-Dist: elasticsearch (>=9.1.1,<10.0.0)
16
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
17
+ Requires-Dist: pymongo (>=4.15.0,<5.0.0)
18
+ Requires-Dist: pyodbc (>=5.3.0,<6.0.0) ; extra == "odbc"
19
+ Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
20
+ Requires-Dist: splunk-sdk (>=2.1.1,<3.0.0)
21
+ Requires-Dist: tenacity (>=9.1.2,<10.0.0)
22
+ Project-URL: Homepage, https://github.com/robd518/pyapiary
23
+ Project-URL: Repository, https://github.com/robd518/pyapiary
24
+ Description-Content-Type: text/markdown
25
+
26
+ # pyapiary
27
+
28
+ A clean, modular set of Python connectors and utilities for working with both **APIs** and **DBMS backends**, unified by a centralized `Broker` abstraction and a consistent interface. Designed for easy testing, code reuse, and plug-and-play extensibility.
29
+
30
+ ## ⚠️ **Deprecation notice**
31
+
32
+ `pyapiary` is the successor to the `ppp-connectors` package.
33
+ The original `ppp-connectors` package has been frozen at its final 1.1.13 release
34
+ and will continue to remain available on PyPI for existing users.
35
+ New development and releases are published under the `pyapiary` package name.
36
+
37
+ ## πŸ“š Table of Contents
38
+
39
+ - [Installation](#installation)
40
+ - [API Connectors](#api-connectors)
41
+ - [Async Support](#async-support)
42
+ - [Example (URLScan)](#example-urlscan)
43
+ - [Customizing API Requests with **kwargs](#customizing-api-requests-with-kwargs)
44
+ - [DBMS Connectors](#dbms-connectors)
45
+ - [MongoDB](#mongodb)
46
+ - [Elasticsearch](#elasticsearch)
47
+ - [ODBC](#odbc-eg-postgres-teradata)
48
+ - [Splunk](#splunk)
49
+ - [Testing](#testing)
50
+ - [Unit tests](#unit-tests)
51
+ - [Integration tests](#integration-tests)
52
+ - [Suppress warnings](#suppress-warnings)
53
+ - [Contributing / Adding a Connector](#contributing--adding-a-connector)
54
+ - [Dev Environment](#dev-environment)
55
+ - [Secrets and Redaction](#secrets-and-redaction)
56
+ - [Summary](#summary)
57
+
58
+
59
+
60
+ ---
61
+
62
+ ## πŸ“¦ Installation
63
+
64
+ ```bash
65
+ pip install ppp-connectors
66
+ ```
67
+
68
+ Copy the `.env.example` to `.env` for local development:
69
+
70
+ ```bash
71
+ cp dev_env/.env.example dev_env/.env
72
+ ```
73
+
74
+ Environment variables are loaded automatically via the `combine_env_configs()` helper.
75
+
76
+ ---
77
+
78
+ ## πŸ”Œ API Connectors
79
+
80
+ All API connectors inherit from a common `Broker` abstraction that comes in two flavors:
81
+
82
+ - `Broker` for synchronous usage
83
+ - `AsyncBroker` for asynchronous usage
84
+
85
+ Each API connector has both a sync and async version (e.g., `URLScanConnector` and `AsyncURLScanConnector`) with **identical method names** and consistent behavior. Additionally, both support context-management with `with` and `async with`.
86
+
87
+ ### 🧰 Shared Features
88
+
89
+ - Accept API credentials via env vars or constructor args (`load_env_vars=True`)
90
+ - Unified interface: `.get()`, `.post()`, etc.
91
+ - Custom headers, query params, and body data via `**kwargs`
92
+ - Logging, retry/backoff support
93
+ - Proxy and SSL configuration
94
+ - Optional VCR integration for tests
95
+
96
+ > Choose the version based on your environment:
97
+ > - Use `URLScanConnector` in CLI scripts and sync jobs
98
+ > - Use `AsyncURLScanConnector` in FastAPI or async pipelines
99
+
100
+ ---
101
+
102
+ ### 🌐 Sync Example (URLScan)
103
+ ``` python
104
+ from pyapiary.api_connectors.urlscan import URLScanConnector
105
+
106
+ scanner = URLScanConnector(load_env_vars=True)
107
+ result = scanner.scan(url="https://example.com")
108
+ print(result.json())
109
+ ```
110
+ ---
111
+
112
+ ### ⚑ Async Example (URLScan)
113
+ ``` python
114
+ import asyncio
115
+ from pyapiary.api_connectors.urlscan import AsyncURLScanConnector
116
+
117
+ async def main():
118
+ scanner = AsyncURLScanConnector(load_env_vars=True)
119
+ response = await scanner.scan(url="https://example.com")
120
+ print(await response.json())
121
+
122
+ asyncio.run(main())
123
+ ```
124
+
125
+ ### Customizing API Requests with **kwargs
126
+
127
+ All connector methods accept arbitrary keyword arguments using `**kwargs`. These arguments are passed directly to the underlying `httpx` request methods, enabling support for any feature available in [`httpx`](https://www.python-httpx.org/api/#request) β€” including custom headers, query parameters, timeouts, authentication, and more. Additionally, for APIs that accept arbitrary fields in their request body (like `URLScan`), these can also be passed as part of `**kwargs` and will be merged into the outgoing request. This enables full control over how API requests are constructed without needing to modify connector internals.
128
+
129
+ #### Example (URLScan with custom headers and params)
130
+
131
+ ```python
132
+ result = scanner.scan(
133
+ url="https://example.com",
134
+ visibility="unlisted",
135
+ headers={"X-Custom-Header": "my-value"},
136
+ params={"pretty": "true"}
137
+ )
138
+ ```
139
+
140
+ This pattern allows flexibility without needing to subclass or modify the connector.
141
+
142
+ ### Proxy Awareness
143
+
144
+ API connectors inherit from the `Broker` class and support flexible proxy configuration for outgoing HTTP requests. You can set proxies in multiple ways:
145
+ - a single `proxy` parameter (applies to all requests),
146
+ - a per-scheme `mounts` parameter (e.g., separate proxies for `http` and `https` as a dictionary),
147
+ - or environment variables (from `.env` or OS environment, specifically `HTTP_PROXY` and `HTTPS_PROXY`).
148
+ > 🧠 **Note for async connectors:** Per-scheme `mounts` are not supported by `httpx.AsyncClient`. If you pass `mounts` to an async connector, it will raise a `ValueError`. Use the `proxy` argument or rely on environment variables (`load_env_vars=True`) instead.
149
+
150
+ **Proxy precedence:**
151
+ `mounts` > `proxy` > environment source (`.env` via `load_env_vars=True`, else OS environment if `trust_env=True`) > none.
152
+
153
+ - If you provide explicit `mounts`, these override all other proxy settings.
154
+ - If you set `proxy`, it overrides environment proxies but is overridden by `mounts`.
155
+ - If neither is set, and `load_env_vars=True`, proxy settings are loaded from `.env` via `combine_env_configs()`.
156
+ - If both `.env` and OS environment have the same variable, OS environment takes precedence.
157
+ - If no explicit proxy or mounts are set but `trust_env=True`, HTTPX will use OS environment proxy settings (including `NO_PROXY`).
158
+
159
+ **Examples:**
160
+
161
+ *Using a single proxy:*
162
+ ```python
163
+ from pyapiary.api_connectors.urlscan import URLScanConnector
164
+ conn = URLScanConnector(proxy="http://myproxy:8080")
165
+ ```
166
+
167
+ *Using per-scheme mounts:*
168
+ ```python
169
+ conn = URLScanConnector(mounts={"https://": "http://myproxy:8080", "http://": "http://myproxy2:8888"})
170
+ ```
171
+
172
+ *Loading proxy from `.env`:*
173
+ ```python
174
+ # .env file contains: HTTP_PROXY="http://myproxy:8080"
175
+ conn = URLScanConnector(load_env_vars=True)
176
+ # Uses HTTP_PROXY from .env even if not in OS environment.
177
+ ```
178
+
179
+ **Note:** Any changes to proxy settings require re-instantiating the connector for changes to take effect.
180
+
181
+ ### SSL Verification and Per-Request Options
182
+
183
+ You can now pass any `httpx.Client` keyword arguments (such as `verify=False`, `http2=True`) when instantiating a connector. These options will be applied to all requests made by that connector.
184
+
185
+ Additionally, per-request keyword arguments can be passed to methods like `.get()`, `.post()`, etc., and will be forwarded to `httpx.Client.request` for that single call.
186
+
187
+ Setting `verify=False` disables SSL verification and can be useful for testing against servers with self-signed certificates, but **should not be used in production** unless you understand the security implications.
188
+
189
+ **Examples:**
190
+
191
+ *Disable SSL verification at the connector level:*
192
+ ```python
193
+ conn = URLScanConnector(verify=False)
194
+ response = conn.get("https://self-signed.badssl.com/")
195
+ print(response.status_code)
196
+ ```
197
+
198
+ *Disable SSL verification for a single request:*
199
+ ```python
200
+ conn = URLScanConnector()
201
+ response = conn.get("https://self-signed.badssl.com/", verify=False)
202
+ print(response.status_code)
203
+ ```
204
+
205
+ *Enable HTTP/2:*
206
+ ```python
207
+ conn = URLScanConnector(http2=True)
208
+ response = conn.get("https://nghttp2.org/httpbin/get")
209
+ print(response.http_version)
210
+ ```
211
+
212
+ ---
213
+
214
+ ## πŸ—ƒοΈ DBMS Connectors
215
+
216
+ Each database connector follows a class-based pattern and supports reusable sessions, query helpers, and in some cases bulk helpers (e.g., `insert_many`, `bulk_insert`, etc.).
217
+
218
+ ### MongoDB
219
+
220
+ Note: `query(...)` is deprecated in favor of `find(filter=..., projection=..., batch_size=...)`. The `query` method remains as a compatibility alias and logs a deprecation warning.
221
+
222
+ Sync connector
223
+ ```python
224
+ from pyapiary.dbms_connectors.mongo import MongoConnector
225
+
226
+ # Recommended: use as a context manager (auto-closes)
227
+ with MongoConnector(
228
+ "mongodb://localhost:27017",
229
+ username="root",
230
+ password="example",
231
+ auth_retry_attempts=3,
232
+ auth_retry_wait=1.0,
233
+ ) as conn:
234
+ # Clean up prior test docs
235
+ conn.delete_many("mydb", "mycol", {"_sample": True})
236
+
237
+ # Insert and upsert
238
+ conn.insert_many("mydb", "mycol", [{"_id": 1, "foo": "bar", "_sample": True}])
239
+ conn.upsert_many(
240
+ "mydb",
241
+ "mycol",
242
+ [{"_id": 1, "foo": "baz", "_sample": True}, {"_id": 2, "foo": "qux", "_sample": True}],
243
+ unique_key="_id",
244
+ )
245
+
246
+ # Find with projection and paging
247
+ for doc in conn.find("mydb", "mycol", filter={"_sample": True}, projection={"_id": 1, "foo": 1}, batch_size=100):
248
+ print(doc)
249
+
250
+ # Distinct values
251
+ vals = conn.distinct("mydb", "mycol", key="foo", filter={"_sample": True})
252
+ print(vals)
253
+
254
+ # Manual lifecycle control is also supported
255
+ conn = MongoConnector("mongodb://localhost:27017")
256
+ try:
257
+ list(conn.find("mydb", "mycol", filter={}))
258
+ finally:
259
+ conn.close()
260
+ ```
261
+
262
+ Async connector
263
+ ```python
264
+ import asyncio
265
+ from pyapiary.dbms_connectors.mongo_async import AsyncMongoConnector
266
+
267
+ async def main():
268
+ async with AsyncMongoConnector(
269
+ "mongodb://localhost:27017",
270
+ username="root",
271
+ password="example",
272
+ auth_retry_attempts=3,
273
+ auth_retry_wait=1.0,
274
+ ) as conn:
275
+ await conn.delete_many("mydb", "mycol", {"_sample": True})
276
+ await conn.insert_many("mydb", "mycol", [{"_id": 1, "foo": "bar", "_sample": True}])
277
+ await conn.upsert_many(
278
+ "mydb", "mycol",
279
+ [{"_id": 1, "foo": "baz", "_sample": True}],
280
+ unique_key="_id",
281
+ )
282
+ async for doc in conn.find("mydb", "mycol", filter={"_sample": True}, projection={"_id": 1, "foo": 1}):
283
+ print(doc)
284
+ vals = await conn.distinct("mydb", "mycol", key="foo", filter={"_sample": True})
285
+ print(vals)
286
+
287
+ asyncio.run(main())
288
+ ```
289
+
290
+ ### Elasticsearch
291
+
292
+ ```python
293
+ # The query method returns a generator; use list() or iterate to access results
294
+ from pyapiary.dbms_connectors.elasticsearch import ElasticsearchConnector
295
+
296
+ conn = ElasticsearchConnector(["http://localhost:9200"])
297
+ results = list(conn.query("my-index", {"query": {"match_all": {}}}))
298
+ for doc in results:
299
+ print(doc)
300
+ ```
301
+
302
+ ### ODBC (e.g., Postgres, Teradata)
303
+
304
+ For automatic connection handling, use `ODBCConnector` as a context manager
305
+
306
+ ```python
307
+ from pyapiary.dbms_connectors.odbc import ODBCConnector
308
+
309
+ with ODBCConnector("DSN=PostgresLocal;UID=postgres;PWD=postgres") as db:
310
+ rows = conn.query("SELECT * FROM my_table")
311
+ print(list(rows))
312
+ ```
313
+
314
+ If you'd like to keep manual control, you can still use the `.close()` method
315
+
316
+ ```python
317
+ from pyapiary.dbms_connectors.odbc import ODBCConnector
318
+
319
+ conn = ODBCConnector("DSN=PostgresLocal;UID=postgres;PWD=postgres")
320
+ rows = conn.query("SELECT * FROM my_table")
321
+ print(list(rows))
322
+ conn.close()
323
+ ```
324
+
325
+ ### Splunk
326
+
327
+ ```python
328
+ from pyapiary.dbms_connectors.splunk import SplunkConnector
329
+
330
+ conn = SplunkConnector("localhost", 8089, "admin", "admin123", scheme="https", verify=False)
331
+ results = conn.query("search index=_internal | head 5")
332
+ ```
333
+
334
+ ---
335
+
336
+ ## πŸ§ͺ Testing
337
+
338
+ ### βœ… Unit tests
339
+
340
+ - Located in `tests/<connector_name>/test_unit_<connector>.py`
341
+ - Use mocking (`MagicMock`, `patch`) to avoid hitting external APIs
342
+ - Async connectors use `pytest-asyncio` and require tests to be decorated with `@pytest.mark.asyncio`
343
+
344
+ ### πŸ” Integration tests
345
+
346
+ - Use [VCR.py](https://github.com/kevin1024/vcrpy) to record HTTP interactions
347
+ - Cassettes stored in: `tests/<connector_name>/cassettes/`
348
+ - Automatically redact secrets (API keys, tokens, etc.)
349
+ - Marked with `@pytest.mark.integration`
350
+
351
+ ```bash
352
+ pytest -m integration
353
+ ```
354
+
355
+ ### 🧼 Suppress warnings
356
+
357
+ Add this to `pytest.ini`:
358
+
359
+ ```ini
360
+ [pytest]
361
+ markers =
362
+ integration: marks integration tests
363
+ ```
364
+
365
+ ---
366
+
367
+ ## πŸ§‘β€πŸ’» Contributing / Adding a Connector
368
+
369
+ To add a new connector:
370
+
371
+ 1. **Module**: Place your module in:
372
+ - `src/pyapiary/api_connectors/` for API-based integrations
373
+ - `src/pyapiary/dbms_connectors/` for database-style connectors
374
+
375
+ 2. **Base class**:
376
+ - Use the `Broker` class for APIs
377
+ - Use the appropriate DBMS connector template for DBMSs
378
+
379
+ 3. **Auth**: Pull secrets using `combine_env_configs()` to support `.env`, environment variables, and CI/CD injection.
380
+
381
+ 4. **Testing**:
382
+ - Add unit tests in: `tests/<name>/test_unit_<connector>.py`
383
+ - Add integration tests in: `tests/<name>/test_integration_<connector>.py`
384
+ - Save cassettes in: `tests/<name>/cassettes/`
385
+
386
+ 5. **Docs**:
387
+ - Add an example usage to this `README.md`
388
+ - Document all methods with docstrings
389
+ - Ensure your connector supports logging if `enable_logging=True` is passed
390
+
391
+ 6. **Export**:
392
+ - Optionally expose your connector via `__init__.py` for easier importing
393
+
394
+ ---
395
+
396
+ ## πŸ› οΈ Dev Environment
397
+
398
+ ```bash
399
+ git clone https://github.com/robd518/pyapiary.git
400
+ cd pyapiary
401
+
402
+ cp .env.example .env
403
+
404
+ python -m venv .venv
405
+ source .venv/bin/activate
406
+
407
+ poetry install # if using poetry, or `pip install -e .[dev]`
408
+
409
+ pytest # run all tests
410
+ black . # format code
411
+ flake8 . # linting
412
+ ```
413
+
414
+ ---
415
+
416
+ ## πŸ” Secrets and Redaction
417
+
418
+ Sensitive values like API keys are redacted using the `AUTH_PARAM_REDACT` list in `conftest.py`. This ensures `.yaml` cassettes don’t leak credentials.
419
+
420
+ Redacted fields include:
421
+ - Query/body fields like `api_key`, `key`, `token`
422
+ - Header fields like `Authorization`, `X-API-Key`
423
+ - URI query parameters
424
+
425
+ ---
426
+
427
+ ## βœ… Summary
428
+
429
+ - Centralized request broker for all APIs
430
+ - Full support for both sync and async API connectors with consistent method signatures
431
+ - Robust DBMS connectors
432
+ - Easy-to-write unit and integration tests with automatic redaction
433
+ - Environment-agnostic configuration system
434
+ - VCR-powered CI-friendly test suite
435
+