pfmsoft-api-request 0.1.3__py3-none-any.whl
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.
- pfmsoft/api_request/__init__.py +37 -0
- pfmsoft/api_request/cache/__init__.py +36 -0
- pfmsoft/api_request/cache/memory_cache.py +177 -0
- pfmsoft/api_request/cache/metadata_helpers.py +55 -0
- pfmsoft/api_request/cache/models.py +83 -0
- pfmsoft/api_request/cache/protocols.py +166 -0
- pfmsoft/api_request/cache/sqlite_cache/__init__.py +7 -0
- pfmsoft/api_request/cache/sqlite_cache/connection_helpers.py +126 -0
- pfmsoft/api_request/cache/sqlite_cache/query_helpers.py +107 -0
- pfmsoft/api_request/cache/sqlite_cache/sqlite_cache.py +233 -0
- pfmsoft/api_request/cache/sqlite_cache/table_definitions.sql +23 -0
- pfmsoft/api_request/cli/__init__.py +17 -0
- pfmsoft/api_request/cli/cache/__init__.py +12 -0
- pfmsoft/api_request/cli/cache/info.py +11 -0
- pfmsoft/api_request/cli/helpers.py +41 -0
- pfmsoft/api_request/cli/main_typer.py +52 -0
- pfmsoft/api_request/cli/request/__init__.py +11 -0
- pfmsoft/api_request/cli/request/request.py +222 -0
- pfmsoft/api_request/cli/request/validate.py +11 -0
- pfmsoft/api_request/helpers/http_session_factory.py +80 -0
- pfmsoft/api_request/helpers/json_io.py +256 -0
- pfmsoft/api_request/helpers/save_text_file.py +43 -0
- pfmsoft/api_request/helpers/whenever/__init__.py +4 -0
- pfmsoft/api_request/helpers/whenever/instant_helpers.py +41 -0
- pfmsoft/api_request/logging_config.py +202 -0
- pfmsoft/api_request/py.typed +0 -0
- pfmsoft/api_request/rate_limit/__init__.py +24 -0
- pfmsoft/api_request/rate_limit/aio_limiter.py +89 -0
- pfmsoft/api_request/rate_limit/protocols.py +101 -0
- pfmsoft/api_request/request/__init__.py +11 -0
- pfmsoft/api_request/request/api_requester.py +681 -0
- pfmsoft/api_request/request/intermediate_models.py +143 -0
- pfmsoft/api_request/request/models.py +355 -0
- pfmsoft/api_request/request/protocols.py +54 -0
- pfmsoft/api_request/settings.py +65 -0
- pfmsoft_api_request-0.1.3.dist-info/METADATA +171 -0
- pfmsoft_api_request-0.1.3.dist-info/RECORD +40 -0
- pfmsoft_api_request-0.1.3.dist-info/WHEEL +4 -0
- pfmsoft_api_request-0.1.3.dist-info/entry_points.txt +3 -0
- pfmsoft_api_request-0.1.3.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""SQLite persistence helpers for the cache backend.
|
|
2
|
+
|
|
3
|
+
This module contains SQL-backed helpers for storing and retrieving
|
|
4
|
+
`CachedResponse` rows in the `WebCache` table.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import sqlite3
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from ..models import CachedResponse
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def write_cached_response(
|
|
17
|
+
connection: sqlite3.Connection, cache_key: str, cached_response: CachedResponse
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Insert or replace a cached response row by key.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
connection: Active SQLite connection.
|
|
23
|
+
cache_key: Cache key string (UUID text representation).
|
|
24
|
+
cached_response: Normalized cache model to persist.
|
|
25
|
+
"""
|
|
26
|
+
query = """
|
|
27
|
+
INSERT INTO WebCache (
|
|
28
|
+
cache_key,
|
|
29
|
+
response_text,
|
|
30
|
+
response_metadata_json,
|
|
31
|
+
etag,
|
|
32
|
+
last_modified,
|
|
33
|
+
expires_at,
|
|
34
|
+
cache_timestamp
|
|
35
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
36
|
+
ON CONFLICT(cache_key) DO UPDATE SET
|
|
37
|
+
response_text = excluded.response_text,
|
|
38
|
+
response_metadata_json = excluded.response_metadata_json,
|
|
39
|
+
etag = excluded.etag,
|
|
40
|
+
last_modified = excluded.last_modified,
|
|
41
|
+
expires_at = excluded.expires_at,
|
|
42
|
+
cache_timestamp = excluded.cache_timestamp
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
with connection:
|
|
46
|
+
connection.execute(
|
|
47
|
+
query,
|
|
48
|
+
(
|
|
49
|
+
cache_key,
|
|
50
|
+
cached_response.response_text,
|
|
51
|
+
cached_response.response_metadata_json,
|
|
52
|
+
cached_response.etag,
|
|
53
|
+
cached_response.last_modified,
|
|
54
|
+
cached_response.expires_at,
|
|
55
|
+
cached_response.cache_timestamp,
|
|
56
|
+
),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def query_cached_response(
|
|
61
|
+
connection: sqlite3.Connection, cache_key: str
|
|
62
|
+
) -> CachedResponse | None:
|
|
63
|
+
"""Query one cached response row by key.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
connection: Active SQLite connection.
|
|
67
|
+
cache_key: Cache key string (UUID text representation).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Matching cached response, or None when no row exists.
|
|
71
|
+
"""
|
|
72
|
+
query = """
|
|
73
|
+
SELECT
|
|
74
|
+
cache_key,
|
|
75
|
+
response_text,
|
|
76
|
+
response_metadata_json,
|
|
77
|
+
etag,
|
|
78
|
+
last_modified,
|
|
79
|
+
expires_at,
|
|
80
|
+
cache_timestamp
|
|
81
|
+
FROM WebCache
|
|
82
|
+
WHERE cache_key = ?
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
row = connection.execute(query, (cache_key,)).fetchone()
|
|
86
|
+
if row is None:
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
return CachedResponse(
|
|
90
|
+
cache_key=UUID(row["cache_key"]),
|
|
91
|
+
response_text=row["response_text"],
|
|
92
|
+
response_metadata_json=row["response_metadata_json"],
|
|
93
|
+
etag=row["etag"],
|
|
94
|
+
last_modified=row["last_modified"],
|
|
95
|
+
expires_at=row["expires_at"],
|
|
96
|
+
cache_timestamp=row["cache_timestamp"],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def delete_cached_response(connection: sqlite3.Connection, cache_key: str) -> None:
|
|
101
|
+
"""Delete a cached response row by key.
|
|
102
|
+
|
|
103
|
+
This operation is idempotent and does not raise for missing keys.
|
|
104
|
+
"""
|
|
105
|
+
query = "DELETE FROM WebCache WHERE cache_key = ?"
|
|
106
|
+
with connection:
|
|
107
|
+
connection.execute(query, (cache_key,))
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""SQLite-backed cache provider for request orchestration.
|
|
2
|
+
|
|
3
|
+
This module provides a `CacheProtocol` implementation backed by a `WebCache`
|
|
4
|
+
table and a small factory for dependency injection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sqlite3
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from typing import Self
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
from whenever import Instant
|
|
14
|
+
|
|
15
|
+
from ...request.models import ResponseMetadata
|
|
16
|
+
from ..metadata_helpers import merge_cached_revalidation_metadata
|
|
17
|
+
from ..models import CachedResponse, CacheInfo
|
|
18
|
+
from ..protocols import CacheFactoryProtocol, CacheProtocol
|
|
19
|
+
from . import query_helpers
|
|
20
|
+
from .connection_helpers import create_read_write_connection
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SqliteCache(CacheProtocol):
|
|
24
|
+
"""SQLite-backed cache implementation.
|
|
25
|
+
|
|
26
|
+
This cache persists entries in the `WebCache` table and maps CRUD operations
|
|
27
|
+
to SQL statements through query helpers.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
db: str | Path | sqlite3.Connection,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize cache with a database path or active SQLite connection.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
db: Either a filesystem path to the SQLite database or an existing
|
|
38
|
+
sqlite3 connection.
|
|
39
|
+
|
|
40
|
+
Notes:
|
|
41
|
+
When initialized with a path, a read-write connection is created and
|
|
42
|
+
closed automatically via the context manager.
|
|
43
|
+
|
|
44
|
+
When initialized with an existing connection, that connection is never
|
|
45
|
+
closed by this class.
|
|
46
|
+
"""
|
|
47
|
+
self._connection: sqlite3.Connection | None = None
|
|
48
|
+
self._close_connection_on_exit: bool = False
|
|
49
|
+
if isinstance(db, sqlite3.Connection):
|
|
50
|
+
self._connection = db
|
|
51
|
+
self._close_connection_on_exit = False
|
|
52
|
+
self._connection_path: Path | None = None
|
|
53
|
+
else:
|
|
54
|
+
self._connection_path = Path(db)
|
|
55
|
+
|
|
56
|
+
async def __aenter__(self) -> Self:
|
|
57
|
+
"""Enter context and open path-owned connections when needed."""
|
|
58
|
+
if self._connection_path is not None:
|
|
59
|
+
self._connection = create_read_write_connection(self._connection_path)
|
|
60
|
+
self._close_connection_on_exit = True
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
async def __aexit__(
|
|
64
|
+
self,
|
|
65
|
+
exc_type: type[BaseException] | None,
|
|
66
|
+
exc_value: BaseException | None,
|
|
67
|
+
traceback: TracebackType | None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Exit context and close only connections owned by this instance."""
|
|
70
|
+
_ = exc_type, exc_value, traceback
|
|
71
|
+
if self._connection is not None:
|
|
72
|
+
if self._close_connection_on_exit:
|
|
73
|
+
self._connection.close()
|
|
74
|
+
|
|
75
|
+
def _ensure_connection(self) -> sqlite3.Connection:
|
|
76
|
+
"""Ensure an active SQLite connection is present."""
|
|
77
|
+
if self._connection is None:
|
|
78
|
+
raise RuntimeError("No active SQLite connection")
|
|
79
|
+
return self._connection
|
|
80
|
+
|
|
81
|
+
async def get(self, cache_key: UUID) -> CachedResponse | None:
|
|
82
|
+
"""Get a cached response by key, or None when missing."""
|
|
83
|
+
connection = self._ensure_connection()
|
|
84
|
+
return query_helpers.query_cached_response(connection, str(cache_key))
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _ensure_validators(metadata: ResponseMetadata) -> None:
|
|
88
|
+
"""Ensure at least one cache validator is present."""
|
|
89
|
+
if metadata.etag is None and metadata.last_modified is None:
|
|
90
|
+
raise ValueError("Cached responses require etag or last_modified")
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _build_cached_response(
|
|
94
|
+
*,
|
|
95
|
+
cache_key: UUID,
|
|
96
|
+
text: str,
|
|
97
|
+
metadata: ResponseMetadata,
|
|
98
|
+
) -> CachedResponse:
|
|
99
|
+
"""Build a CachedResponse from response text and metadata."""
|
|
100
|
+
SqliteCache._ensure_validators(metadata)
|
|
101
|
+
return CachedResponse(
|
|
102
|
+
cache_key=cache_key,
|
|
103
|
+
response_text=text,
|
|
104
|
+
response_metadata_json=metadata.serialize(),
|
|
105
|
+
etag=metadata.etag,
|
|
106
|
+
last_modified=metadata.last_modified,
|
|
107
|
+
expires_at=metadata.expires_at,
|
|
108
|
+
cache_timestamp=Instant.now().timestamp_nanos(),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
async def set(
|
|
112
|
+
self, cache_key: UUID, text: str, metadata: ResponseMetadata
|
|
113
|
+
) -> CachedResponse:
|
|
114
|
+
"""Create or replace a cached response entry.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If both metadata validators are absent.
|
|
118
|
+
"""
|
|
119
|
+
cached_response = self._build_cached_response(
|
|
120
|
+
cache_key=cache_key,
|
|
121
|
+
text=text,
|
|
122
|
+
metadata=metadata,
|
|
123
|
+
)
|
|
124
|
+
connection = self._ensure_connection()
|
|
125
|
+
query_helpers.write_cached_response(
|
|
126
|
+
connection,
|
|
127
|
+
str(cache_key),
|
|
128
|
+
cached_response,
|
|
129
|
+
)
|
|
130
|
+
return cached_response
|
|
131
|
+
|
|
132
|
+
async def update_304(
|
|
133
|
+
self, cache_key: UUID, metadata: ResponseMetadata
|
|
134
|
+
) -> CachedResponse:
|
|
135
|
+
"""Refresh a stale entry from 304 metadata while preserving body text.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
KeyError: If no existing entry is present.
|
|
139
|
+
ValueError: If merged metadata lacks both validators.
|
|
140
|
+
"""
|
|
141
|
+
existing = await self.get(cache_key)
|
|
142
|
+
if existing is None:
|
|
143
|
+
raise KeyError(f"No cached response found for key {cache_key}")
|
|
144
|
+
|
|
145
|
+
existing_metadata = ResponseMetadata.deserialize(
|
|
146
|
+
existing.response_metadata_json
|
|
147
|
+
)
|
|
148
|
+
merged_metadata = merge_cached_revalidation_metadata(
|
|
149
|
+
cached=existing_metadata,
|
|
150
|
+
refreshed=metadata,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
cached_response = self._build_cached_response(
|
|
154
|
+
cache_key=cache_key,
|
|
155
|
+
text=existing.response_text,
|
|
156
|
+
metadata=merged_metadata,
|
|
157
|
+
)
|
|
158
|
+
connection = self._ensure_connection()
|
|
159
|
+
query_helpers.write_cached_response(
|
|
160
|
+
connection,
|
|
161
|
+
str(cache_key),
|
|
162
|
+
cached_response,
|
|
163
|
+
)
|
|
164
|
+
return cached_response
|
|
165
|
+
|
|
166
|
+
async def delete(self, cache_key: UUID) -> None:
|
|
167
|
+
"""Delete a cached response from the cache.
|
|
168
|
+
|
|
169
|
+
This operation is idempotent and does not raise for missing keys.
|
|
170
|
+
"""
|
|
171
|
+
connection = self._ensure_connection()
|
|
172
|
+
query_helpers.delete_cached_response(connection, str(cache_key))
|
|
173
|
+
|
|
174
|
+
async def clear(
|
|
175
|
+
self, only_expired: bool = False, age_limit: int | None = None
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Clear cached entries matching the requested filters.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
only_expired: When true, remove only expired entries.
|
|
181
|
+
age_limit: When provided, remove entries with `cache_age` greater
|
|
182
|
+
than or equal to this nanosecond threshold.
|
|
183
|
+
|
|
184
|
+
Notes:
|
|
185
|
+
When both filters are provided, entries must satisfy both.
|
|
186
|
+
"""
|
|
187
|
+
connection = self._ensure_connection()
|
|
188
|
+
if not only_expired and age_limit is None:
|
|
189
|
+
with connection:
|
|
190
|
+
connection.execute("DELETE FROM WebCache")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
clauses: list[str] = []
|
|
194
|
+
params: list[int] = []
|
|
195
|
+
|
|
196
|
+
if only_expired:
|
|
197
|
+
clauses.append("(expires_at IS NOT NULL AND expires_at <= ?)")
|
|
198
|
+
params.append(Instant.now().timestamp())
|
|
199
|
+
|
|
200
|
+
if age_limit is not None:
|
|
201
|
+
cutoff = Instant.now().timestamp_nanos() - age_limit
|
|
202
|
+
clauses.append("cache_timestamp <= ?")
|
|
203
|
+
params.append(cutoff)
|
|
204
|
+
|
|
205
|
+
where_clause = " AND ".join(clauses)
|
|
206
|
+
query = f"DELETE FROM WebCache WHERE {where_clause}"
|
|
207
|
+
connection = self._ensure_connection()
|
|
208
|
+
with connection:
|
|
209
|
+
connection.execute(query, tuple(params))
|
|
210
|
+
|
|
211
|
+
async def flush(self) -> None:
|
|
212
|
+
"""Commit pending writes on the active SQLite connection."""
|
|
213
|
+
# hot cache not yet implememnted
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
async def cache_info(self) -> CacheInfo:
|
|
217
|
+
"""Get summary information about the cache."""
|
|
218
|
+
connection = self._ensure_connection()
|
|
219
|
+
row = connection.execute("SELECT COUNT(*) AS size FROM WebCache").fetchone()
|
|
220
|
+
size = 0 if row is None else int(row["size"])
|
|
221
|
+
return CacheInfo(size=size)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class SqliteCacheFactory(CacheFactoryProtocol):
|
|
225
|
+
"""Factory for creating `SqliteCache` instances backed by a DB path."""
|
|
226
|
+
|
|
227
|
+
def __init__(self, db_path: str | Path) -> None:
|
|
228
|
+
"""Initialize with the SQLite database path."""
|
|
229
|
+
self._db_path = db_path
|
|
230
|
+
|
|
231
|
+
def __call__(self) -> CacheProtocol:
|
|
232
|
+
"""Create and return a new SQLite-backed cache instance."""
|
|
233
|
+
return SqliteCache(self._db_path)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
-- Table definitions for the api-request web cache SQLite database.
|
|
2
|
+
-- JSON payload columns are stored as TEXT.
|
|
3
|
+
-- Timestamps are stored as integers; units vary by column.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
-- The WebCache table stores one row per cached response key.
|
|
7
|
+
-- cache_key: unique key identifying a cache entry.
|
|
8
|
+
-- response_text: serialized response body text.
|
|
9
|
+
-- response_metadata_json: JSON-encoded response metadata.
|
|
10
|
+
-- etag: ETag validator, if present.
|
|
11
|
+
-- last_modified: Last-Modified validator, if present.
|
|
12
|
+
-- expires_at: Unix timestamp in seconds when the entry becomes stale.
|
|
13
|
+
-- cache_timestamp: Unix timestamp in nanoseconds for write/update time.
|
|
14
|
+
CREATE TABLE IF NOT EXISTS WebCache (
|
|
15
|
+
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
16
|
+
cache_key TEXT NOT NULL UNIQUE,
|
|
17
|
+
response_text TEXT NOT NULL,
|
|
18
|
+
response_metadata_json TEXT NOT NULL,
|
|
19
|
+
etag TEXT,
|
|
20
|
+
last_modified TEXT,
|
|
21
|
+
expires_at INTEGER,
|
|
22
|
+
cache_timestamp INTEGER NOT NULL
|
|
23
|
+
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""CLI command modules for api-request.
|
|
2
|
+
|
|
3
|
+
This package contains Typer applications and helper functions used by the
|
|
4
|
+
`eve-auth`/`api-request` command-line entrypoints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from .cache import app as cache_app
|
|
10
|
+
from .request import app as request_app
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(no_args_is_help=True)
|
|
13
|
+
|
|
14
|
+
app.add_typer(
|
|
15
|
+
cache_app, name="cache", help="Commands for working with the cache database."
|
|
16
|
+
)
|
|
17
|
+
app.add_typer(request_app, name="request", help="Commands for making API requests.")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Commands for working with the cache database."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from .info import app as info_app
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(
|
|
8
|
+
no_args_is_help=True,
|
|
9
|
+
name="cache",
|
|
10
|
+
help="Commands for working with the cache database.",
|
|
11
|
+
)
|
|
12
|
+
app.add_typer(info_app)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Helper functions for the api-request CLI."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import cast
|
|
5
|
+
|
|
6
|
+
from typer import Context
|
|
7
|
+
|
|
8
|
+
from pfmsoft.api_request.settings import SETTINGS_KEY, ApiRequestSettings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_api_request_settings_from_context(ctx: Context) -> ApiRequestSettings:
|
|
12
|
+
"""Return ApiRequestSettings stored in the Typer context.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
ctx: Typer command context whose obj mapping should contain the
|
|
16
|
+
initialized ApiRequestSettings.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
ApiRequestSettings stored under the `settings.SETTINGS_KEY` key.
|
|
20
|
+
|
|
21
|
+
Raises:
|
|
22
|
+
ValueError: If the context does not contain initialized ApiRequestSettings.
|
|
23
|
+
"""
|
|
24
|
+
if ctx.obj is None or SETTINGS_KEY not in ctx.obj:
|
|
25
|
+
raise ValueError("ApiRequestSettings not found in context.")
|
|
26
|
+
return cast(ApiRequestSettings, ctx.obj[SETTINGS_KEY])
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_stdin() -> str:
|
|
30
|
+
"""Read piped or redirected stdin content until EOF.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Full stdin content as a string.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If stdin is attached to an interactive terminal instead
|
|
37
|
+
of a pipe or redirected input source.
|
|
38
|
+
"""
|
|
39
|
+
if sys.stdin.isatty():
|
|
40
|
+
raise ValueError("Error: provide a file path or pipe data via stdin.")
|
|
41
|
+
return sys.stdin.read()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Main entry point for the api-request CLI.
|
|
2
|
+
|
|
3
|
+
NOTE: This is not currently the entry point, as so far there is only one command.
|
|
4
|
+
If additional commands are added, this module will be the entry point and will
|
|
5
|
+
dispatch to subcommands.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from pfmsoft.api_request import __app_name__, __version__
|
|
14
|
+
from pfmsoft.api_request.cli import app as api_request_app
|
|
15
|
+
from pfmsoft.api_request.cli.helpers import get_api_request_settings_from_context
|
|
16
|
+
from pfmsoft.api_request.logging_config import setup_logging
|
|
17
|
+
from pfmsoft.api_request.settings import SETTINGS_KEY, get_settings
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_options(ctx: typer.Context) -> None:
|
|
23
|
+
"""Initialize settings and logging for standalone CLI execution.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ctx: Typer command context used to store shared application settings
|
|
27
|
+
for downstream subcommands.
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
The resolved ApiRequestSettings object is stored in ctx.obj under the
|
|
31
|
+
`api-request-settings` key.
|
|
32
|
+
"""
|
|
33
|
+
settings = get_settings()
|
|
34
|
+
setup_logging(log_dir=settings.logging_directory)
|
|
35
|
+
ctx.obj = {SETTINGS_KEY: settings}
|
|
36
|
+
logger.info(
|
|
37
|
+
f"Starting {__app_name__} v{__version__} with settings: {asdict(settings)!r}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
app = typer.Typer(callback=default_options, no_args_is_help=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command()
|
|
45
|
+
def version(ctx: typer.Context) -> None:
|
|
46
|
+
"""Print the version of the api-request CLI."""
|
|
47
|
+
settings = get_api_request_settings_from_context(ctx)
|
|
48
|
+
typer.echo(f"{__app_name__} v{__version__}")
|
|
49
|
+
typer.echo(f"Settings: {asdict(settings)!r}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
app.add_typer(api_request_app)
|