pympacds-http 0.1.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.
- pympacds_http-0.1.0/PKG-INFO +102 -0
- pympacds_http-0.1.0/README.md +91 -0
- pympacds_http-0.1.0/pyproject.toml +69 -0
- pympacds_http-0.1.0/setup.cfg +4 -0
- pympacds_http-0.1.0/src/pympacds_http/__init__.py +3 -0
- pympacds_http-0.1.0/src/pympacds_http/contracts.py +43 -0
- pympacds_http-0.1.0/src/pympacds_http/service.py +420 -0
- pympacds_http-0.1.0/src/pympacds_http.egg-info/PKG-INFO +102 -0
- pympacds_http-0.1.0/src/pympacds_http.egg-info/SOURCES.txt +15 -0
- pympacds_http-0.1.0/src/pympacds_http.egg-info/dependency_links.txt +1 -0
- pympacds_http-0.1.0/src/pympacds_http.egg-info/entry_points.txt +2 -0
- pympacds_http-0.1.0/src/pympacds_http.egg-info/requires.txt +2 -0
- pympacds_http-0.1.0/src/pympacds_http.egg-info/top_level.txt +1 -0
- pympacds_http-0.1.0/tests/test_contracts.py +65 -0
- pympacds_http-0.1.0/tests/test_dbus_integration.py +156 -0
- pympacds_http-0.1.0/tests/test_schema.py +38 -0
- pympacds_http-0.1.0/tests/test_service.py +388 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pympacds-http
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HTTP client service for pympacds
|
|
5
|
+
Author-email: Oscar Diaz <odiaz@ieee.org>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: pympacds>=0.2.0
|
|
10
|
+
Requires-Dist: aiohttp>=3.0
|
|
11
|
+
|
|
12
|
+
# pympacds-http
|
|
13
|
+
|
|
14
|
+
HTTP client service for [pympacds](https://github.com/dargor0/pympacds). Sends
|
|
15
|
+
data over HTTP on behalf of other services and exposes it over D-Bus, with
|
|
16
|
+
automatic retry and optional mutual TLS.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- Fire-and-forget D-Bus API: `send`, `download`, and `request` each return a
|
|
21
|
+
random request token immediately; the result is delivered asynchronously via
|
|
22
|
+
the `request_completed(token, result)` signal, so callers correlate a call
|
|
23
|
+
with its outcome by token.
|
|
24
|
+
- A single pooled `aiohttp.ClientSession` (native asyncio, no thread
|
|
25
|
+
offloading), created at startup with no network I/O and closed on shutdown.
|
|
26
|
+
- Automatic retry of transient failures (network error, timeout, HTTP 5xx)
|
|
27
|
+
with exponential backoff (`retries`, `retry_backoff_s`).
|
|
28
|
+
- TLS verification control (`tls_verify`) plus mutual TLS via
|
|
29
|
+
`tls_certfile`/`tls_keyfile` (must be provided together).
|
|
30
|
+
- `download` / `request` with a `download_path` write the response body
|
|
31
|
+
atomically to disk (temp file + rename) for binary/large downloads.
|
|
32
|
+
- Runtime configuration: `[http]` keys are writable via the framework
|
|
33
|
+
`ConfigContract`.
|
|
34
|
+
- Exports the framework `HealthContract` (always) and `ConfigContract`
|
|
35
|
+
(opt-in via `[dbus] contract_config = true`) alongside the HTTP contract.
|
|
36
|
+
|
|
37
|
+
### Why aiohttp
|
|
38
|
+
|
|
39
|
+
The service uses [aiohttp](https://docs.aiohttp.org/) because it is asyncio
|
|
40
|
+
native: requests run directly on the framework's single event loop with no
|
|
41
|
+
thread offloading. It is isolated in this service's own package, so the core
|
|
42
|
+
`pympacds` framework keeps its zero-dependency property (the core's
|
|
43
|
+
`httpconfprov` middleware continues to use the stdlib `urllib.request`).
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pympacds-http
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
The service reads its parameters from the `[http]` INI section (see
|
|
54
|
+
`config/http.ini.example`). Key options:
|
|
55
|
+
|
|
56
|
+
| Key | Default | Description |
|
|
57
|
+
|-----|---------|-------------|
|
|
58
|
+
| `timeout_s` | `10` | Per-request timeout |
|
|
59
|
+
| `retries` | `3` | Number of retry attempts |
|
|
60
|
+
| `retry_backoff_s` | `1` | Initial backoff (exponential) between retries |
|
|
61
|
+
| `tls_verify` | `true` | Verify TLS certificates |
|
|
62
|
+
| `tls_certfile` / `tls_keyfile` | `""` | Client cert/key (PEM) for mutual TLS |
|
|
63
|
+
| `max_redirects` | `5` | Maximum redirects to follow |
|
|
64
|
+
| `default_url` | `""` | Target URL used by `send()` (empty = `send()` fails) |
|
|
65
|
+
| `default_method` | `POST` | Default HTTP method used by `send()` |
|
|
66
|
+
| `default_headers` | `""` | Default headers (JSON object) used by `send()` |
|
|
67
|
+
|
|
68
|
+
Run with:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pympacds-http -c /etc/pympacds/http.ini
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## D-Bus API
|
|
75
|
+
|
|
76
|
+
Interface `org.pympacds.HTTP` at object path `/org/pympacds/http`:
|
|
77
|
+
|
|
78
|
+
| Member | Type | Description |
|
|
79
|
+
|--------|------|-------------|
|
|
80
|
+
| `send(payload)` | `send(s) -> s` | Send with default url/method/headers; returns a token |
|
|
81
|
+
| `download(payload, download_path)` | `download(ss) -> s` | Like `send`, but saves the body to `download_path` |
|
|
82
|
+
| `request(method, url, headers, body, download_path)` | `request(sssss) -> s` | Explicit request; empty `download_path` = return body |
|
|
83
|
+
| `request_completed(token, result)` | signal `(ss)` | Emitted on completion; carries the token and JSON result |
|
|
84
|
+
|
|
85
|
+
All sends are **fire-and-forget**: the method returns immediately with a
|
|
86
|
+
randomized token, the request runs asynchronously, and `request_completed` is
|
|
87
|
+
emitted with the full JSON outcome:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"status_code": 200,
|
|
92
|
+
"url": "https://example.com/api",
|
|
93
|
+
"elapsed_ms": 123,
|
|
94
|
+
"body": "...",
|
|
95
|
+
"download_path": null,
|
|
96
|
+
"bytes_written": null,
|
|
97
|
+
"error": null
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The framework `HealthContract` (at `.../health`) and `ConfigContract`
|
|
102
|
+
(`[dbus] contract_config = true`) are also exported.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# pympacds-http
|
|
2
|
+
|
|
3
|
+
HTTP client service for [pympacds](https://github.com/dargor0/pympacds). Sends
|
|
4
|
+
data over HTTP on behalf of other services and exposes it over D-Bus, with
|
|
5
|
+
automatic retry and optional mutual TLS.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Fire-and-forget D-Bus API: `send`, `download`, and `request` each return a
|
|
10
|
+
random request token immediately; the result is delivered asynchronously via
|
|
11
|
+
the `request_completed(token, result)` signal, so callers correlate a call
|
|
12
|
+
with its outcome by token.
|
|
13
|
+
- A single pooled `aiohttp.ClientSession` (native asyncio, no thread
|
|
14
|
+
offloading), created at startup with no network I/O and closed on shutdown.
|
|
15
|
+
- Automatic retry of transient failures (network error, timeout, HTTP 5xx)
|
|
16
|
+
with exponential backoff (`retries`, `retry_backoff_s`).
|
|
17
|
+
- TLS verification control (`tls_verify`) plus mutual TLS via
|
|
18
|
+
`tls_certfile`/`tls_keyfile` (must be provided together).
|
|
19
|
+
- `download` / `request` with a `download_path` write the response body
|
|
20
|
+
atomically to disk (temp file + rename) for binary/large downloads.
|
|
21
|
+
- Runtime configuration: `[http]` keys are writable via the framework
|
|
22
|
+
`ConfigContract`.
|
|
23
|
+
- Exports the framework `HealthContract` (always) and `ConfigContract`
|
|
24
|
+
(opt-in via `[dbus] contract_config = true`) alongside the HTTP contract.
|
|
25
|
+
|
|
26
|
+
### Why aiohttp
|
|
27
|
+
|
|
28
|
+
The service uses [aiohttp](https://docs.aiohttp.org/) because it is asyncio
|
|
29
|
+
native: requests run directly on the framework's single event loop with no
|
|
30
|
+
thread offloading. It is isolated in this service's own package, so the core
|
|
31
|
+
`pympacds` framework keeps its zero-dependency property (the core's
|
|
32
|
+
`httpconfprov` middleware continues to use the stdlib `urllib.request`).
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pympacds-http
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
The service reads its parameters from the `[http]` INI section (see
|
|
43
|
+
`config/http.ini.example`). Key options:
|
|
44
|
+
|
|
45
|
+
| Key | Default | Description |
|
|
46
|
+
|-----|---------|-------------|
|
|
47
|
+
| `timeout_s` | `10` | Per-request timeout |
|
|
48
|
+
| `retries` | `3` | Number of retry attempts |
|
|
49
|
+
| `retry_backoff_s` | `1` | Initial backoff (exponential) between retries |
|
|
50
|
+
| `tls_verify` | `true` | Verify TLS certificates |
|
|
51
|
+
| `tls_certfile` / `tls_keyfile` | `""` | Client cert/key (PEM) for mutual TLS |
|
|
52
|
+
| `max_redirects` | `5` | Maximum redirects to follow |
|
|
53
|
+
| `default_url` | `""` | Target URL used by `send()` (empty = `send()` fails) |
|
|
54
|
+
| `default_method` | `POST` | Default HTTP method used by `send()` |
|
|
55
|
+
| `default_headers` | `""` | Default headers (JSON object) used by `send()` |
|
|
56
|
+
|
|
57
|
+
Run with:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pympacds-http -c /etc/pympacds/http.ini
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## D-Bus API
|
|
64
|
+
|
|
65
|
+
Interface `org.pympacds.HTTP` at object path `/org/pympacds/http`:
|
|
66
|
+
|
|
67
|
+
| Member | Type | Description |
|
|
68
|
+
|--------|------|-------------|
|
|
69
|
+
| `send(payload)` | `send(s) -> s` | Send with default url/method/headers; returns a token |
|
|
70
|
+
| `download(payload, download_path)` | `download(ss) -> s` | Like `send`, but saves the body to `download_path` |
|
|
71
|
+
| `request(method, url, headers, body, download_path)` | `request(sssss) -> s` | Explicit request; empty `download_path` = return body |
|
|
72
|
+
| `request_completed(token, result)` | signal `(ss)` | Emitted on completion; carries the token and JSON result |
|
|
73
|
+
|
|
74
|
+
All sends are **fire-and-forget**: the method returns immediately with a
|
|
75
|
+
randomized token, the request runs asynchronously, and `request_completed` is
|
|
76
|
+
emitted with the full JSON outcome:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"status_code": 200,
|
|
81
|
+
"url": "https://example.com/api",
|
|
82
|
+
"elapsed_ms": 123,
|
|
83
|
+
"body": "...",
|
|
84
|
+
"download_path": null,
|
|
85
|
+
"bytes_written": null,
|
|
86
|
+
"error": null
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The framework `HealthContract` (at `.../health`) and `ConfigContract`
|
|
91
|
+
(`[dbus] contract_config = true`) are also exported.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pympacds-http"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "HTTP client service for pympacds"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name="Oscar Diaz", email="odiaz@ieee.org"}
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"pympacds>=0.2.0",
|
|
17
|
+
"aiohttp>=3.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
pympacds-http = "pympacds_http.service:main"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
asyncio_mode = "auto"
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
pythonpath = ["src", "../../src"]
|
|
30
|
+
addopts = [
|
|
31
|
+
"-v",
|
|
32
|
+
"--tb=short",
|
|
33
|
+
"--cov=pympacds_http",
|
|
34
|
+
"--cov-report=term-missing",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 100
|
|
39
|
+
target-version = "py310"
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "W", "I", "B", "UP"]
|
|
43
|
+
|
|
44
|
+
# D-Bus type-signature string annotations ("s", "b", "ss", ...) are the
|
|
45
|
+
# dbus-fast/dbus-next convention, not Python type references.
|
|
46
|
+
[tool.ruff.lint.per-file-ignores]
|
|
47
|
+
"src/pympacds_http/contracts.py" = ["F821", "F722"]
|
|
48
|
+
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
python_version = "3.10"
|
|
51
|
+
ignore_missing_imports = true
|
|
52
|
+
mypy_path = ["../../src"]
|
|
53
|
+
|
|
54
|
+
# The core framework has its own (pre-existing) typing debt ("object"-typed
|
|
55
|
+
# D-Bus globals, D-Bus signature strings). Treat it as Any rather than
|
|
56
|
+
# re-checking it from the service.
|
|
57
|
+
[[tool.mypy.overrides]]
|
|
58
|
+
module = "pympacds"
|
|
59
|
+
follow_imports = "silent"
|
|
60
|
+
|
|
61
|
+
[[tool.mypy.overrides]]
|
|
62
|
+
module = "pympacds.*"
|
|
63
|
+
follow_imports = "silent"
|
|
64
|
+
|
|
65
|
+
# D-Bus type-signature annotations ("s", "b", "ss", ...) are the dbus-fast/dbus-next
|
|
66
|
+
# convention, not Python types; mypy tries to resolve them as forward references.
|
|
67
|
+
[[tool.mypy.overrides]]
|
|
68
|
+
module = "pympacds_http.contracts"
|
|
69
|
+
disable_error_code = ["name-defined", "valid-type"]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""HTTP D-Bus contract (REQ-HTTP-004)."""
|
|
2
|
+
|
|
3
|
+
from pympacds.contracts import ServiceContract, dbus_method, dbus_signal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HttpContract(ServiceContract):
|
|
7
|
+
"""HTTP request interface."""
|
|
8
|
+
|
|
9
|
+
iface_name = "HTTP"
|
|
10
|
+
iface_version = "1.0.0"
|
|
11
|
+
iface_provides = ["http"]
|
|
12
|
+
iface_requires = ["network"]
|
|
13
|
+
|
|
14
|
+
def __init__(self, ifname: str, base):
|
|
15
|
+
super().__init__(ifname, base)
|
|
16
|
+
self._require(
|
|
17
|
+
"dbus_http_send",
|
|
18
|
+
"dbus_http_download",
|
|
19
|
+
"dbus_http_request",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@dbus_method()
|
|
23
|
+
def send(self, payload: "s") -> "s":
|
|
24
|
+
return self.base.dbus_http_send(payload)
|
|
25
|
+
|
|
26
|
+
@dbus_method()
|
|
27
|
+
def download(self, payload: "s", download_path: "s") -> "s":
|
|
28
|
+
return self.base.dbus_http_download(payload, download_path)
|
|
29
|
+
|
|
30
|
+
@dbus_method()
|
|
31
|
+
def request(
|
|
32
|
+
self,
|
|
33
|
+
method: "s",
|
|
34
|
+
url: "s",
|
|
35
|
+
headers: "s",
|
|
36
|
+
body: "s",
|
|
37
|
+
download_path: "s",
|
|
38
|
+
) -> "s":
|
|
39
|
+
return self.base.dbus_http_request(method, url, headers, body, download_path)
|
|
40
|
+
|
|
41
|
+
@dbus_signal()
|
|
42
|
+
def request_completed(self, token: "s", result: "s") -> "ss":
|
|
43
|
+
return [token, result]
|