Skip to content

Commit 180da13

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

8 files changed

Lines changed: 969 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies = [
1212
"aiosqlite==0.22.1",
1313
"fastapi==0.139.2",
1414
"granian[reload]==2.8.0",
15+
"litestar[standard]==2.24.0",
1516
"packaging==26.2",
1617
"pydantic==2.14.0a1",
1718
"python-json-logger==4.1.0",

src/hub_api/client.py

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

394395
plugins[plugin_type][plugin_name].variants[variant_name] = api_schemas.VariantReference(
@@ -436,6 +437,7 @@ async def get_plugin_type_index(
436437
plugins[plugin_name] = api_schemas.PluginRef(
437438
default_variant=default_variant,
438439
logo_url=logo_http_url,
440+
variants={},
439441
)
440442

441443
plugins[plugin_name].variants[variant_name] = api_schemas.VariantReference(
@@ -449,6 +451,38 @@ async def get_plugin_type_index(
449451

450452
return plugins
451453

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

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: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""Maintainer endpoints."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Annotated
6+
7+
from litestar import Controller, get
8+
from litestar.di import NamedDependency, Provide
9+
from litestar.openapi.spec.example import Example
10+
from litestar.params import PathParameter, QueryParameter
11+
12+
from hub_api import client # ruff: ignore[typing-only-first-party-import]
13+
from hub_api.schemas import api as api_schemas # ruff: ignore[typing-only-first-party-import]
14+
15+
from .dependencies import get_hub
16+
17+
18+
class MaintainersController(Controller):
19+
path = "/meltano/api/v1/maintainers"
20+
21+
dependencies = { # ruff: ignore[mutable-class-default]
22+
"hub": Provide(get_hub),
23+
}
24+
25+
@get("/", summary="Get maintainers list")
26+
async def get_maintainers(self, hub: NamedDependency[client.MeltanoHub]) -> api_schemas.MaintainersList: # ruff: ignore[no-self-use]
27+
"""Retrieve global index of plugins."""
28+
return await hub.get_maintainers()
29+
30+
@get("/top", summary="Get top plugin maintainers")
31+
async def get_top_maintainers( # ruff: ignore[no-self-use]
32+
self,
33+
hub: NamedDependency[client.MeltanoHub],
34+
count: Annotated[
35+
int,
36+
QueryParameter(
37+
ge=1,
38+
lt=50,
39+
description="The number of maintainers to return",
40+
),
41+
] = 10,
42+
) -> list[api_schemas.MaintainerPluginCount]:
43+
"""Retrieve top maintainers."""
44+
return await hub.get_top_maintainers(count)
45+
46+
@get("/{maintainer:str}", summary="Get maintainer details")
47+
async def get_maintainer( # ruff: ignore[no-self-use]
48+
self,
49+
hub: NamedDependency[client.MeltanoHub],
50+
maintainer: Annotated[
51+
str,
52+
PathParameter(
53+
description="The maintainer identifier",
54+
pattern=r"^[A-Za-z0-9-_]+$",
55+
examples=[
56+
Example(value="meltanolabs"),
57+
Example(value="singer-io"),
58+
],
59+
),
60+
],
61+
) -> api_schemas.MaintainerDetails:
62+
"""Retrieve maintainer details."""
63+
return await hub.get_maintainer(maintainer)

0 commit comments

Comments
 (0)