pfmsoft-api-request 0.1.3__tar.gz → 0.1.7__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.
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/PKG-INFO +2 -2
- pfmsoft_api_request-0.1.7/pyproject.toml +82 -0
- pfmsoft_api_request-0.1.3/pyproject.toml → pfmsoft_api_request-0.1.7/pyproject.toml.orig +2 -2
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/sqlite_cache/query_helpers.py +14 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/sqlite_cache/sqlite_cache.py +7 -2
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/request/request.py +3 -3
- pfmsoft_api_request-0.1.7/src/pfmsoft/api_request/helpers/package_resource.py +16 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/api_requester.py +2 -2
- pfmsoft_api_request-0.1.7/src/pfmsoft/api_request/settings.py +94 -0
- pfmsoft_api_request-0.1.3/src/pfmsoft/api_request/cache/sqlite_cache/connection_helpers.py +0 -126
- pfmsoft_api_request-0.1.3/src/pfmsoft/api_request/helpers/http_session_factory.py +0 -80
- pfmsoft_api_request-0.1.3/src/pfmsoft/api_request/helpers/json_io.py +0 -256
- pfmsoft_api_request-0.1.3/src/pfmsoft/api_request/helpers/save_text_file.py +0 -43
- pfmsoft_api_request-0.1.3/src/pfmsoft/api_request/settings.py +0 -65
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/LICENSE +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/README.md +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/memory_cache.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/metadata_helpers.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/models.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/protocols.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/sqlite_cache/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/sqlite_cache/table_definitions.sql +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/cache/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/cache/info.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/helpers.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/main_typer.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/request/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/request/validate.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/helpers/whenever/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/helpers/whenever/instant_helpers.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/logging_config.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/py.typed +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/rate_limit/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/rate_limit/aio_limiter.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/rate_limit/protocols.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/__init__.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/intermediate_models.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/models.py +0 -0
- {pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/protocols.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pfmsoft-api-request
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: A command line api request tool with caching and rate limiting.
|
|
5
5
|
Author: Chad Lowe
|
|
6
6
|
Author-email: Chad Lowe <pfmsoft.dev@gmail.com>
|
|
@@ -8,7 +8,7 @@ License-Expression: MIT
|
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: aiolimiter>=1.2.1
|
|
10
10
|
Requires-Dist: httpx2>=2.5.0
|
|
11
|
-
Requires-Dist: pfmsoft-eve-snippets>=0.1.
|
|
11
|
+
Requires-Dist: pfmsoft-eve-snippets>=0.1.7
|
|
12
12
|
Requires-Dist: pydantic>=2.13.4
|
|
13
13
|
Requires-Dist: pydantic-settings>=2.14.2
|
|
14
14
|
Requires-Dist: typer>=0.26.8
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pfmsoft-api-request"
|
|
3
|
+
version = "0.1.7"
|
|
4
|
+
description = "A command line api request tool with caching and rate limiting."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"aiolimiter>=1.2.1",
|
|
9
|
+
"httpx2>=2.5.0",
|
|
10
|
+
"pfmsoft-eve-snippets>=0.1.7",
|
|
11
|
+
"pydantic>=2.13.4",
|
|
12
|
+
"pydantic-settings>=2.14.2",
|
|
13
|
+
"typer>=0.26.8",
|
|
14
|
+
"whenever>=0.10.2",
|
|
15
|
+
]
|
|
16
|
+
license = "MIT"
|
|
17
|
+
license-files = ["LICENSE"]
|
|
18
|
+
|
|
19
|
+
[[project.authors]]
|
|
20
|
+
name = "Chad Lowe"
|
|
21
|
+
email = "pfmsoft.dev@gmail.com"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
api-request = "pfmsoft.api_request.cli.main_typer:app"
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Documentation = "https://github.com/DonalChilde/pfmsoft-api-request#readme"
|
|
28
|
+
Issues = "https://github.com/DonalChilde/pfmsoft-api-request/issues"
|
|
29
|
+
Source = "https://github.com/DonalChilde/pfmsoft-api-request"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["uv_build>=0.11.21,<0.12.0"]
|
|
33
|
+
build-backend = "uv_build"
|
|
34
|
+
|
|
35
|
+
[tool.uv]
|
|
36
|
+
exclude-newer = "1 week"
|
|
37
|
+
|
|
38
|
+
[tool.uv.build-backend]
|
|
39
|
+
module-name = "pfmsoft.api_request"
|
|
40
|
+
|
|
41
|
+
[tool.uv.exclude-newer-package]
|
|
42
|
+
pfmsoft-eve-snippets = false
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
pythonpath = "src"
|
|
46
|
+
log_file = "dev/pytest-logs/tests.log"
|
|
47
|
+
log_file_date_format = "%Y-%m-%dT%H:%M:%S.%f%z"
|
|
48
|
+
log_file_format = "%(asctime)s %(levelname)s:%(funcName)s: %(message)s [in %(pathname)s:%(lineno)d]"
|
|
49
|
+
log_file_level = "INFO"
|
|
50
|
+
|
|
51
|
+
[tool.coverage.run]
|
|
52
|
+
branch = true
|
|
53
|
+
parallel = true
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
select = [
|
|
57
|
+
"B",
|
|
58
|
+
"UP",
|
|
59
|
+
"D",
|
|
60
|
+
"FIX",
|
|
61
|
+
"I",
|
|
62
|
+
"F401",
|
|
63
|
+
]
|
|
64
|
+
ignore = [
|
|
65
|
+
"D401",
|
|
66
|
+
"D101",
|
|
67
|
+
"D421",
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint.pydocstyle]
|
|
71
|
+
convention = "google"
|
|
72
|
+
|
|
73
|
+
[tool.ruff.format]
|
|
74
|
+
docstring-code-format = true
|
|
75
|
+
docstring-code-line-length = 88
|
|
76
|
+
|
|
77
|
+
[dependency-groups]
|
|
78
|
+
dev = [
|
|
79
|
+
"pytest>=9.1.1",
|
|
80
|
+
"pytest-cov>=7.1.0",
|
|
81
|
+
"ruff>=0.15.21",
|
|
82
|
+
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pfmsoft-api-request"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
4
4
|
description = "A command line api request tool with caching and rate limiting."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "Chad Lowe", email = "pfmsoft.dev@gmail.com" }]
|
|
@@ -8,7 +8,7 @@ requires-python = ">=3.14"
|
|
|
8
8
|
dependencies = [
|
|
9
9
|
"aiolimiter>=1.2.1",
|
|
10
10
|
"httpx2>=2.5.0",
|
|
11
|
-
"pfmsoft-eve-snippets>=0.1.
|
|
11
|
+
"pfmsoft-eve-snippets>=0.1.7",
|
|
12
12
|
"pydantic>=2.13.4",
|
|
13
13
|
"pydantic-settings>=2.14.2",
|
|
14
14
|
"typer>=0.26.8",
|
|
@@ -8,10 +8,24 @@ import logging
|
|
|
8
8
|
import sqlite3
|
|
9
9
|
from uuid import UUID
|
|
10
10
|
|
|
11
|
+
from pfmsoft.api_request.helpers.package_resource import load_package_resouce_text
|
|
12
|
+
|
|
11
13
|
from ..models import CachedResponse
|
|
12
14
|
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
17
|
+
_table_def_parent = "pfmsoft.api_request.cache.sqlite_cache"
|
|
18
|
+
_table_def_sql = "table_definitions.sql"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_table_definitions() -> str:
|
|
22
|
+
"""Load the packaged SQL script for creating the database schema.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
The contents of the SQL script as a string.
|
|
26
|
+
"""
|
|
27
|
+
return load_package_resouce_text(_table_def_parent, _table_def_sql)
|
|
28
|
+
|
|
15
29
|
|
|
16
30
|
def write_cached_response(
|
|
17
31
|
connection: sqlite3.Connection, cache_key: str, cached_response: CachedResponse
|
|
@@ -10,14 +10,16 @@ from types import TracebackType
|
|
|
10
10
|
from typing import Self
|
|
11
11
|
from uuid import UUID
|
|
12
12
|
|
|
13
|
+
from pfmsoft.eve_snippets.sqlite3.connection_helpers import create_read_write_connection
|
|
13
14
|
from whenever import Instant
|
|
14
15
|
|
|
16
|
+
from pfmsoft.api_request.cache.sqlite_cache.query_helpers import load_table_definitions
|
|
17
|
+
|
|
15
18
|
from ...request.models import ResponseMetadata
|
|
16
19
|
from ..metadata_helpers import merge_cached_revalidation_metadata
|
|
17
20
|
from ..models import CachedResponse, CacheInfo
|
|
18
21
|
from ..protocols import CacheFactoryProtocol, CacheProtocol
|
|
19
22
|
from . import query_helpers
|
|
20
|
-
from .connection_helpers import create_read_write_connection
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
class SqliteCache(CacheProtocol):
|
|
@@ -56,7 +58,10 @@ class SqliteCache(CacheProtocol):
|
|
|
56
58
|
async def __aenter__(self) -> Self:
|
|
57
59
|
"""Enter context and open path-owned connections when needed."""
|
|
58
60
|
if self._connection_path is not None:
|
|
59
|
-
|
|
61
|
+
table_definitions = load_table_definitions()
|
|
62
|
+
self._connection = create_read_write_connection(
|
|
63
|
+
self._connection_path, init_sql=table_definitions
|
|
64
|
+
)
|
|
60
65
|
self._close_connection_on_exit = True
|
|
61
66
|
return self
|
|
62
67
|
|
|
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Annotated
|
|
8
8
|
|
|
9
9
|
import typer
|
|
10
|
+
from pfmsoft.eve_snippets import save_text_file
|
|
10
11
|
from rich.console import Console
|
|
11
12
|
from rich.json import JSON
|
|
12
13
|
|
|
@@ -16,7 +17,6 @@ from pfmsoft.api_request.cli.helpers import (
|
|
|
16
17
|
get_api_request_settings_from_context,
|
|
17
18
|
get_stdin,
|
|
18
19
|
)
|
|
19
|
-
from pfmsoft.api_request.helpers.save_text_file import save_text_file
|
|
20
20
|
from pfmsoft.api_request.logging_config import setup_logging
|
|
21
21
|
from pfmsoft.api_request.rate_limit.aio_limiter import AiolimiterRateLimiterFactory
|
|
22
22
|
from pfmsoft.api_request.request.models import (
|
|
@@ -206,8 +206,8 @@ def request(
|
|
|
206
206
|
else:
|
|
207
207
|
output_path = save_text_file(
|
|
208
208
|
text=ResponsesRoot(root=responses).model_dump_json(indent=indent),
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
directory=file_out.parent,
|
|
210
|
+
filename=file_out.name,
|
|
211
211
|
overwrite=overwrite,
|
|
212
212
|
)
|
|
213
213
|
messenger.print(f"[bold green]Responses saved to {output_path}[/bold green]")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Helpers for working with package resources."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files as resource_files
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def load_package_resouce_text(package: str, resource_name: str) -> str:
|
|
7
|
+
"""Load a resource from the package as text.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
package: Package name where the resource is located.
|
|
11
|
+
resource_name: Name of the resource file to load.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
The contents of the resource file as a string.
|
|
15
|
+
"""
|
|
16
|
+
return resource_files(package).joinpath(resource_name).read_text()
|
|
@@ -76,12 +76,12 @@ from uuid import UUID
|
|
|
76
76
|
|
|
77
77
|
from httpx2 import AsyncClient
|
|
78
78
|
from httpx2 import Response as HttpResponse
|
|
79
|
+
from pfmsoft.eve_snippets import json_io
|
|
80
|
+
from pfmsoft.eve_snippets.httpx2.http_session_factory import config_async_http_client
|
|
79
81
|
from whenever import Instant
|
|
80
82
|
|
|
81
83
|
from pfmsoft.api_request.cache.models import CachedResponse
|
|
82
84
|
from pfmsoft.api_request.cache.protocols import CacheFactory, CacheProtocol
|
|
83
|
-
from pfmsoft.api_request.helpers import json_io
|
|
84
|
-
from pfmsoft.api_request.helpers.http_session_factory import config_async_http_client
|
|
85
85
|
from pfmsoft.api_request.rate_limit.protocols import (
|
|
86
86
|
RateLimiterFactoryProtocol,
|
|
87
87
|
RateLimiterProtocol,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Settings for the api-request module."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from uuid import NAMESPACE_DNS, uuid5
|
|
7
|
+
|
|
8
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
|
+
from typer import get_app_dir
|
|
10
|
+
|
|
11
|
+
from pfmsoft.api_request import (
|
|
12
|
+
__app_name__,
|
|
13
|
+
__url__,
|
|
14
|
+
__version__,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Typical application settings
|
|
20
|
+
USER_AGENT = f"{__app_name__}/{__version__} ({__url__})"
|
|
21
|
+
"""User-Agent header value sent to remote OAuth and ESI services."""
|
|
22
|
+
APP_DOMAIN = f"{__app_name__}"
|
|
23
|
+
APP_NAMESPACE = uuid5(NAMESPACE_DNS, __app_name__)
|
|
24
|
+
ENV_PREFIX = __app_name__.replace(".", "_").replace("-", "_").upper() + "_"
|
|
25
|
+
SETTINGS_KEY = ENV_PREFIX + "SETTINGS"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(slots=True, kw_only=True)
|
|
29
|
+
class ApiRequestSettings:
|
|
30
|
+
"""Settings for the api-request module."""
|
|
31
|
+
|
|
32
|
+
application_directory: Path
|
|
33
|
+
"""Directory where the application stores its data."""
|
|
34
|
+
logging_directory: Path
|
|
35
|
+
"""Directory where the application stores its log files."""
|
|
36
|
+
web_cache_path: Path
|
|
37
|
+
"""Path to the web cache SQLite database."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ApiRequestSettingsPydantic(BaseSettings):
|
|
41
|
+
"""Pydantic settings for the api-request module."""
|
|
42
|
+
|
|
43
|
+
application_directory: Path = Path(get_app_dir(__app_name__)).resolve()
|
|
44
|
+
"""Directory where the application stores its data."""
|
|
45
|
+
|
|
46
|
+
model_config = SettingsConfigDict(
|
|
47
|
+
env_prefix=ENV_PREFIX,
|
|
48
|
+
env_file=(".env", ".env.dev"),
|
|
49
|
+
env_file_encoding="utf-8",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_settings(
|
|
54
|
+
application_directory: Path | None = None,
|
|
55
|
+
) -> ApiRequestSettings:
|
|
56
|
+
"""Build runtime settings from a Pydantic settings model or application directory.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
application_directory (Path | None): Optional application directory path.
|
|
60
|
+
If not provided, the default application directory is used.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Runtime settings dataclass used by the application.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
ValueError: If the provided application directory exists but is not a directory.
|
|
67
|
+
"""
|
|
68
|
+
if application_directory is None:
|
|
69
|
+
# If the application directory is not provided, use the value from the Pydantic
|
|
70
|
+
# settings model. This allows for environment variable overrides and .env file loading.
|
|
71
|
+
application_directory = ApiRequestSettingsPydantic().application_directory
|
|
72
|
+
application_directory = application_directory.expanduser().resolve()
|
|
73
|
+
if application_directory.exists() and not application_directory.is_dir():
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Application directory '{application_directory}' exists but is not a directory."
|
|
76
|
+
)
|
|
77
|
+
settings = _initialize_settings(application_directory)
|
|
78
|
+
return settings
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _initialize_settings(application_directory: Path) -> ApiRequestSettings:
|
|
82
|
+
"""Build default runtime settings.
|
|
83
|
+
|
|
84
|
+
Also ensures that the application directories exist.
|
|
85
|
+
"""
|
|
86
|
+
settings = ApiRequestSettings(
|
|
87
|
+
application_directory=application_directory,
|
|
88
|
+
logging_directory=application_directory / "logs",
|
|
89
|
+
web_cache_path=application_directory / "api_requests_web_cache.sqlite",
|
|
90
|
+
)
|
|
91
|
+
# Ensure that the application directories exist.
|
|
92
|
+
settings.application_directory.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
settings.logging_directory.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
return settings
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
"""SQLite connection and schema bootstrap helpers.
|
|
2
|
-
|
|
3
|
-
These helpers centralize URI construction and consistent connection setup for
|
|
4
|
-
cache backends and tests.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import logging
|
|
8
|
-
import sqlite3
|
|
9
|
-
from collections.abc import Iterator
|
|
10
|
-
from contextlib import contextmanager
|
|
11
|
-
from importlib.resources import files as resource_files
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
_table_def_parent = "pfmsoft.api_request.cache.sqlite_cache"
|
|
17
|
-
_table_def_sql = "table_definitions.sql"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def read_only_uri(db_path: str) -> str:
|
|
21
|
-
"""Build a read-only SQLite URI for use with sqlite3.connect(uri=True).
|
|
22
|
-
|
|
23
|
-
Args:
|
|
24
|
-
db_path: Filesystem path to the SQLite database.
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
URI string with mode=ro.
|
|
28
|
-
"""
|
|
29
|
-
return f"file:{db_path}?mode=ro"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def read_write_uri(db_path: str) -> str:
|
|
33
|
-
"""Build a read-write/create SQLite URI for sqlite3.connect(uri=True).
|
|
34
|
-
|
|
35
|
-
Args:
|
|
36
|
-
db_path: Filesystem path to the SQLite database.
|
|
37
|
-
|
|
38
|
-
Returns:
|
|
39
|
-
URI string with mode=rwc.
|
|
40
|
-
"""
|
|
41
|
-
return f"file:{db_path}?mode=rwc"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def create_read_only_connection(db_path: str | Path) -> sqlite3.Connection:
|
|
45
|
-
"""Create a read-only SQLite connection for an existing database.
|
|
46
|
-
|
|
47
|
-
The caller is responsible for closing the connection when done.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
db_path: Path to an existing SQLite database file.
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Open SQLite connection configured with sqlite3.Row row factory.
|
|
54
|
-
|
|
55
|
-
Notes:
|
|
56
|
-
The target database is expected to already contain the required schema.
|
|
57
|
-
Read-only connections cannot modify schema or persisted rows.
|
|
58
|
-
"""
|
|
59
|
-
if isinstance(db_path, Path):
|
|
60
|
-
db_path = str(db_path.resolve())
|
|
61
|
-
uri = read_only_uri(db_path)
|
|
62
|
-
connection = sqlite3.connect(uri, uri=True)
|
|
63
|
-
connection.row_factory = sqlite3.Row
|
|
64
|
-
return connection
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def create_read_write_connection(db_path: str | Path) -> sqlite3.Connection:
|
|
68
|
-
"""Create a read-write SQLite connection and ensure the packaged schema exists.
|
|
69
|
-
|
|
70
|
-
The caller is responsible for closing the connection when done.
|
|
71
|
-
|
|
72
|
-
Args:
|
|
73
|
-
db_path: Path to the SQLite database file.
|
|
74
|
-
|
|
75
|
-
Returns:
|
|
76
|
-
Open SQLite connection configured with sqlite3.Row row factory.
|
|
77
|
-
|
|
78
|
-
Notes:
|
|
79
|
-
Missing tables and other schema objects defined in the packaged SQL
|
|
80
|
-
script are created before the connection is returned.
|
|
81
|
-
"""
|
|
82
|
-
if isinstance(db_path, Path):
|
|
83
|
-
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
84
|
-
db_path = str(db_path.resolve())
|
|
85
|
-
else:
|
|
86
|
-
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
87
|
-
db_path = str(Path(db_path).resolve())
|
|
88
|
-
uri = read_write_uri(db_path)
|
|
89
|
-
connection = sqlite3.connect(uri, uri=True)
|
|
90
|
-
logger.info(f"Created read-write connection to database at {db_path}")
|
|
91
|
-
connection.row_factory = sqlite3.Row
|
|
92
|
-
table_defs = resource_files(_table_def_parent).joinpath(_table_def_sql).read_text()
|
|
93
|
-
with connection:
|
|
94
|
-
connection.executescript(table_defs)
|
|
95
|
-
logger.info("Ensured database schema is created.")
|
|
96
|
-
return connection
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@contextmanager
|
|
100
|
-
def db_connection_manager(
|
|
101
|
-
db_path: str | Path, read_only: bool = True
|
|
102
|
-
) -> Iterator[sqlite3.Connection]:
|
|
103
|
-
"""Yield a SQLite connection and close it automatically on exit.
|
|
104
|
-
|
|
105
|
-
Delegates to the read-only or read-write connection factory based on the
|
|
106
|
-
`read_only` flag.
|
|
107
|
-
|
|
108
|
-
Args:
|
|
109
|
-
db_path: Path to the SQLite database file.
|
|
110
|
-
read_only: Whether to open the connection in read-only mode.
|
|
111
|
-
|
|
112
|
-
Yields:
|
|
113
|
-
Open SQLite connection configured with sqlite3.Row row factory.
|
|
114
|
-
"""
|
|
115
|
-
connection: sqlite3.Connection | None = None
|
|
116
|
-
try:
|
|
117
|
-
if read_only:
|
|
118
|
-
logger.info(f"Opening read-only connection to database at {db_path}")
|
|
119
|
-
connection = create_read_only_connection(db_path)
|
|
120
|
-
else:
|
|
121
|
-
logger.info(f"Opening read-write connection to database at {db_path}")
|
|
122
|
-
connection = create_read_write_connection(db_path)
|
|
123
|
-
yield connection
|
|
124
|
-
finally:
|
|
125
|
-
if connection is not None:
|
|
126
|
-
connection.close()
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
"""Create preconfigured sync and async HTTP clients for Eve Auth Manager."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import AsyncIterator, Iterator
|
|
4
|
-
from contextlib import asynccontextmanager, contextmanager
|
|
5
|
-
|
|
6
|
-
from httpx2 import AsyncClient, Client
|
|
7
|
-
|
|
8
|
-
from pfmsoft.api_request.settings import USER_AGENT
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def config_http_client(user_agent: str = USER_AGENT) -> Client:
|
|
12
|
-
"""Create an HTTP client configured with the project User-Agent header.
|
|
13
|
-
|
|
14
|
-
Args:
|
|
15
|
-
user_agent: User-Agent header value to send with requests. Defaults to
|
|
16
|
-
the project setting.
|
|
17
|
-
|
|
18
|
-
Returns:
|
|
19
|
-
Configured synchronous HTTP client.
|
|
20
|
-
|
|
21
|
-
Note:
|
|
22
|
-
The caller owns the returned client and must close it when finished.
|
|
23
|
-
"""
|
|
24
|
-
return Client(headers={"User-Agent": user_agent})
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
async def config_async_http_client(user_agent: str = USER_AGENT) -> AsyncClient:
|
|
28
|
-
"""Create an async HTTP client configured with the project User-Agent header.
|
|
29
|
-
|
|
30
|
-
Args:
|
|
31
|
-
user_agent: User-Agent header value to send with requests. Defaults to
|
|
32
|
-
the project setting.
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
Configured asynchronous HTTP client.
|
|
36
|
-
|
|
37
|
-
Note:
|
|
38
|
-
The caller owns the returned client and must close it when finished.
|
|
39
|
-
"""
|
|
40
|
-
return AsyncClient(headers={"User-Agent": user_agent})
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
@contextmanager
|
|
44
|
-
def client_manager(user_agent: str = USER_AGENT) -> Iterator[Client]:
|
|
45
|
-
"""Yield a configured HTTP client and close it automatically on exit.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
user_agent: User-Agent header value to send with requests.
|
|
49
|
-
|
|
50
|
-
Yields:
|
|
51
|
-
Configured synchronous HTTP client.
|
|
52
|
-
"""
|
|
53
|
-
client: Client | None = None
|
|
54
|
-
try:
|
|
55
|
-
client = config_http_client(user_agent)
|
|
56
|
-
yield client
|
|
57
|
-
finally:
|
|
58
|
-
if client is not None:
|
|
59
|
-
client.close()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@asynccontextmanager
|
|
63
|
-
async def async_client_manager(
|
|
64
|
-
user_agent: str = USER_AGENT,
|
|
65
|
-
) -> AsyncIterator[AsyncClient]:
|
|
66
|
-
"""Yield a configured async HTTP client and close it automatically on exit.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
user_agent: User-Agent header value to send with requests.
|
|
70
|
-
|
|
71
|
-
Yields:
|
|
72
|
-
Configured asynchronous HTTP client.
|
|
73
|
-
"""
|
|
74
|
-
client: AsyncClient | None = None
|
|
75
|
-
try:
|
|
76
|
-
client = await config_async_http_client(user_agent)
|
|
77
|
-
yield client
|
|
78
|
-
finally:
|
|
79
|
-
if client is not None:
|
|
80
|
-
await client.aclose()
|
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
"""Helpers for loading, dumping, and streaming JSON and JSONL data with pydantic_core."""
|
|
2
|
-
|
|
3
|
-
from collections.abc import Iterator
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any, TextIO
|
|
6
|
-
|
|
7
|
-
from pydantic_core import from_json, to_json
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def json_load_path(filepath: Path) -> Any:
|
|
11
|
-
"""Load a JSON file into a Python object.
|
|
12
|
-
|
|
13
|
-
Args:
|
|
14
|
-
filepath: Path to a UTF-8 encoded JSON file.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Parsed Python object produced by pydantic_core.from_json().
|
|
18
|
-
"""
|
|
19
|
-
return from_json(filepath.read_text(encoding="utf-8"))
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def json_loads(json_string: str | bytes) -> Any:
|
|
23
|
-
"""Parse a JSON text or byte payload into a Python object.
|
|
24
|
-
|
|
25
|
-
Args:
|
|
26
|
-
json_string: JSON document encoded as text or bytes.
|
|
27
|
-
|
|
28
|
-
Returns:
|
|
29
|
-
Parsed Python object produced by pydantic_core.from_json().
|
|
30
|
-
"""
|
|
31
|
-
return from_json(json_string)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def json_dumps(
|
|
35
|
-
obj: Any, indent: int | None = None, encoding: str = "utf-8", **kwargs: Any
|
|
36
|
-
) -> str:
|
|
37
|
-
"""Serialize a Python object to JSON text.
|
|
38
|
-
|
|
39
|
-
Args:
|
|
40
|
-
obj: Python object to serialize.
|
|
41
|
-
indent: Optional indentation level passed to pydantic_core.to_json().
|
|
42
|
-
encoding: Text encoding used to decode the serialized JSON bytes.
|
|
43
|
-
**kwargs: Additional keyword arguments forwarded to
|
|
44
|
-
pydantic_core.to_json().
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
JSON document as a string.
|
|
48
|
-
"""
|
|
49
|
-
return to_json(obj, indent=indent, **kwargs).decode(encoding)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def json_dump_bytes(obj: Any, indent: int | None = None, **kwargs: Any) -> bytes:
|
|
53
|
-
"""Serialize a Python object to JSON bytes.
|
|
54
|
-
|
|
55
|
-
Args:
|
|
56
|
-
obj: Python object to serialize.
|
|
57
|
-
indent: Optional indentation level passed to pydantic_core.to_json().
|
|
58
|
-
**kwargs: Additional keyword arguments forwarded to
|
|
59
|
-
pydantic_core.to_json().
|
|
60
|
-
|
|
61
|
-
Returns:
|
|
62
|
-
JSON document as bytes.
|
|
63
|
-
"""
|
|
64
|
-
return to_json(obj, indent=indent, **kwargs)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def json_dumps_path(
|
|
68
|
-
obj: Any,
|
|
69
|
-
*,
|
|
70
|
-
filepath: Path,
|
|
71
|
-
overwrite: bool = False,
|
|
72
|
-
encoding: str = "utf-8",
|
|
73
|
-
indent: int | None = None,
|
|
74
|
-
**kwargs: Any,
|
|
75
|
-
) -> int:
|
|
76
|
-
"""Dump a Python object to a JSON file.
|
|
77
|
-
|
|
78
|
-
Uses pydantic_core.to_json() to serialize the object and writes a trailing
|
|
79
|
-
newline after the JSON document.
|
|
80
|
-
|
|
81
|
-
Args:
|
|
82
|
-
obj: The Python object to dump.
|
|
83
|
-
filepath: The path to the JSON file.
|
|
84
|
-
overwrite: Whether to overwrite the file if it exists.
|
|
85
|
-
encoding: The encoding to use when writing the file.
|
|
86
|
-
indent: The indentation level for the JSON file.
|
|
87
|
-
**kwargs: Additional keyword arguments passed to `json_dumps`.
|
|
88
|
-
|
|
89
|
-
Returns:
|
|
90
|
-
The number of characters written to the file, including the trailing
|
|
91
|
-
newline.
|
|
92
|
-
|
|
93
|
-
Raises:
|
|
94
|
-
FileExistsError: If the file already exists and `overwrite` is False.
|
|
95
|
-
"""
|
|
96
|
-
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
-
if overwrite:
|
|
98
|
-
mode = "w"
|
|
99
|
-
else:
|
|
100
|
-
mode = "x"
|
|
101
|
-
with filepath.open(mode, encoding=encoding) as f:
|
|
102
|
-
counter = 0
|
|
103
|
-
counter += f.write(json_dumps(obj, indent=indent, **kwargs))
|
|
104
|
-
counter += f.write("\n")
|
|
105
|
-
return counter
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def jsonl_loads(jsonl_string: str) -> Iterator[Any]:
|
|
109
|
-
"""Yield Python objects parsed lazily from non-empty JSONL text lines."""
|
|
110
|
-
for line in jsonl_string.splitlines():
|
|
111
|
-
if line.strip():
|
|
112
|
-
yield from_json(line)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def jsonl_loads_indexed(jsonl_string: str) -> Iterator[tuple[int, Any]]:
|
|
116
|
-
"""Yield line-number and object pairs from non-empty JSONL text lines.
|
|
117
|
-
|
|
118
|
-
Line numbers correspond to the original input line positions, including
|
|
119
|
-
blank lines.
|
|
120
|
-
"""
|
|
121
|
-
for line_number, line in enumerate(jsonl_string.splitlines(), start=1):
|
|
122
|
-
if line.strip():
|
|
123
|
-
yield line_number, from_json(line)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def jsonl_load_bytes(jsonl_bytes: bytes) -> Iterator[Any]:
|
|
127
|
-
"""Yield Python objects parsed lazily from non-empty JSONL byte lines."""
|
|
128
|
-
for line in jsonl_bytes.splitlines():
|
|
129
|
-
if line.strip():
|
|
130
|
-
yield from_json(line)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def jsonl_load_bytes_indexed(jsonl_bytes: bytes) -> Iterator[tuple[int, Any]]:
|
|
134
|
-
"""Yield line-number and object pairs from non-empty JSONL byte lines.
|
|
135
|
-
|
|
136
|
-
Line numbers correspond to the original input line positions, including
|
|
137
|
-
blank lines.
|
|
138
|
-
"""
|
|
139
|
-
for line_number, line in enumerate(jsonl_bytes.splitlines(), start=1):
|
|
140
|
-
if line.strip():
|
|
141
|
-
yield line_number, from_json(line)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def jsonl_load_path(filepath: Path) -> Iterator[Any]:
|
|
145
|
-
"""Yield Python objects parsed lazily from non-empty lines in a JSONL file."""
|
|
146
|
-
with filepath.open("r", encoding="utf-8") as f:
|
|
147
|
-
for line in f:
|
|
148
|
-
if line.strip():
|
|
149
|
-
yield from_json(line)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def jsonl_load_path_indexed(filepath: Path) -> Iterator[tuple[int, Any]]:
|
|
153
|
-
"""Yield line-number and object pairs parsed from a JSONL file.
|
|
154
|
-
|
|
155
|
-
Line numbers correspond to the original file line positions, including
|
|
156
|
-
blank lines.
|
|
157
|
-
"""
|
|
158
|
-
with filepath.open("r", encoding="utf-8") as f:
|
|
159
|
-
for line_number, line in enumerate(f, start=1):
|
|
160
|
-
if line.strip():
|
|
161
|
-
yield line_number, from_json(line)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def jsonl_dump_path(
|
|
165
|
-
objs: Iterator[Any],
|
|
166
|
-
*,
|
|
167
|
-
filepath: Path,
|
|
168
|
-
encoding: str = "utf-8",
|
|
169
|
-
overwrite: bool = False,
|
|
170
|
-
append: bool = False,
|
|
171
|
-
**kwargs: Any,
|
|
172
|
-
) -> int:
|
|
173
|
-
"""Write JSONL output from an iterator of Python objects.
|
|
174
|
-
|
|
175
|
-
Serializes one object per line without materializing the full output in
|
|
176
|
-
memory.
|
|
177
|
-
|
|
178
|
-
Args:
|
|
179
|
-
objs: Iterator of Python objects to serialize as JSONL.
|
|
180
|
-
filepath: The path to the JSONL file.
|
|
181
|
-
encoding: The encoding to use when writing the file.
|
|
182
|
-
overwrite: Whether to overwrite the file if it exists.
|
|
183
|
-
append: Whether to append to the file if it exists.
|
|
184
|
-
**kwargs: Additional keyword arguments passed to `json_dumps`.
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
Number of characters written to the file, including newline
|
|
188
|
-
separators.
|
|
189
|
-
|
|
190
|
-
Raises:
|
|
191
|
-
ValueError: If `indent` is provided or if `overwrite` and `append` are
|
|
192
|
-
both True.
|
|
193
|
-
FileExistsError: If the file already exists and neither overwrite nor
|
|
194
|
-
append permits writing.
|
|
195
|
-
"""
|
|
196
|
-
if "indent" in kwargs:
|
|
197
|
-
raise ValueError("indent is not supported for JSONL files.")
|
|
198
|
-
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
-
if overwrite and append:
|
|
200
|
-
raise ValueError("overwrite and append are mutually exclusive.")
|
|
201
|
-
if append:
|
|
202
|
-
mode = "a"
|
|
203
|
-
elif overwrite:
|
|
204
|
-
mode = "w"
|
|
205
|
-
else:
|
|
206
|
-
mode = "x"
|
|
207
|
-
|
|
208
|
-
def write_objs(f: TextIO) -> int:
|
|
209
|
-
counter = 0
|
|
210
|
-
for obj in objs:
|
|
211
|
-
counter += f.write(json_dumps(obj, **kwargs))
|
|
212
|
-
counter += f.write("\n")
|
|
213
|
-
return counter
|
|
214
|
-
|
|
215
|
-
with filepath.open(mode, encoding=encoding) as f:
|
|
216
|
-
return write_objs(f)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def jsonl_dumps(objs: Iterator[Any], encoding: str = "utf-8", **kwargs: Any) -> str:
|
|
220
|
-
"""Serialize an iterator of objects to a JSONL string.
|
|
221
|
-
|
|
222
|
-
Args:
|
|
223
|
-
objs: Iterator of Python objects to serialize as JSONL.
|
|
224
|
-
encoding: Text encoding used when converting each JSON document.
|
|
225
|
-
**kwargs: Additional keyword arguments forwarded to json_dumps().
|
|
226
|
-
|
|
227
|
-
Returns:
|
|
228
|
-
JSONL document with one serialized object per line.
|
|
229
|
-
|
|
230
|
-
Raises:
|
|
231
|
-
ValueError: If `indent` is provided, because JSONL output must remain
|
|
232
|
-
one JSON value per line.
|
|
233
|
-
"""
|
|
234
|
-
if "indent" in kwargs:
|
|
235
|
-
raise ValueError("indent is not supported for JSONL files.")
|
|
236
|
-
return "\n".join(json_dumps(obj, encoding=encoding, **kwargs) for obj in objs)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def jsonl_dump_bytes(objs: Iterator[Any], **kwargs: Any) -> bytes:
|
|
240
|
-
"""Serialize an iterator of objects to JSONL bytes.
|
|
241
|
-
|
|
242
|
-
Args:
|
|
243
|
-
objs: Iterator of Python objects to serialize as JSONL.
|
|
244
|
-
**kwargs: Additional keyword arguments forwarded to json_dump_bytes().
|
|
245
|
-
|
|
246
|
-
Returns:
|
|
247
|
-
JSONL document as bytes with one serialized object per line.
|
|
248
|
-
|
|
249
|
-
Raises:
|
|
250
|
-
ValueError: If `indent` is provided, because JSONL output must remain
|
|
251
|
-
one JSON value per line.
|
|
252
|
-
"""
|
|
253
|
-
if "indent" in kwargs:
|
|
254
|
-
raise ValueError("indent is not supported for JSONL files.")
|
|
255
|
-
jsonl_bytes = b"\n".join(json_dump_bytes(obj, **kwargs) for obj in objs)
|
|
256
|
-
return jsonl_bytes
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
"""Write text files while creating parent directories and enforcing overwrite policy."""
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def save_text_file(
|
|
7
|
-
*,
|
|
8
|
-
text: str,
|
|
9
|
-
output_directory: Path,
|
|
10
|
-
file_name: str,
|
|
11
|
-
overwrite: bool = False,
|
|
12
|
-
encoding: str = "utf-8",
|
|
13
|
-
) -> Path:
|
|
14
|
-
"""Write text to a file in the target output directory.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
text: Text content to write.
|
|
18
|
-
output_directory: Directory that should contain the output file.
|
|
19
|
-
file_name: Name of the output file to create.
|
|
20
|
-
overwrite: If true, replace an existing file. If false, raise an error
|
|
21
|
-
when the target file already exists.
|
|
22
|
-
encoding: Text encoding to use when writing the file.
|
|
23
|
-
|
|
24
|
-
Returns:
|
|
25
|
-
Path object for the written file.
|
|
26
|
-
|
|
27
|
-
Raises:
|
|
28
|
-
FileExistsError: If the target file exists and overwrite is false.
|
|
29
|
-
OSError: If the parent directory cannot be created or the file cannot
|
|
30
|
-
be written.
|
|
31
|
-
|
|
32
|
-
Notes:
|
|
33
|
-
Parent directories are created automatically when missing.
|
|
34
|
-
"""
|
|
35
|
-
output_file = output_directory / file_name
|
|
36
|
-
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
-
if overwrite:
|
|
38
|
-
mode = "w"
|
|
39
|
-
else:
|
|
40
|
-
mode = "x"
|
|
41
|
-
with output_file.open(mode, encoding=encoding) as f:
|
|
42
|
-
f.write(text)
|
|
43
|
-
return output_file
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""Settings for the api-request module."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from dataclasses import asdict, dataclass
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from uuid import NAMESPACE_DNS, uuid5
|
|
7
|
-
|
|
8
|
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
9
|
-
from typer import get_app_dir
|
|
10
|
-
|
|
11
|
-
from pfmsoft.api_request import (
|
|
12
|
-
__app_name__,
|
|
13
|
-
__project_namespace__,
|
|
14
|
-
__url__,
|
|
15
|
-
__version__,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
USER_AGENT = f"{__app_name__} ({__version__}) (+{__url__})"
|
|
21
|
-
APP_DOMAIN = f"{__project_namespace__}.{__app_name__}"
|
|
22
|
-
APP_NAMESPACE = uuid5(NAMESPACE_DNS, APP_DOMAIN)
|
|
23
|
-
ENV_PREFIX = APP_DOMAIN.replace(".", "_").replace("-", "_").upper() + "_"
|
|
24
|
-
SETTINGS_KEY = ENV_PREFIX + "SETTINGS"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@dataclass(slots=True, kw_only=True)
|
|
28
|
-
class ApiRequestSettings:
|
|
29
|
-
"""Settings for the api-request module."""
|
|
30
|
-
|
|
31
|
-
application_directory: Path
|
|
32
|
-
logging_directory: Path
|
|
33
|
-
web_cache_path: Path
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class ApiRequestSettingsPydantic(BaseSettings):
|
|
37
|
-
"""Pydantic settings for the api-request module."""
|
|
38
|
-
|
|
39
|
-
application_directory: Path = Path(get_app_dir(__app_name__))
|
|
40
|
-
|
|
41
|
-
model_config = SettingsConfigDict(
|
|
42
|
-
env_prefix=ENV_PREFIX,
|
|
43
|
-
env_file=(".env", ".env.dev"),
|
|
44
|
-
env_file_encoding="utf-8",
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def get_settings() -> ApiRequestSettings:
|
|
49
|
-
"""Get the settings for the api-request module."""
|
|
50
|
-
logger.info("Loading settings from environment variables...")
|
|
51
|
-
logger.info(f"Environment variable prefix: {ENV_PREFIX}")
|
|
52
|
-
pydantic_settings = ApiRequestSettingsPydantic()
|
|
53
|
-
logger.info(
|
|
54
|
-
f"Loaded settings from environment variables: {pydantic_settings.model_dump()}"
|
|
55
|
-
)
|
|
56
|
-
application_directory = pydantic_settings.application_directory.resolve()
|
|
57
|
-
if not application_directory.exists():
|
|
58
|
-
application_directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
settings = ApiRequestSettings(
|
|
60
|
-
application_directory=application_directory,
|
|
61
|
-
logging_directory=application_directory / "logs",
|
|
62
|
-
web_cache_path=application_directory / "api_requests_web_cache.sqlite",
|
|
63
|
-
)
|
|
64
|
-
logger.info(f"Resolved application settings: {asdict(settings)}")
|
|
65
|
-
return settings
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/models.py
RENAMED
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cache/protocols.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/cache/info.py
RENAMED
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/helpers.py
RENAMED
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/cli/main_typer.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/logging_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/models.py
RENAMED
|
File without changes
|
{pfmsoft_api_request-0.1.3 → pfmsoft_api_request-0.1.7}/src/pfmsoft/api_request/request/protocols.py
RENAMED
|
File without changes
|