easy-fints 1.2.0__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.
- easy_fints/__init__.py +69 -0
- easy_fints/cli.py +188 -0
- easy_fints/client.py +1249 -0
- easy_fints/diagnostics.py +58 -0
- easy_fints/env_config.py +27 -0
- easy_fints/exceptions.py +80 -0
- easy_fints/fastapi_app.py +1135 -0
- easy_fints/helpers.py +907 -0
- easy_fints/models.py +626 -0
- easy_fints/service.py +133 -0
- easy_fints-1.2.0.dist-info/METADATA +369 -0
- easy_fints-1.2.0.dist-info/RECORD +16 -0
- easy_fints-1.2.0.dist-info/WHEEL +5 -0
- easy_fints-1.2.0.dist-info/entry_points.txt +2 -0
- easy_fints-1.2.0.dist-info/licenses/LICENSE +21 -0
- easy_fints-1.2.0.dist-info/top_level.txt +1 -0
easy_fints/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Public package API for REST-server and library-style integration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .client import FinTSClient
|
|
10
|
+
from .exceptions import (
|
|
11
|
+
FinTSCapabilityError,
|
|
12
|
+
FinTSConfigError,
|
|
13
|
+
FinTSOperationError,
|
|
14
|
+
FinTSValidationError,
|
|
15
|
+
TanRequiredError,
|
|
16
|
+
VOPRequiredError,
|
|
17
|
+
)
|
|
18
|
+
from .models import (
|
|
19
|
+
AccountSummary,
|
|
20
|
+
AccountTransactions,
|
|
21
|
+
FinTSConfig,
|
|
22
|
+
TanChallenge,
|
|
23
|
+
TanMethod,
|
|
24
|
+
TanMethodsSnapshot,
|
|
25
|
+
TransferResponse,
|
|
26
|
+
TransferSummary,
|
|
27
|
+
TransactionRecord,
|
|
28
|
+
VOPChallenge,
|
|
29
|
+
)
|
|
30
|
+
from .service import FinTS
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_EXPORT_MAP = {
|
|
34
|
+
"FinTSClient": ("easy_fints.client", "FinTSClient"),
|
|
35
|
+
"FinTS": ("easy_fints.service", "FinTS"),
|
|
36
|
+
"FinTSConfig": ("easy_fints.models", "FinTSConfig"),
|
|
37
|
+
"AccountSummary": ("easy_fints.models", "AccountSummary"),
|
|
38
|
+
"AccountTransactions": ("easy_fints.models", "AccountTransactions"),
|
|
39
|
+
"TransactionRecord": ("easy_fints.models", "TransactionRecord"),
|
|
40
|
+
"TanChallenge": ("easy_fints.models", "TanChallenge"),
|
|
41
|
+
"TanMethod": ("easy_fints.models", "TanMethod"),
|
|
42
|
+
"TanMethodsSnapshot": ("easy_fints.models", "TanMethodsSnapshot"),
|
|
43
|
+
"TransferResponse": ("easy_fints.models", "TransferResponse"),
|
|
44
|
+
"TransferSummary": ("easy_fints.models", "TransferSummary"),
|
|
45
|
+
"VOPChallenge": ("easy_fints.models", "VOPChallenge"),
|
|
46
|
+
"FinTSOperationError": ("easy_fints.exceptions", "FinTSOperationError"),
|
|
47
|
+
"FinTSConfigError": ("easy_fints.exceptions", "FinTSConfigError"),
|
|
48
|
+
"FinTSValidationError": ("easy_fints.exceptions", "FinTSValidationError"),
|
|
49
|
+
"FinTSCapabilityError": ("easy_fints.exceptions", "FinTSCapabilityError"),
|
|
50
|
+
"TanRequiredError": ("easy_fints.exceptions", "TanRequiredError"),
|
|
51
|
+
"VOPRequiredError": ("easy_fints.exceptions", "VOPRequiredError"),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
__all__ = sorted(_EXPORT_MAP)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def __getattr__(name: str) -> Any:
|
|
58
|
+
try:
|
|
59
|
+
module_name, attr_name = _EXPORT_MAP[name]
|
|
60
|
+
except KeyError as exc:
|
|
61
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc
|
|
62
|
+
module = import_module(module_name)
|
|
63
|
+
value = getattr(module, attr_name)
|
|
64
|
+
globals()[name] = value
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def __dir__() -> list[str]:
|
|
69
|
+
return sorted(set(globals()) | set(__all__))
|
easy_fints/cli.py
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Command-line helpers for running the REST server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import signal
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .env_config import load_project_env
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
DEFAULT_HOST = "0.0.0.0"
|
|
17
|
+
DEFAULT_PORT = 8000
|
|
18
|
+
DEFAULT_PID_FILE = ".fints-rest-server.pid"
|
|
19
|
+
DEFAULT_LOG_FILE = ".fints-rest-server.log"
|
|
20
|
+
STARTUP_WAIT_SECONDS = 0.75
|
|
21
|
+
STOP_WAIT_SECONDS = 5.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _read_pid(pid_file: Path) -> int | None:
|
|
25
|
+
if not pid_file.exists():
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
return int(pid_file.read_text(encoding="utf-8").strip())
|
|
29
|
+
except (TypeError, ValueError):
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _process_exists(pid: int) -> bool:
|
|
34
|
+
try:
|
|
35
|
+
os.kill(pid, 0)
|
|
36
|
+
except ProcessLookupError:
|
|
37
|
+
return False
|
|
38
|
+
except PermissionError:
|
|
39
|
+
return True
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _remove_pid_file(pid_file: Path) -> None:
|
|
44
|
+
try:
|
|
45
|
+
pid_file.unlink()
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_start_options(args: argparse.Namespace) -> tuple[str, int, Path, Path]:
|
|
51
|
+
host = args.host or os.getenv("FINTS_SERVER_HOST", DEFAULT_HOST)
|
|
52
|
+
port = args.port
|
|
53
|
+
if port is None:
|
|
54
|
+
port = int(os.getenv("FINTS_SERVER_PORT", str(DEFAULT_PORT)))
|
|
55
|
+
pid_file = Path(args.pid_file or os.getenv("FINTS_SERVER_PID_FILE", DEFAULT_PID_FILE)).expanduser()
|
|
56
|
+
log_file = Path(args.log_file or os.getenv("FINTS_SERVER_LOG_FILE", DEFAULT_LOG_FILE)).expanduser()
|
|
57
|
+
return host, int(port), pid_file, log_file
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _resolve_pid_file(value: str | Path | None) -> Path:
|
|
61
|
+
configured = value or os.getenv("FINTS_SERVER_PID_FILE", DEFAULT_PID_FILE)
|
|
62
|
+
return Path(configured).expanduser()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _start_server(args: argparse.Namespace) -> int:
|
|
66
|
+
if args.env_file:
|
|
67
|
+
os.environ["FINTS_ENV_FILE"] = str(args.env_file)
|
|
68
|
+
load_project_env()
|
|
69
|
+
host, port, pid_file, log_file = _resolve_start_options(args)
|
|
70
|
+
|
|
71
|
+
existing_pid = _read_pid(pid_file)
|
|
72
|
+
if existing_pid and _process_exists(existing_pid):
|
|
73
|
+
print(f"Server already running with PID {existing_pid}.", file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
if pid_file.exists():
|
|
76
|
+
_remove_pid_file(pid_file)
|
|
77
|
+
|
|
78
|
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
pid_file.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
command = [
|
|
82
|
+
sys.executable,
|
|
83
|
+
"-m",
|
|
84
|
+
"uvicorn",
|
|
85
|
+
"easy_fints.fastapi_app:app",
|
|
86
|
+
"--host",
|
|
87
|
+
host,
|
|
88
|
+
"--port",
|
|
89
|
+
str(port),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
with log_file.open("ab") as log_handle:
|
|
93
|
+
process = subprocess.Popen(
|
|
94
|
+
command,
|
|
95
|
+
stdout=log_handle,
|
|
96
|
+
stderr=subprocess.STDOUT,
|
|
97
|
+
stdin=subprocess.DEVNULL,
|
|
98
|
+
start_new_session=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
time.sleep(STARTUP_WAIT_SECONDS)
|
|
102
|
+
if process.poll() is not None:
|
|
103
|
+
print(
|
|
104
|
+
f"Server failed to start. See log file: {log_file}",
|
|
105
|
+
file=sys.stderr,
|
|
106
|
+
)
|
|
107
|
+
return process.returncode or 1
|
|
108
|
+
|
|
109
|
+
pid_file.write_text(f"{process.pid}\n", encoding="utf-8")
|
|
110
|
+
print(
|
|
111
|
+
f"Started fints-rest-server on {host}:{port} with PID {process.pid}. "
|
|
112
|
+
f"Log: {log_file}"
|
|
113
|
+
)
|
|
114
|
+
return 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _stop_server(args: argparse.Namespace) -> int:
|
|
118
|
+
pid_file = _resolve_pid_file(args.pid_file)
|
|
119
|
+
pid = _read_pid(pid_file)
|
|
120
|
+
if pid is None:
|
|
121
|
+
if pid_file.exists():
|
|
122
|
+
_remove_pid_file(pid_file)
|
|
123
|
+
print(f"No running server found. Missing or invalid PID file: {pid_file}", file=sys.stderr)
|
|
124
|
+
return 1
|
|
125
|
+
|
|
126
|
+
if not _process_exists(pid):
|
|
127
|
+
_remove_pid_file(pid_file)
|
|
128
|
+
print(f"Removed stale PID file for process {pid}.")
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
os.kill(pid, signal.SIGTERM)
|
|
132
|
+
deadline = time.monotonic() + STOP_WAIT_SECONDS
|
|
133
|
+
while time.monotonic() < deadline:
|
|
134
|
+
if not _process_exists(pid):
|
|
135
|
+
_remove_pid_file(pid_file)
|
|
136
|
+
print(f"Stopped fints-rest-server with PID {pid}.")
|
|
137
|
+
return 0
|
|
138
|
+
time.sleep(0.1)
|
|
139
|
+
|
|
140
|
+
print(f"Timed out while stopping server with PID {pid}.", file=sys.stderr)
|
|
141
|
+
return 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _status_server(args: argparse.Namespace) -> int:
|
|
145
|
+
pid_file = _resolve_pid_file(args.pid_file)
|
|
146
|
+
pid = _read_pid(pid_file)
|
|
147
|
+
if pid is None:
|
|
148
|
+
print("fints-rest-server is not running.")
|
|
149
|
+
return 1
|
|
150
|
+
if not _process_exists(pid):
|
|
151
|
+
print(f"fints-rest-server is not running. Stale PID file: {pid_file}")
|
|
152
|
+
return 1
|
|
153
|
+
print(f"fints-rest-server is running with PID {pid}.")
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
158
|
+
parser = argparse.ArgumentParser(prog="fints-rest-server")
|
|
159
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
160
|
+
|
|
161
|
+
start_parser = subparsers.add_parser("start", help="Start the REST API server in the background")
|
|
162
|
+
start_parser.add_argument("--env-file", type=Path, help="Load environment variables from this file before resolving defaults")
|
|
163
|
+
start_parser.add_argument("--host")
|
|
164
|
+
start_parser.add_argument("--port", type=int)
|
|
165
|
+
start_parser.add_argument("--pid-file", type=Path)
|
|
166
|
+
start_parser.add_argument("--log-file", type=Path)
|
|
167
|
+
start_parser.set_defaults(handler=_start_server)
|
|
168
|
+
|
|
169
|
+
stop_parser = subparsers.add_parser("stop", help="Stop the background REST API server")
|
|
170
|
+
stop_parser.add_argument("--pid-file", type=Path)
|
|
171
|
+
stop_parser.set_defaults(handler=_stop_server)
|
|
172
|
+
|
|
173
|
+
status_parser = subparsers.add_parser("status", help="Show whether the background REST API server is running")
|
|
174
|
+
status_parser.add_argument("--pid-file", type=Path)
|
|
175
|
+
status_parser.set_defaults(handler=_status_server)
|
|
176
|
+
|
|
177
|
+
return parser
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def main(argv: list[str] | None = None) -> int:
|
|
181
|
+
parser = build_parser()
|
|
182
|
+
normalized_argv = ["--help" if arg == "-help" else arg for arg in (argv or sys.argv[1:])]
|
|
183
|
+
args = parser.parse_args(normalized_argv)
|
|
184
|
+
return int(args.handler(args))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
raise SystemExit(main())
|