Skip to content

Commit c70456b

Browse files
renovate[bot]edgarrmondragon
authored andcommitted
WIP: Litestar
Signed-off-by: Edgar Ramírez Mondragón <edgarrm358@gmail.com>
1 parent 0f0d96e commit c70456b

8 files changed

Lines changed: 970 additions & 4 deletions

File tree

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies = [
1111
"aiosqlite~=0.22.0",
1212
"fastapi==0.128.3",
1313
"granian[reload]~=2.7.0",
14+
"litestar[standard]~=2.20.0",
1415
"packaging~=26.0",
1516
"pydantic~=2.12",
1617
"python-json-logger~=4.0",
@@ -56,6 +57,9 @@ exclude-newer = "P7D"
5657
preview = true
5758
required-version = ">=0.9.17"
5859

60+
# [tool.uv.sources]
61+
# litestar = { git = "https://github.com/litestar-org/litestar", rev = "main" }
62+
5963
[tool.deptry.per_rule_ignores]
6064
DEP002 = [
6165
"granian", # Not imported, but used as the ASGI server as a uvicorn alternative
@@ -114,6 +118,8 @@ addopts = ["--durations=5", "-ra", "--strict-config", "--strict-markers"]
114118
asyncio_default_fixture_loop_scope = "session"
115119
filterwarnings = [
116120
"error",
121+
"once:Core Pydantic V1 functionality isn't compatible with Python 3.14 or greater:UserWarning",
122+
"once:Call to deprecated function 'resolve_before_request'. Deprecated in litestar 3.0. This function will be removed in 4.0. Use '.before_request attribute' instead:DeprecationWarning",
117123
]
118124
log_cli_level = "INFO"
119125
minversion = "9"

src/hub_api/client.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ async def get_plugin_index(self: MeltanoHub) -> api_schemas.PluginIndex:
388388
plugins[plugin_type][plugin_name] = api_schemas.PluginRef(
389389
default_variant=default_variant,
390390
logo_url=logo_http_url,
391+
variants={},
391392
)
392393

393394
plugins[plugin_type][plugin_name].variants[variant_name] = api_schemas.VariantReference(
@@ -435,6 +436,7 @@ async def get_plugin_type_index(
435436
plugins[plugin_name] = api_schemas.PluginRef(
436437
default_variant=default_variant,
437438
logo_url=logo_http_url,
439+
variants={},
438440
)
439441

440442
plugins[plugin_name].variants[variant_name] = api_schemas.VariantReference(
@@ -448,6 +450,38 @@ async def get_plugin_type_index(
448450

449451
return plugins
450452

453+
async def get_plugin_variants(self: MeltanoHub, plugin_id: ids.PluginID) -> api_schemas.PluginRef:
454+
"""Get plugin variants."""
455+
sql = """
456+
SELECT
457+
p.name AS plugin,
458+
p.plugin_type,
459+
pv.logo_url,
460+
pv.name AS variant
461+
FROM plugins p
462+
JOIN plugin_variants pv ON pv.plugin_id = p.id
463+
WHERE p.id = :plugin_id AND p.plugin_type = :plugin_type
464+
"""
465+
ref = api_schemas.PluginRef(default_variant="", logo_url=None, variants={})
466+
params = {
467+
"plugin_id": plugin_id.as_db_id(),
468+
"plugin_type": plugin_id.plugin_type.value,
469+
}
470+
for row in await fetch_all_dicts(self.db, sql, params):
471+
if not ref.default_variant:
472+
ref.default_variant = row["variant"]
473+
# breakpoint()
474+
ref.logo_url = pydantic.HttpUrl(f"{self.base_hub_url}{row['logo_url']}") if row["logo_url"] else None
475+
ref.variants[row["variant"]] = api_schemas.VariantReference(
476+
ref=_build_variant_path(
477+
plugin_type=plugin_id.plugin_type,
478+
plugin_name=plugin_id.plugin_name,
479+
plugin_variant=row["variant"],
480+
base_url=self.base_url,
481+
),
482+
)
483+
return ref
484+
451485
async def get_sdk_plugins(
452486
self: MeltanoHub,
453487
*,

src/hub_api/litestar_app/app.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import annotations
2+
3+
import http
4+
5+
from litestar import Litestar, MediaType, Request, Response
6+
from litestar.config.compression import CompressionConfig
7+
from litestar.exceptions import MethodNotAllowedException
8+
from litestar.plugins.pydantic import PydanticPlugin
9+
10+
from hub_api.exceptions import NotFoundError
11+
12+
from . import maintainers, plugins
13+
14+
15+
def not_found_exception_handler(_: Request, exc: NotFoundError) -> Response: # type: ignore[type-arg]
16+
"""Default handler for exceptions subclassed from HTTPException."""
17+
return Response(
18+
media_type=MediaType.JSON,
19+
content={"details": exc.args[0]},
20+
status_code=http.HTTPStatus.NOT_FOUND,
21+
)
22+
23+
24+
def method_not_allowed_exception_handler(_: Request, exc: MethodNotAllowedException) -> Response: # type: ignore[type-arg]
25+
"""Default handler for exceptions subclassed from HTTPException."""
26+
return Response(
27+
media_type=MediaType.JSON,
28+
content={"details": exc.args[0]},
29+
status_code=http.HTTPStatus.METHOD_NOT_ALLOWED,
30+
headers=exc.headers,
31+
)
32+
33+
34+
app = Litestar(
35+
[
36+
maintainers.MaintainersController,
37+
plugins.PluginsController,
38+
],
39+
compression_config=CompressionConfig(backend="gzip", minimum_size=1000),
40+
exception_handlers={ # ty: ignore[invalid-argument-type]
41+
NotFoundError: not_found_exception_handler,
42+
MethodNotAllowedException: method_not_allowed_exception_handler,
43+
},
44+
plugins=[
45+
PydanticPlugin(
46+
exclude_none=True,
47+
exclude_unset=True,
48+
),
49+
],
50+
debug=True,
51+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Litestar app dependencies."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
7+
from hub_api import client, database
8+
9+
if TYPE_CHECKING:
10+
from collections.abc import AsyncGenerator
11+
12+
from litestar.connection import Request
13+
14+
15+
async def get_hub(request: Request) -> AsyncGenerator[client.MeltanoHub]: # type: ignore[type-arg]
16+
"""Get a Meltano hub instance."""
17+
db = await database.open_db()
18+
try:
19+
yield client.MeltanoHub(db=db, base_url=str(request.base_url))
20+
finally:
21+
await db.close()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Maintainer endpoints."""
2+
3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Annotated, ClassVar
6+
7+
from litestar import Controller, get
8+
from litestar.di import Provide
9+
from litestar.openapi.spec.example import Example
10+
from litestar.params import Parameter
11+
12+
from hub_api import client # noqa: TC001
13+
from hub_api.schemas import api as api_schemas # noqa: TC001
14+
15+
from .dependencies import get_hub
16+
17+
if TYPE_CHECKING:
18+
from litestar.types.composite_types import Dependencies
19+
20+
21+
class MaintainersController(Controller):
22+
path = "/meltano/api/v1/maintainers"
23+
24+
dependencies: ClassVar[Dependencies] = { # type: ignore[misc]
25+
"hub": Provide(get_hub),
26+
}
27+
28+
@get("/", summary="Get maintainers list")
29+
async def get_maintainers(self, hub: client.MeltanoHub) -> api_schemas.MaintainersList: # noqa: PLR6301
30+
"""Retrieve global index of plugins."""
31+
return await hub.get_maintainers()
32+
33+
@get("/top", summary="Get top plugin maintainers")
34+
async def get_top_maintainers( # noqa: PLR6301
35+
self,
36+
hub: client.MeltanoHub,
37+
count: Annotated[
38+
int,
39+
Parameter(
40+
...,
41+
ge=1,
42+
lt=50,
43+
description="The number of maintainers to return",
44+
),
45+
] = 10,
46+
) -> list[api_schemas.MaintainerPluginCount]:
47+
"""Retrieve top maintainers."""
48+
return await hub.get_top_maintainers(count)
49+
50+
@get("/{maintainer:str}", summary="Get maintainer details")
51+
async def get_maintainer( # noqa: PLR6301
52+
self,
53+
hub: client.MeltanoHub,
54+
maintainer: Annotated[
55+
str,
56+
Parameter(
57+
...,
58+
description="The maintainer identifier",
59+
pattern=r"^[A-Za-z0-9-_]+$",
60+
examples=[
61+
Example(value="meltanolabs"),
62+
Example(value="singer-io"),
63+
],
64+
),
65+
],
66+
) -> api_schemas.MaintainerDetails:
67+
"""Retrieve maintainer details."""
68+
return await hub.get_maintainer(maintainer)

0 commit comments

Comments
 (0)