mrok 0.1.6__py3-none-any.whl → 0.1.7__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.
- mrok/__init__.py +6 -0
- mrok/agent/__init__.py +0 -0
- mrok/agent/sidecar/__init__.py +3 -0
- mrok/agent/sidecar/app.py +30 -0
- mrok/agent/sidecar/main.py +27 -0
- mrok/agent/ziticorn.py +29 -0
- mrok/cli/__init__.py +3 -0
- mrok/cli/commands/__init__.py +7 -0
- mrok/cli/commands/admin/__init__.py +10 -0
- mrok/cli/commands/admin/bootstrap.py +58 -0
- mrok/cli/commands/admin/register/__init__.py +8 -0
- mrok/cli/commands/admin/register/extensions.py +46 -0
- mrok/cli/commands/admin/register/instances.py +60 -0
- mrok/cli/commands/admin/unregister/__init__.py +8 -0
- mrok/cli/commands/admin/unregister/extensions.py +33 -0
- mrok/cli/commands/admin/unregister/instances.py +34 -0
- mrok/cli/commands/admin/utils.py +23 -0
- mrok/cli/commands/agent/__init__.py +6 -0
- mrok/cli/commands/agent/run/__init__.py +7 -0
- mrok/cli/commands/agent/run/asgi.py +49 -0
- mrok/cli/commands/agent/run/sidecar.py +54 -0
- mrok/cli/commands/controller/__init__.py +7 -0
- mrok/cli/commands/controller/openapi.py +47 -0
- mrok/cli/commands/controller/run.py +87 -0
- mrok/cli/main.py +97 -0
- mrok/cli/rich.py +18 -0
- mrok/conf.py +32 -0
- mrok/controller/__init__.py +0 -0
- mrok/controller/app.py +62 -0
- mrok/controller/auth.py +87 -0
- mrok/controller/dependencies/__init__.py +4 -0
- mrok/controller/dependencies/conf.py +7 -0
- mrok/controller/dependencies/ziti.py +27 -0
- mrok/controller/openapi/__init__.py +3 -0
- mrok/controller/openapi/examples.py +44 -0
- mrok/controller/openapi/utils.py +35 -0
- mrok/controller/pagination.py +79 -0
- mrok/controller/routes.py +294 -0
- mrok/controller/schemas.py +67 -0
- mrok/errors.py +2 -0
- mrok/http/__init__.py +0 -0
- mrok/http/config.py +65 -0
- mrok/http/forwarder.py +299 -0
- mrok/http/lifespan.py +10 -0
- mrok/http/master.py +90 -0
- mrok/http/protocol.py +11 -0
- mrok/http/server.py +14 -0
- mrok/logging.py +76 -0
- mrok/ziti/__init__.py +15 -0
- mrok/ziti/api.py +467 -0
- mrok/ziti/bootstrap.py +71 -0
- mrok/ziti/constants.py +6 -0
- mrok/ziti/errors.py +25 -0
- mrok/ziti/identities.py +161 -0
- mrok/ziti/pki.py +52 -0
- mrok/ziti/services.py +87 -0
- {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/METADATA +7 -9
- mrok-0.1.7.dist-info/RECORD +61 -0
- {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/WHEEL +1 -2
- mrok-0.1.6.dist-info/RECORD +0 -6
- mrok-0.1.6.dist-info/top_level.txt +0 -1
- {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/entry_points.txt +0 -0
- {mrok-0.1.6.dist-info → mrok-0.1.7.dist-info}/licenses/LICENSE.txt +0 -0
mrok/cli/main.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from pyfiglet import Figlet
|
|
6
|
+
from rich.text import Text
|
|
7
|
+
from typer.core import TyperGroup
|
|
8
|
+
|
|
9
|
+
from mrok.cli import commands
|
|
10
|
+
from mrok.cli.rich import get_console
|
|
11
|
+
from mrok.conf import get_settings
|
|
12
|
+
from mrok.logging import setup_logging
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gradient(start_hex, end_hex, num_samples=10):
|
|
16
|
+
start_rgb = tuple(int(start_hex[i : i + 2], 16) for i in range(1, 6, 2))
|
|
17
|
+
end_rgb = tuple(int(end_hex[i : i + 2], 16) for i in range(1, 6, 2))
|
|
18
|
+
gradient_colors = [start_hex]
|
|
19
|
+
for sample in range(1, num_samples):
|
|
20
|
+
red = int(start_rgb[0] + (float(sample) / (num_samples - 1)) * (end_rgb[0] - start_rgb[0]))
|
|
21
|
+
green = int(
|
|
22
|
+
start_rgb[1] + (float(sample) / (num_samples - 1)) * (end_rgb[1] - start_rgb[1])
|
|
23
|
+
)
|
|
24
|
+
blue = int(start_rgb[2] + (float(sample) / (num_samples - 1)) * (end_rgb[2] - start_rgb[2]))
|
|
25
|
+
gradient_colors.append(f"#{red:02X}{green:02X}{blue:02X}")
|
|
26
|
+
|
|
27
|
+
return gradient_colors
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def show_banner():
|
|
31
|
+
program_name = "mrok"
|
|
32
|
+
figlet = Figlet("georgia11")
|
|
33
|
+
|
|
34
|
+
banner_text = figlet.renderText(program_name)
|
|
35
|
+
|
|
36
|
+
banner_lines = [Text(line) for line in banner_text.splitlines()]
|
|
37
|
+
max_line_length = max(len(line) for line in banner_lines)
|
|
38
|
+
half_length = max_line_length // 2
|
|
39
|
+
|
|
40
|
+
colors = gradient("#00C9CD", "#472AFF", half_length) + gradient(
|
|
41
|
+
"#472AFF", "#392D9C", half_length + 1
|
|
42
|
+
)
|
|
43
|
+
console = get_console()
|
|
44
|
+
|
|
45
|
+
for line in banner_lines:
|
|
46
|
+
colored_line = Text()
|
|
47
|
+
for i in range(len(line)):
|
|
48
|
+
char = line[i : i + 1]
|
|
49
|
+
char.stylize(colors[i])
|
|
50
|
+
colored_line = Text.assemble(colored_line, char)
|
|
51
|
+
console.print(colored_line)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MrokGroup(TyperGroup):
|
|
55
|
+
def get_help(self, ctx):
|
|
56
|
+
show_banner()
|
|
57
|
+
return super().get_help(ctx)
|
|
58
|
+
|
|
59
|
+
def invoke(self, ctx):
|
|
60
|
+
show_banner()
|
|
61
|
+
return super().invoke(ctx)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
app = typer.Typer(
|
|
65
|
+
help="SoftwareOne Marketplace Extension Channel CLI",
|
|
66
|
+
add_completion=True,
|
|
67
|
+
rich_markup_mode="rich",
|
|
68
|
+
cls=MrokGroup,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
err_console = get_console(stderr=True)
|
|
72
|
+
|
|
73
|
+
for name, module in inspect.getmembers(commands):
|
|
74
|
+
if not inspect.ismodule(module):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if hasattr(module, "command"): # pragma: no branch
|
|
78
|
+
app.command(name=name.replace("_", "-"))(module.command)
|
|
79
|
+
elif hasattr(module, "app"): # pragma: no branch
|
|
80
|
+
app.add_typer(module.app, name=name.replace("_", "-"))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.callback()
|
|
84
|
+
def main(
|
|
85
|
+
ctx: typer.Context,
|
|
86
|
+
):
|
|
87
|
+
settings = get_settings()
|
|
88
|
+
setup_logging(settings, cli_mode=True)
|
|
89
|
+
ctx.obj = settings
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def run():
|
|
93
|
+
try:
|
|
94
|
+
app()
|
|
95
|
+
except Exception as e:
|
|
96
|
+
err_console.print(f"[bold red]Error:[/bold red] {e}")
|
|
97
|
+
sys.exit(-1)
|
mrok/cli/rich.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.highlighter import ReprHighlighter
|
|
3
|
+
from rich.theme import Theme
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MrokHighlighter(ReprHighlighter):
|
|
7
|
+
prefixes = ("EXT", "ext", "INS", "ins")
|
|
8
|
+
highlights = ReprHighlighter.highlights + [
|
|
9
|
+
rf"(?P<mrok_id>(?:{'|'.join(prefixes)})(?:-\d{{4}})*)"
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_console(stderr: bool = False) -> Console:
|
|
14
|
+
return Console(
|
|
15
|
+
stderr=stderr,
|
|
16
|
+
highlighter=MrokHighlighter(),
|
|
17
|
+
theme=Theme({"repr.mrok_id": "bold light_salmon3"}),
|
|
18
|
+
)
|
mrok/conf.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from dynaconf import Dynaconf, LazySettings
|
|
2
|
+
|
|
3
|
+
type Settings = LazySettings
|
|
4
|
+
|
|
5
|
+
DEFAULT_SETTINGS = {
|
|
6
|
+
"LOGGING": {
|
|
7
|
+
"debug": False,
|
|
8
|
+
"rich": False,
|
|
9
|
+
},
|
|
10
|
+
"PROXY": {
|
|
11
|
+
"identity": "public",
|
|
12
|
+
"mode": "zrok",
|
|
13
|
+
},
|
|
14
|
+
"ZITI": {
|
|
15
|
+
"ssl_verify": False,
|
|
16
|
+
},
|
|
17
|
+
"PAGINATION": {"limit": 50},
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
_settings = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_settings() -> Settings:
|
|
24
|
+
global _settings
|
|
25
|
+
if not _settings:
|
|
26
|
+
_settings = Dynaconf(
|
|
27
|
+
envvar_prefix="MROK",
|
|
28
|
+
settings_files=["settings.yaml", ".secrets.yaml"],
|
|
29
|
+
merge_enabled=True,
|
|
30
|
+
)
|
|
31
|
+
_settings.configure(**DEFAULT_SETTINGS)
|
|
32
|
+
return _settings
|
|
File without changes
|
mrok/controller/app.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
import fastapi_pagination
|
|
5
|
+
from fastapi import Depends, FastAPI
|
|
6
|
+
from fastapi.routing import APIRoute, APIRouter
|
|
7
|
+
|
|
8
|
+
from mrok.conf import get_settings
|
|
9
|
+
from mrok.controller.auth import authenticate
|
|
10
|
+
from mrok.controller.openapi import generate_openapi_spec
|
|
11
|
+
from mrok.controller.routes import router as extensions_router
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
tags_metadata = [
|
|
17
|
+
{
|
|
18
|
+
"name": "Extensions",
|
|
19
|
+
"description": "Manage Extensions (services).",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "Instances",
|
|
23
|
+
"description": "Manage Extension Instances (identities).",
|
|
24
|
+
},
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def setup_custom_serialization(router: APIRouter):
|
|
29
|
+
for api_route in router.routes:
|
|
30
|
+
if (
|
|
31
|
+
isinstance(api_route, APIRoute)
|
|
32
|
+
and hasattr(api_route, "response_model")
|
|
33
|
+
and api_route.response_model
|
|
34
|
+
):
|
|
35
|
+
api_route.response_model_exclude_none = True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def setup_app():
|
|
39
|
+
app = FastAPI(
|
|
40
|
+
title="mrok Controller API",
|
|
41
|
+
description="API to orchestrate OpenZiti for Extensions.",
|
|
42
|
+
swagger_ui_parameters={"showExtensions": False, "showCommonExtensions": False},
|
|
43
|
+
openapi_tags=tags_metadata,
|
|
44
|
+
version="5.0.0",
|
|
45
|
+
root_path="/public/v1",
|
|
46
|
+
)
|
|
47
|
+
fastapi_pagination.add_pagination(app)
|
|
48
|
+
|
|
49
|
+
setup_custom_serialization(extensions_router)
|
|
50
|
+
|
|
51
|
+
# TODO: Add healthcheck
|
|
52
|
+
app.include_router(
|
|
53
|
+
extensions_router, prefix="/extensions", dependencies=[Depends(authenticate)]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
settings = get_settings()
|
|
57
|
+
|
|
58
|
+
app.openapi = partial(generate_openapi_spec, app, settings)
|
|
59
|
+
return app
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
app = setup_app()
|
mrok/controller/auth.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import jwt
|
|
6
|
+
from fastapi import Depends, HTTPException, Request, status
|
|
7
|
+
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
8
|
+
|
|
9
|
+
from mrok.controller.dependencies.conf import AppSettings
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("mrok.controller")
|
|
12
|
+
|
|
13
|
+
UNAUTHORIZED_EXCEPTION = HTTPException(
|
|
14
|
+
status_code=status.HTTP_401_UNAUTHORIZED, detail="Unauthorized."
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class JWTCredentials(HTTPAuthorizationCredentials):
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class JWTBearer(HTTPBearer):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__(auto_error=False)
|
|
25
|
+
|
|
26
|
+
async def __call__(self, request: Request) -> JWTCredentials:
|
|
27
|
+
credentials = await super().__call__(request)
|
|
28
|
+
if not credentials:
|
|
29
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
30
|
+
try:
|
|
31
|
+
return JWTCredentials(
|
|
32
|
+
scheme=credentials.scheme,
|
|
33
|
+
credentials=credentials.credentials,
|
|
34
|
+
)
|
|
35
|
+
except jwt.InvalidTokenError:
|
|
36
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def authenticate(
|
|
40
|
+
settings: AppSettings,
|
|
41
|
+
credentials: Annotated[JWTCredentials, Depends(JWTBearer())],
|
|
42
|
+
):
|
|
43
|
+
async with httpx.AsyncClient(
|
|
44
|
+
timeout=httpx.Timeout(
|
|
45
|
+
connect=0.25,
|
|
46
|
+
read=settings.auth.read_timeout,
|
|
47
|
+
write=2.0,
|
|
48
|
+
pool=5.0,
|
|
49
|
+
),
|
|
50
|
+
) as client:
|
|
51
|
+
try:
|
|
52
|
+
config_resp = await client.get(settings.auth.openid_config_url)
|
|
53
|
+
config_resp.raise_for_status()
|
|
54
|
+
config = config_resp.json()
|
|
55
|
+
issuer = config["issuer"]
|
|
56
|
+
jwks_uri = config["jwks_uri"]
|
|
57
|
+
|
|
58
|
+
jwks_resp = await client.get(jwks_uri)
|
|
59
|
+
jwks_resp.raise_for_status()
|
|
60
|
+
jwks = jwks_resp.json()
|
|
61
|
+
|
|
62
|
+
header = jwt.get_unverified_header(credentials.credentials)
|
|
63
|
+
kid = header["kid"]
|
|
64
|
+
|
|
65
|
+
key_data = next((k for k in jwks["keys"] if k["kid"] == kid), None)
|
|
66
|
+
except Exception:
|
|
67
|
+
logger.exception("Error fetching openid-config/jwks")
|
|
68
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
69
|
+
if key_data is None:
|
|
70
|
+
logger.error("Key ID not found in JWKS")
|
|
71
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
payload = jwt.decode(
|
|
75
|
+
credentials.credentials,
|
|
76
|
+
jwt.PyJWK(key_data),
|
|
77
|
+
algorithms=[header["alg"]],
|
|
78
|
+
issuer=issuer,
|
|
79
|
+
audience=settings.auth.audience,
|
|
80
|
+
)
|
|
81
|
+
return payload
|
|
82
|
+
except jwt.InvalidKeyError as e:
|
|
83
|
+
logger.error(f"Invalid jwt token: {e}")
|
|
84
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
85
|
+
except jwt.InvalidTokenError as e:
|
|
86
|
+
logger.error(f"Invalid jwt token: {e}")
|
|
87
|
+
raise UNAUTHORIZED_EXCEPTION
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from fastapi import Depends
|
|
5
|
+
|
|
6
|
+
from mrok.controller.dependencies.conf import AppSettings
|
|
7
|
+
from mrok.ziti import api
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class APIClientFactory[T: api.BaseZitiAPI]:
|
|
11
|
+
def __init__(self, client_cls: type[T]):
|
|
12
|
+
self.client_cls = client_cls
|
|
13
|
+
|
|
14
|
+
async def __call__(self, settings: AppSettings) -> AsyncGenerator[T]:
|
|
15
|
+
client = self.client_cls(settings)
|
|
16
|
+
async with client:
|
|
17
|
+
yield client
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ZitiManagementAPI = Annotated[
|
|
21
|
+
api.ZitiManagementAPI,
|
|
22
|
+
Depends(APIClientFactory(api.ZitiManagementAPI)),
|
|
23
|
+
]
|
|
24
|
+
ZitiClientAPI = Annotated[
|
|
25
|
+
api.ZitiClientAPI,
|
|
26
|
+
Depends(APIClientFactory(api.ZitiClientAPI)),
|
|
27
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from mrok.ziti.constants import MROK_SERVICE_TAG_NAME, MROK_VERSION_TAG_NAME
|
|
2
|
+
|
|
3
|
+
EXTENSION_RESPONSE = {
|
|
4
|
+
"id": "5Jm3PpLQ4mdzqXNRszhE0G",
|
|
5
|
+
"name": "ext-1234-5678",
|
|
6
|
+
"extension": {"id": "EXT-1234-5678"},
|
|
7
|
+
"tags": {"account": "ACC-5555-3333", MROK_VERSION_TAG_NAME: "1.0"},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
INSTANCE_RESPONSE = {
|
|
12
|
+
"id": "h.KUkPOyZ4",
|
|
13
|
+
"name": "ins-1234-5678-0001.ext-1234-5678",
|
|
14
|
+
"extension": {"id": "EXT-1234-5678"},
|
|
15
|
+
"instance": {"id": "INS-1234-5678-0001"},
|
|
16
|
+
"tags": {
|
|
17
|
+
"account": "ACC-5555-3333",
|
|
18
|
+
MROK_VERSION_TAG_NAME: "1.0",
|
|
19
|
+
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
INSTANCE_CREATE_RESPONSE = {
|
|
24
|
+
"id": "h.KUkPOyZ4",
|
|
25
|
+
"name": "ins-1234-5678-0001.ext-1234-5678",
|
|
26
|
+
"extension": {"id": "EXT-1234-5678"},
|
|
27
|
+
"instance": {"id": "INS-1234-5678-0001"},
|
|
28
|
+
"identity": {
|
|
29
|
+
"ztAPI": "https://ziti.exts.platform.softwareone.com/edge/client/v1",
|
|
30
|
+
"ztAPIs": None,
|
|
31
|
+
"configTypes": None,
|
|
32
|
+
"id": {
|
|
33
|
+
"key": "pem:-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
|
|
34
|
+
"cert": "pem:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
|
35
|
+
"ca": "pem:-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n",
|
|
36
|
+
},
|
|
37
|
+
"enableHa": None,
|
|
38
|
+
},
|
|
39
|
+
"tags": {
|
|
40
|
+
"account": "ACC-5555-3333",
|
|
41
|
+
MROK_VERSION_TAG_NAME: "1.0",
|
|
42
|
+
MROK_SERVICE_TAG_NAME: "ext-1234-5678",
|
|
43
|
+
},
|
|
44
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.openapi.utils import get_openapi
|
|
3
|
+
|
|
4
|
+
from mrok.conf import Settings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def generate_openapi_spec(app: FastAPI, settings: Settings):
|
|
8
|
+
if app.openapi_schema: # pragma: no cover
|
|
9
|
+
return app.openapi_schema
|
|
10
|
+
|
|
11
|
+
# for api_route in app.routes:
|
|
12
|
+
# if isinstance(api_route, APIRoute):
|
|
13
|
+
# for dep in api_route.dependant.dependencies:
|
|
14
|
+
# if dep.call and isinstance(dep.call, RQLQuery):
|
|
15
|
+
# api_route.description = (
|
|
16
|
+
# f"{api_route.description}\n\n"
|
|
17
|
+
# "## Available RQL filters\n\n"
|
|
18
|
+
# f"{dep.call.rules.get_documentation()}"
|
|
19
|
+
# )
|
|
20
|
+
|
|
21
|
+
spec = get_openapi(
|
|
22
|
+
title=app.title,
|
|
23
|
+
version=app.version,
|
|
24
|
+
openapi_version=app.openapi_version,
|
|
25
|
+
description=app.description,
|
|
26
|
+
tags=app.openapi_tags,
|
|
27
|
+
routes=app.routes,
|
|
28
|
+
)
|
|
29
|
+
# spec = inject_code_samples(
|
|
30
|
+
# spec,
|
|
31
|
+
# SnippetRenderer(),
|
|
32
|
+
# settings.api_base_url,
|
|
33
|
+
# )
|
|
34
|
+
app.openapi_schema = spec
|
|
35
|
+
return app.openapi_schema
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import Query
|
|
7
|
+
from fastapi_pagination import create_page, resolve_params
|
|
8
|
+
from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from mrok.controller.schemas import BaseSchema
|
|
12
|
+
from mrok.ziti.api import BaseZitiAPI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MetaPagination(BaseModel):
|
|
16
|
+
limit: int
|
|
17
|
+
offset: int
|
|
18
|
+
total: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Meta(BaseModel):
|
|
22
|
+
pagination: MetaPagination
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LimitOffsetParams(BaseModel, AbstractParams):
|
|
26
|
+
limit: int = Query(50, ge=0, le=1000, description="Page size limit")
|
|
27
|
+
offset: int = Query(0, ge=0, description="Page offset")
|
|
28
|
+
|
|
29
|
+
def to_raw_params(self) -> RawParams:
|
|
30
|
+
return RawParams( # pragma: no cover
|
|
31
|
+
limit=self.limit,
|
|
32
|
+
offset=self.offset,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LimitOffsetPage[S: BaseSchema](AbstractPage[S]):
|
|
37
|
+
data: list[S]
|
|
38
|
+
meta: Meta = Field(alias="$meta")
|
|
39
|
+
|
|
40
|
+
__params_type__ = LimitOffsetParams # type: ignore
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def create(
|
|
44
|
+
cls,
|
|
45
|
+
items: Sequence[S],
|
|
46
|
+
params: AbstractParams,
|
|
47
|
+
*,
|
|
48
|
+
total: int | None = None,
|
|
49
|
+
**kwargs: Any,
|
|
50
|
+
) -> LimitOffsetPage[S]:
|
|
51
|
+
if not isinstance(params, LimitOffsetParams): # pragma: no cover
|
|
52
|
+
raise TypeError("params must be of type LimitOffsetParams")
|
|
53
|
+
return cls( # type: ignore
|
|
54
|
+
data=items,
|
|
55
|
+
meta=Meta(
|
|
56
|
+
pagination=MetaPagination(
|
|
57
|
+
limit=params.limit,
|
|
58
|
+
offset=params.offset,
|
|
59
|
+
total=total,
|
|
60
|
+
)
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def paginate[S: BaseSchema](
|
|
66
|
+
api: BaseZitiAPI,
|
|
67
|
+
endpoint: str,
|
|
68
|
+
schema_cls: type[S],
|
|
69
|
+
extra_params: dict | None = None,
|
|
70
|
+
) -> AbstractPage[S]:
|
|
71
|
+
params: LimitOffsetParams = resolve_params()
|
|
72
|
+
page = await api.get_page(endpoint, params.limit, params.offset, extra_params)
|
|
73
|
+
pagination_meta = page["meta"]["pagination"]
|
|
74
|
+
total = pagination_meta["totalCount"]
|
|
75
|
+
return create_page(
|
|
76
|
+
[schema_cls(**item) for item in page["data"]],
|
|
77
|
+
params=params,
|
|
78
|
+
total=total,
|
|
79
|
+
)
|