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.
- pyapiary-2.0.0/PKG-INFO +435 -0
- pyapiary-2.0.0/README.md +409 -0
- pyapiary-2.0.0/pyproject.toml +36 -0
- pyapiary-2.0.0/src/pyapiary/__init__.py +31 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/__init__.py +0 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/broker.py +398 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/flashpoint.py +195 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/generic.py +105 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/ipqs.py +68 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/spycloud.py +207 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/twilio.py +114 -0
- pyapiary-2.0.0/src/pyapiary/api_connectors/urlscan.py +148 -0
- pyapiary-2.0.0/src/pyapiary/dbms_connectors/__init__.py +0 -0
- pyapiary-2.0.0/src/pyapiary/dbms_connectors/elasticsearch.py +143 -0
- pyapiary-2.0.0/src/pyapiary/dbms_connectors/mongo.py +390 -0
- pyapiary-2.0.0/src/pyapiary/dbms_connectors/mongo_async.py +323 -0
- pyapiary-2.0.0/src/pyapiary/dbms_connectors/odbc.py +110 -0
- pyapiary-2.0.0/src/pyapiary/dbms_connectors/splunk.py +131 -0
- pyapiary-2.0.0/src/pyapiary/helpers.py +102 -0
- pyapiary-2.0.0/src/pyapiary/tests/__init__.py +0 -0
- pyapiary-2.0.0/src/pyapiary/tests/conftest.py +47 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_broker/test_integration_broker.py +14 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_broker/test_unit_asyncbroker.py +17 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_broker/test_unit_broker.py +67 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_elasticsearch/test_unit_elasticsearch.py +67 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/cassettes/test_flashpoint_search_fraud_vcr.yaml +66 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/test_integration_flashpoint.py +11 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/test_unit_async_flashpoint.py +49 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_flashpoint/test_unit_flashpoint.py +45 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_generic/cassettes/test_generic_get_github_api.yaml +87 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_generic/test_integration_generic_connector.py +12 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_generic/test_unit_async_generic_connector.py +54 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_generic/test_unit_generic_connector.py +34 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/__init__.py +0 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/cassettes/test_ipqs_malicious_url_vcr.yaml +64 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/test_integration_ipqs.py +13 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/test_unit_async_ipqs.py +53 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_ipqs/test_unit_ipqs.py +45 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_mongodb/test_unit_async_mongo.py +109 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_mongodb/test_unit_mongo.py +219 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_odbc/test_unit_odbc.py +82 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_splunk/test_unit_splunk.py +56 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/cassettes/test_spycloud_ato_search_vcr.yaml +1870 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/test_integration_spycloud.py +12 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/test_unit_async_spycloud.py +44 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_spycloud/test_unit_spycloud.py +46 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_twilio/cassettes/test_lookup_phone_vcr.yaml +68 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_twilio/test_integration_twilio.py +14 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_twilio/test_unit_async_twilio.py +34 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_twilio/test_unit_twilio.py +45 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/cassettes/test_urlscan_results_vcr.yaml +279 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/test_integration_urlscan.py +12 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/test_unit_async_urlscan.py +49 -0
- pyapiary-2.0.0/src/pyapiary/tests/test_urlscan/test_unit_urlscan.py +39 -0
pyapiary-2.0.0/PKG-INFO
ADDED
|
@@ -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
|
+
|