pyapbase 0.0.10__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.
- pyapbase/__init__.py +31 -0
- pyapbase/apps/__init__.py +8 -0
- pyapbase/apps/hubapp.py +136 -0
- pyapbase/apps/kafka.py +166 -0
- pyapbase/apps/runner.py +69 -0
- pyapbase/apps/subapp.py +328 -0
- pyapbase/auth/__init__.py +4 -0
- pyapbase/auth/context.py +874 -0
- pyapbase/auth/token.py +242 -0
- pyapbase/crypt/__init__.py +5 -0
- pyapbase/crypt/jwt.py +609 -0
- pyapbase/crypt/key.py +885 -0
- pyapbase/store/__init__.py +15 -0
- pyapbase/store/dbcache.py +385 -0
- pyapbase/store/dbcore.py +476 -0
- pyapbase/store/dbcrud.py +627 -0
- pyapbase/store/objstore.py +235 -0
- pyapbase/store/redstore.py +154 -0
- pyapbase/utils/__init__.py +11 -0
- pyapbase/utils/apires.py +16 -0
- pyapbase/utils/config.py +50 -0
- pyapbase/utils/infoui.py +35 -0
- pyapbase/utils/logger.py +80 -0
- pyapbase/utils/reldir.py +21 -0
- pyapbase-0.0.10.dist-info/METADATA +29 -0
- pyapbase-0.0.10.dist-info/RECORD +27 -0
- pyapbase-0.0.10.dist-info/WHEEL +4 -0
pyapbase/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Docstring"""
|
|
2
|
+
"""Docstring"""
|
|
3
|
+
from .store import DBCORE, DBCRUD, CACHE, OBJSTORE, REDSTORE
|
|
4
|
+
from .crypt import KEY, JWT
|
|
5
|
+
from .auth import VERIFY, CONTEXT
|
|
6
|
+
from .apps import APPS, SAPS, KAFKA, DRAMATIQ
|
|
7
|
+
|
|
8
|
+
from .utils import Logging, ConfigUtil, RelDirs, ClientInfo, APIResponse
|
|
9
|
+
|
|
10
|
+
__all__ = ["DBMS","CAUTH", "APPS", "SAPS", "KAFKA", "DRAMATIQ", "UTILS"]
|
|
11
|
+
|
|
12
|
+
class DBMS:
|
|
13
|
+
# Store
|
|
14
|
+
DBCORE = DBCORE
|
|
15
|
+
DBCRUD = DBCRUD
|
|
16
|
+
CACHE = CACHE
|
|
17
|
+
OBJSTORE = OBJSTORE
|
|
18
|
+
REDSTORE = REDSTORE
|
|
19
|
+
|
|
20
|
+
class CAUTH:
|
|
21
|
+
KEY = KEY
|
|
22
|
+
JWT = JWT
|
|
23
|
+
CONTEXT = CONTEXT
|
|
24
|
+
VERIFY = VERIFY
|
|
25
|
+
|
|
26
|
+
class UTILS:
|
|
27
|
+
Logging = Logging
|
|
28
|
+
ConfigUtil = ConfigUtil
|
|
29
|
+
ClientInfo = ClientInfo
|
|
30
|
+
RelDirs = RelDirs
|
|
31
|
+
APIResponse = APIResponse
|
pyapbase/apps/hubapp.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Docstring"""
|
|
2
|
+
from sqlmodel import MetaData, Session
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
from fastapi import FastAPI, Request, Depends, WebSocket
|
|
5
|
+
from contextlib import asynccontextmanager, AsyncExitStack
|
|
6
|
+
from typing import Dict, Any, Annotated, Optional, List, Callable, Awaitable
|
|
7
|
+
|
|
8
|
+
__all__ = ["APPS"]
|
|
9
|
+
|
|
10
|
+
class DeployBase(BaseSettings):
|
|
11
|
+
srv_env: str = ".env.server"
|
|
12
|
+
dbc_env: str = ".env.dbapi"
|
|
13
|
+
oau_env: str = ".env.oauth"
|
|
14
|
+
key_env: str = ".env.secret"
|
|
15
|
+
sap_env: str = ".env.sapps"
|
|
16
|
+
env_secrets: Optional[str] = None
|
|
17
|
+
|
|
18
|
+
class ServerBase(BaseSettings):
|
|
19
|
+
host: str = "0.0.0.0"
|
|
20
|
+
port: int = 8000
|
|
21
|
+
reload: bool = False
|
|
22
|
+
workers: int = 1
|
|
23
|
+
https_redir: bool = False
|
|
24
|
+
session_secret: Optional[str] = None
|
|
25
|
+
allowed_hosts: Optional[List[str]] = None
|
|
26
|
+
allowed_origins: Optional[List[str]] = None
|
|
27
|
+
deploy_env: Optional[str] = None
|
|
28
|
+
eager_dispatch: bool = False
|
|
29
|
+
secure_cookie: Optional[bool] = None
|
|
30
|
+
global_cookie: Optional[bool] = None
|
|
31
|
+
cookie_methods: Optional[List[str]] = None
|
|
32
|
+
kongopa_schema: Optional[str] = None
|
|
33
|
+
supvcfg_dir: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
class AppMeta:
|
|
36
|
+
def __init__(self, metas: Dict[str, MetaData]):
|
|
37
|
+
self.metalist = list(metas.values())
|
|
38
|
+
self.schmlist = list(metas.keys())
|
|
39
|
+
self.metadict = metas
|
|
40
|
+
|
|
41
|
+
class AppSpan:
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
amigs: Optional[List[Any]] = None,
|
|
45
|
+
spans: Optional[Dict[FastAPI, Any]] = None,
|
|
46
|
+
startup: Optional[List[Callable[[], Awaitable[None]]]] = None,
|
|
47
|
+
shutdown: Optional[List[Callable[[], Awaitable[None]]]] = None,
|
|
48
|
+
):
|
|
49
|
+
self.amig_list = amigs or []
|
|
50
|
+
self.span_dict = spans or {}
|
|
51
|
+
self.startup = startup or []
|
|
52
|
+
self.shutdown = shutdown or []
|
|
53
|
+
|
|
54
|
+
@asynccontextmanager
|
|
55
|
+
async def lifespan(self, app: FastAPI):
|
|
56
|
+
async with AsyncExitStack() as stack:
|
|
57
|
+
# Manage the lifecycle of sub_app
|
|
58
|
+
for subamig in self.amig_list:
|
|
59
|
+
await stack.enter_async_context(subamig())
|
|
60
|
+
for subapp, subspan in self.span_dict.items():
|
|
61
|
+
await stack.enter_async_context(subspan(subapp))
|
|
62
|
+
for fn in self.startup:
|
|
63
|
+
await fn()
|
|
64
|
+
yield
|
|
65
|
+
for fn in self.shutdown:
|
|
66
|
+
await fn()
|
|
67
|
+
|
|
68
|
+
class AppMsvc:
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
apps: Dict[str, FastAPI],
|
|
72
|
+
lifespan,
|
|
73
|
+
sessdep: Optional[Any] = None,
|
|
74
|
+
deploy_env: Optional[str] = None,
|
|
75
|
+
):
|
|
76
|
+
depends = [
|
|
77
|
+
Depends(self.session_http(sessdep)),
|
|
78
|
+
Depends(self.session_ws(sessdep))
|
|
79
|
+
] if sessdep is not None else None
|
|
80
|
+
deploy_env = deploy_env or "production"
|
|
81
|
+
is_production = deploy_env == "production"
|
|
82
|
+
if is_production:
|
|
83
|
+
self.app = FastAPI(
|
|
84
|
+
dependencies= depends,
|
|
85
|
+
redirect_slashes=False,
|
|
86
|
+
lifespan=lifespan,
|
|
87
|
+
docs_url=None,
|
|
88
|
+
redoc_url=None,
|
|
89
|
+
openapi_url=None
|
|
90
|
+
)
|
|
91
|
+
else:
|
|
92
|
+
self.app = FastAPI(
|
|
93
|
+
dependencies= depends,
|
|
94
|
+
redirect_slashes=False,
|
|
95
|
+
lifespan=lifespan
|
|
96
|
+
)
|
|
97
|
+
app_msg = {"message": "Hello Multi Micro Services API!"}
|
|
98
|
+
for key, subapp in apps.items():
|
|
99
|
+
self.app.mount(f"/{key}", subapp)
|
|
100
|
+
app_msg[key] = f"{key.capitalize()} Service at '/{key}'"
|
|
101
|
+
|
|
102
|
+
@self.app.get("/welcome")
|
|
103
|
+
async def welcome():
|
|
104
|
+
return app_msg
|
|
105
|
+
|
|
106
|
+
@self.app.get("/health", tags=["Health"])
|
|
107
|
+
async def health():
|
|
108
|
+
return {"status": "ok"}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def session_ws(sessdep):
|
|
113
|
+
def _session(
|
|
114
|
+
websocket: WebSocket,
|
|
115
|
+
sess_pgs: Annotated[Session, Depends(sessdep)]
|
|
116
|
+
):
|
|
117
|
+
"""Injects session into request scope."""
|
|
118
|
+
websocket.scope["db"] = {"pgsql": sess_pgs}
|
|
119
|
+
return _session
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def session_http(sessdep):
|
|
123
|
+
def _session(
|
|
124
|
+
request: Request,
|
|
125
|
+
sess_pgs: Annotated[Session, Depends(sessdep)]
|
|
126
|
+
):
|
|
127
|
+
"""Injects session into request scope."""
|
|
128
|
+
request.scope["db"] = {"pgsql": sess_pgs}
|
|
129
|
+
return _session
|
|
130
|
+
|
|
131
|
+
class APPS:
|
|
132
|
+
AppMeta = AppMeta
|
|
133
|
+
AppSpan = AppSpan
|
|
134
|
+
AppMsvc = AppMsvc
|
|
135
|
+
DeployBase = DeployBase
|
|
136
|
+
ServerBase = ServerBase
|
pyapbase/apps/kafka.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
|
|
2
|
+
import json, ssl, hashlib, logging, asyncio
|
|
3
|
+
from typing import Dict, Optional, Union, List, Callable
|
|
4
|
+
|
|
5
|
+
from aiokafka import AIOKafkaConsumer, AIOKafkaProducer
|
|
6
|
+
from pydantic_settings import BaseSettings
|
|
7
|
+
from ..store import DBCORE, DBCRUD
|
|
8
|
+
|
|
9
|
+
__all__ = ["GlobalConsumer", ]
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
PgsCore = DBCORE.PgsCore
|
|
14
|
+
OdbMsvc = DBCRUD.OdbMsvc
|
|
15
|
+
DbsMsvc = DBCRUD.DbsMsvc
|
|
16
|
+
|
|
17
|
+
class ConfigBase(BaseSettings):
|
|
18
|
+
kafka_bootstrap_servers: str = "kafka-service:9092"
|
|
19
|
+
security_protocol: str = "PLAINTEXT"
|
|
20
|
+
sasl_plain_username: Optional[str] = None
|
|
21
|
+
sasl_plain_password: Optional[str] = None
|
|
22
|
+
kafka_topic: str = "audit_logs"
|
|
23
|
+
consumer_group: str = "audit_logger"
|
|
24
|
+
|
|
25
|
+
class GlobalConsumer:
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
config:ConfigBase,
|
|
29
|
+
kafka_topics: Optional[Union[str, List[str]]] = None
|
|
30
|
+
):
|
|
31
|
+
kafka_topics = kafka_topics or config.kafka_topic
|
|
32
|
+
self.config = config
|
|
33
|
+
self.topics = [kafka_topics] if isinstance(kafka_topics, str) else kafka_topics
|
|
34
|
+
self.handlers: Dict[str, Callable] = {}
|
|
35
|
+
self.default_handler: Optional[Callable] = None
|
|
36
|
+
|
|
37
|
+
# ---------------- DECORATOR API ----------------
|
|
38
|
+
|
|
39
|
+
def register(self, topic: Optional[str] = None):
|
|
40
|
+
def decorator(fn) -> Callable:
|
|
41
|
+
if topic:
|
|
42
|
+
self.handlers[topic] = fn
|
|
43
|
+
else:
|
|
44
|
+
self.default_handler = fn
|
|
45
|
+
return fn
|
|
46
|
+
return decorator
|
|
47
|
+
|
|
48
|
+
# ---------------- EXECUTION ----------------
|
|
49
|
+
|
|
50
|
+
async def _execute(self, handler, data):
|
|
51
|
+
try:
|
|
52
|
+
result = handler(data) # keep simple contract
|
|
53
|
+
if asyncio.iscoroutine(result):
|
|
54
|
+
await result
|
|
55
|
+
except Exception:
|
|
56
|
+
log.exception("Handler execution failed")
|
|
57
|
+
|
|
58
|
+
async def _process(self, msg):
|
|
59
|
+
|
|
60
|
+
if not msg.key or not msg.value:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
if msg.key != hashlib.sha256(msg.value).hexdigest().encode():
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
handler = self.handlers.get(msg.topic, self.default_handler)
|
|
67
|
+
if not handler:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads(msg.value)
|
|
72
|
+
await self._execute(handler, data)
|
|
73
|
+
except Exception:
|
|
74
|
+
log.exception("Message processing failed offset=%s", msg.offset)
|
|
75
|
+
|
|
76
|
+
# ---------------- RUN ----------------
|
|
77
|
+
|
|
78
|
+
async def run(self):
|
|
79
|
+
bootstrap_servers=self.config.kafka_bootstrap_servers
|
|
80
|
+
ssl_context = None
|
|
81
|
+
sasl_mechanism= "PLAIN"
|
|
82
|
+
security_protocol = "PLAINTEXT"
|
|
83
|
+
sasl_plain_username = None
|
|
84
|
+
sasl_plain_password = None
|
|
85
|
+
if self.config.security_protocol == "SASL_SSL":
|
|
86
|
+
ssl_context = ssl.create_default_context()
|
|
87
|
+
security_protocol = self.config.security_protocol
|
|
88
|
+
sasl_plain_username=self.config.sasl_plain_username
|
|
89
|
+
sasl_plain_password=self.config.sasl_plain_password
|
|
90
|
+
|
|
91
|
+
consumer = AIOKafkaConsumer(
|
|
92
|
+
*self.topics,
|
|
93
|
+
bootstrap_servers=bootstrap_servers,
|
|
94
|
+
ssl_context=ssl_context,
|
|
95
|
+
sasl_mechanism=sasl_mechanism,
|
|
96
|
+
security_protocol=security_protocol,
|
|
97
|
+
sasl_plain_username=sasl_plain_username,
|
|
98
|
+
sasl_plain_password=sasl_plain_password,
|
|
99
|
+
group_id=self.config.consumer_group,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
await consumer.start()
|
|
103
|
+
log.info("Global consumer started")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
async for msg in consumer:
|
|
107
|
+
await self._process(msg)
|
|
108
|
+
finally:
|
|
109
|
+
await consumer.stop()
|
|
110
|
+
log.info("Kafka consumer stopped")
|
|
111
|
+
|
|
112
|
+
class GlobalProducer:
|
|
113
|
+
def __init__(self, config:ConfigBase):
|
|
114
|
+
self.config = config
|
|
115
|
+
self.producer: AIOKafkaProducer | None = None
|
|
116
|
+
|
|
117
|
+
async def start(self):
|
|
118
|
+
bootstrap_servers = self.config.kafka_bootstrap_servers
|
|
119
|
+
ssl_context = None
|
|
120
|
+
security_protocol = "PLAINTEXT"
|
|
121
|
+
sasl_mechanism= "PLAIN"
|
|
122
|
+
sasl_plain_username = None
|
|
123
|
+
sasl_plain_password = None
|
|
124
|
+
if self.config.security_protocol == "SASL_SSL":
|
|
125
|
+
ssl_context = ssl.create_default_context()
|
|
126
|
+
security_protocol = self.config.security_protocol
|
|
127
|
+
sasl_plain_username=self.config.sasl_plain_username
|
|
128
|
+
sasl_plain_password=self.config.sasl_plain_password
|
|
129
|
+
try:
|
|
130
|
+
self.producer = AIOKafkaProducer(
|
|
131
|
+
bootstrap_servers=bootstrap_servers,
|
|
132
|
+
ssl_context=ssl_context,
|
|
133
|
+
sasl_mechanism=sasl_mechanism,
|
|
134
|
+
security_protocol=security_protocol,
|
|
135
|
+
sasl_plain_username=sasl_plain_username,
|
|
136
|
+
sasl_plain_password=sasl_plain_password,
|
|
137
|
+
)
|
|
138
|
+
await self.producer.start()
|
|
139
|
+
log.info("Kafka producer started")
|
|
140
|
+
except Exception:
|
|
141
|
+
log.exception("Kafka producer start failed")
|
|
142
|
+
raise RuntimeError("Producer initialization failed")
|
|
143
|
+
|
|
144
|
+
async def stop(self):
|
|
145
|
+
if self.producer:
|
|
146
|
+
await self.producer.stop()
|
|
147
|
+
log.info("Kafka producer stopped")
|
|
148
|
+
|
|
149
|
+
async def send_event(self, event: dict, topic: Optional[str] = None):
|
|
150
|
+
if not self.producer:
|
|
151
|
+
raise Exception("GlobalProducer: producer not started")
|
|
152
|
+
try:
|
|
153
|
+
topic = topic or self.config.kafka_topic
|
|
154
|
+
payload = json.dumps(event).encode("utf-8")
|
|
155
|
+
await self.producer.send_and_wait(
|
|
156
|
+
topic = topic,
|
|
157
|
+
value = payload,
|
|
158
|
+
key = hashlib.sha256(payload).hexdigest().encode("utf-8")
|
|
159
|
+
)
|
|
160
|
+
except Exception:
|
|
161
|
+
raise Exception("GlobalProducer: Kafka send failed")
|
|
162
|
+
|
|
163
|
+
class KAFKA:
|
|
164
|
+
ConfigBase = ConfigBase
|
|
165
|
+
GlobalConsumer = GlobalConsumer
|
|
166
|
+
GlobalProducer = GlobalProducer
|
pyapbase/apps/runner.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
broker.py
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import platform, sys, logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
import dramatiq
|
|
10
|
+
import dramatiq.cli as cli
|
|
11
|
+
|
|
12
|
+
__all__ = ["setup_broker"]
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_broker_configured = False
|
|
17
|
+
|
|
18
|
+
def setup_broker(
|
|
19
|
+
redis_url: Optional[str] = None
|
|
20
|
+
) -> dramatiq.Broker:
|
|
21
|
+
global _broker_configured
|
|
22
|
+
if _broker_configured:
|
|
23
|
+
return dramatiq.get_broker()
|
|
24
|
+
|
|
25
|
+
if redis_url:
|
|
26
|
+
from dramatiq.brokers.redis import RedisBroker
|
|
27
|
+
broker = RedisBroker(url=redis_url)
|
|
28
|
+
log.info("Dramatiq broker: Redis at %s", redis_url)
|
|
29
|
+
else:
|
|
30
|
+
from dramatiq.brokers.stub import StubBroker
|
|
31
|
+
broker = StubBroker()
|
|
32
|
+
broker.emit_after("process_boot")
|
|
33
|
+
log.info("Dramatiq broker: StubBroker (in-memory, no redis_url)")
|
|
34
|
+
|
|
35
|
+
old_broker = dramatiq.get_broker()
|
|
36
|
+
for actor in old_broker.actors.values():
|
|
37
|
+
actor.broker = broker
|
|
38
|
+
broker.declare_actor(actor)
|
|
39
|
+
|
|
40
|
+
dramatiq.set_broker(broker)
|
|
41
|
+
_broker_configured = True
|
|
42
|
+
return broker
|
|
43
|
+
|
|
44
|
+
class DramatiqRunner:
|
|
45
|
+
"""Launches a Dramatiq CLI worker process for a configurable set of actor modules."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, *modules, redis_url: Optional[str] = None,):
|
|
48
|
+
self.modules = []
|
|
49
|
+
for module in modules:
|
|
50
|
+
self.add_module(module)
|
|
51
|
+
setup_broker(redis_url)
|
|
52
|
+
|
|
53
|
+
def add_module(self, module):
|
|
54
|
+
self.modules.append(module.__name__ if hasattr(module, "__name__") else module)
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
def run(self, concurrency=1, threads=8):
|
|
58
|
+
processes = 1 if platform.system() == "Windows" else concurrency
|
|
59
|
+
argv = [
|
|
60
|
+
*self.modules,
|
|
61
|
+
"--threads", str(threads),
|
|
62
|
+
"--processes", str(processes),
|
|
63
|
+
]
|
|
64
|
+
args = cli.make_argument_parser().parse_args(argv)
|
|
65
|
+
sys.exit(cli.main(args))
|
|
66
|
+
|
|
67
|
+
class DRAMATIQ:
|
|
68
|
+
DramatiqRunner = DramatiqRunner
|
|
69
|
+
setup_broker = staticmethod(setup_broker)
|