gandharva 0.0.1__tar.gz
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.
- gandharva-0.0.1/PKG-INFO +55 -0
- gandharva-0.0.1/README.md +4 -0
- gandharva-0.0.1/pyproject.toml +100 -0
- gandharva-0.0.1/src/gandharva/__init__.py +35 -0
- gandharva-0.0.1/src/gandharva/_app/__init__.py +17 -0
- gandharva-0.0.1/src/gandharva/_app/_base.py +72 -0
- gandharva-0.0.1/src/gandharva/_app/_fastapi.py +147 -0
- gandharva-0.0.1/src/gandharva/_app/_gandharva.py +82 -0
- gandharva-0.0.1/src/gandharva/_app/_panel.py +354 -0
- gandharva-0.0.1/src/gandharva/_app/_pydantic.py +357 -0
- gandharva-0.0.1/src/gandharva/_convert/__init__.py +29 -0
- gandharva-0.0.1/src/gandharva/_convert/_common.py +107 -0
- gandharva-0.0.1/src/gandharva/_convert/_from_pydantic.py +25 -0
- gandharva-0.0.1/src/gandharva/_convert/_to_cli.py +66 -0
- gandharva-0.0.1/src/gandharva/_convert/_to_fastapi.py +69 -0
- gandharva-0.0.1/src/gandharva/_convert/_to_panel.py +85 -0
- gandharva-0.0.1/src/gandharva/_convert/_to_pydantic.py +23 -0
- gandharva-0.0.1/src/gandharva/_runner.py +147 -0
- gandharva-0.0.1/src/gandharva/exceptions.py +33 -0
- gandharva-0.0.1/src/gandharva/py.typed +0 -0
- gandharva-0.0.1/src/gandharva/typing/__init__.py +41 -0
- gandharva-0.0.1/src/gandharva/typing/_fastapi.py +110 -0
- gandharva-0.0.1/src/gandharva/typing/_panel.py +198 -0
gandharva-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gandharva
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: All-purpose data science application builder
|
|
5
|
+
Author: hingebase
|
|
6
|
+
Author-email: hingebase <zcliu@pku.edu.cn>
|
|
7
|
+
License-Expression: Apache-2.0
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Framework :: FastAPI
|
|
12
|
+
Classifier: Framework :: Pydantic :: 2
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
27
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
28
|
+
Classifier: Topic :: Education
|
|
29
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
30
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
31
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
32
|
+
Classifier: Typing :: Typed
|
|
33
|
+
Requires-Dist: fastapi>=0.119.1,<2.0.0
|
|
34
|
+
Requires-Dist: hypothesis-jsonschema>=0.23.1,<2.0.0
|
|
35
|
+
Requires-Dist: lumen>=0.6.1,<2.0.0
|
|
36
|
+
Requires-Dist: packaging>=26.0.0
|
|
37
|
+
Requires-Dist: panel>=1.8.3,<2.0.0
|
|
38
|
+
Requires-Dist: platformdirs>=4.6.0,<5.0.0
|
|
39
|
+
Requires-Dist: pydantic>=2.12.1,<4.0.0
|
|
40
|
+
Requires-Dist: pydantic-settings>=2.13.0,<4.0.0
|
|
41
|
+
Requires-Dist: rich>=14.2.0
|
|
42
|
+
Requires-Dist: rich-argparse>=1.7.2,<2.0.0
|
|
43
|
+
Requires-Dist: typing-extensions>=4.15.0,<5.0.0
|
|
44
|
+
Requires-Dist: pandas-stubs>=1.4.2,<4.0.0 ; extra == 'typing'
|
|
45
|
+
Requires-Python: >=3.10, <3.15
|
|
46
|
+
Project-URL: Homepage, https://github.com/hingebase/gandharva
|
|
47
|
+
Project-URL: Source Code, https://github.com/hingebase/gandharva
|
|
48
|
+
Project-URL: Issue Tracker, https://github.com/hingebase/gandharva/issues
|
|
49
|
+
Provides-Extra: typing
|
|
50
|
+
Description-Content-Type: text/markdown
|
|
51
|
+
|
|
52
|
+
# Gandharva
|
|
53
|
+
In active development. Check the [issue tracker][1] for upcoming features.
|
|
54
|
+
|
|
55
|
+
[1]: https://github.com/hingebase/gandharva/issues
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv-build >=0.10.2,<0.12.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gandharva"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "All-purpose data science application builder"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10,<3.15"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Environment :: Console",
|
|
15
|
+
"Environment :: Web Environment",
|
|
16
|
+
"Framework :: FastAPI",
|
|
17
|
+
"Framework :: Pydantic :: 2",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Education",
|
|
20
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
21
|
+
"Intended Audience :: Information Technology",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"Operating System :: MacOS",
|
|
24
|
+
"Operating System :: Microsoft :: Windows",
|
|
25
|
+
"Operating System :: POSIX :: Linux",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
"Programming Language :: Python :: Implementation :: CPython",
|
|
33
|
+
"Topic :: Education",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
35
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
36
|
+
"Topic :: Software Development :: User Interfaces",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
dependencies = [
|
|
40
|
+
"fastapi >=0.119.1,<2.0.0",
|
|
41
|
+
"hypothesis-jsonschema >=0.23.1,<2.0.0",
|
|
42
|
+
"lumen >=0.6.1,<2.0.0",
|
|
43
|
+
"packaging >=26.0.0",
|
|
44
|
+
"panel >=1.8.3,<2.0.0",
|
|
45
|
+
"platformdirs >=4.6.0,<5.0.0",
|
|
46
|
+
"pydantic >=2.12.1,<4.0.0",
|
|
47
|
+
"pydantic-settings >=2.13.0,<4.0.0",
|
|
48
|
+
"rich >=14.2.0",
|
|
49
|
+
"rich-argparse >=1.7.2,<2.0.0",
|
|
50
|
+
"typing-extensions >=4.15.0,<5.0.0",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.optional-dependencies]
|
|
54
|
+
typing = [
|
|
55
|
+
"pandas-stubs >=1.4.2,<4.0.0",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[dependency-groups]
|
|
59
|
+
dev = [
|
|
60
|
+
"attrs >=25.4.0",
|
|
61
|
+
"basedpyright >=1.39.3",
|
|
62
|
+
"httpx >=0.28.1",
|
|
63
|
+
"nodejs-wheel-binaries <=20.18.0; sys_platform == 'darwin' and platform_machine == 'x86_64'",
|
|
64
|
+
"pytest >=9.0.3",
|
|
65
|
+
"ruff >=0.15.11",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[[project.authors]]
|
|
69
|
+
name = "hingebase"
|
|
70
|
+
email = "zcliu@pku.edu.cn"
|
|
71
|
+
|
|
72
|
+
[project.urls]
|
|
73
|
+
Homepage = "https://github.com/hingebase/gandharva"
|
|
74
|
+
"Source Code" = "https://github.com/hingebase/gandharva"
|
|
75
|
+
"Issue Tracker" = "https://github.com/hingebase/gandharva/issues"
|
|
76
|
+
|
|
77
|
+
[tool.basedpyright]
|
|
78
|
+
reportUnnecessaryTypeIgnoreComment = "warning"
|
|
79
|
+
strict = ["."]
|
|
80
|
+
typeCheckingMode = "off"
|
|
81
|
+
|
|
82
|
+
[tool.ruff]
|
|
83
|
+
extend-exclude = [".pixi"]
|
|
84
|
+
line-length = 79
|
|
85
|
+
preview = true
|
|
86
|
+
|
|
87
|
+
[tool.ruff.lint]
|
|
88
|
+
select = ["ALL"]
|
|
89
|
+
|
|
90
|
+
[tool.ruff.lint.per-file-ignores]
|
|
91
|
+
"tests/**/*.py" = ["INP001", "S101"]
|
|
92
|
+
|
|
93
|
+
[tool.ruff.lint.pycodestyle]
|
|
94
|
+
max-doc-length = 72
|
|
95
|
+
|
|
96
|
+
[tool.tombi.files]
|
|
97
|
+
exclude = [".venv/**/*.toml"]
|
|
98
|
+
|
|
99
|
+
[tool.tombi.lint.rules]
|
|
100
|
+
tables-out-of-order = "off"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Copyright 2026 hingebase
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied. See the License for the specific language governing
|
|
13
|
+
# permissions and limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Gandharva: All-purpose data science application builder."""
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"ApplicationBuilderError",
|
|
19
|
+
"ApplicationRegisterError",
|
|
20
|
+
"Error",
|
|
21
|
+
"Field",
|
|
22
|
+
"Gandharva",
|
|
23
|
+
"run",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
from pydantic import Field
|
|
27
|
+
|
|
28
|
+
from . import typing as typing
|
|
29
|
+
from ._app import Gandharva
|
|
30
|
+
from ._runner import run
|
|
31
|
+
from .exceptions import (
|
|
32
|
+
ApplicationBuilderError,
|
|
33
|
+
ApplicationRegisterError,
|
|
34
|
+
Error,
|
|
35
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Copyright 2026 hingebase
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied. See the License for the specific language governing
|
|
13
|
+
# permissions and limitations under the License.
|
|
14
|
+
|
|
15
|
+
__all__ = ["Gandharva"]
|
|
16
|
+
|
|
17
|
+
from ._gandharva import Gandharva
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Copyright 2026 hingebase
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied. See the License for the specific language governing
|
|
13
|
+
# permissions and limitations under the License.
|
|
14
|
+
|
|
15
|
+
__all__ = ["App", "normalize"]
|
|
16
|
+
|
|
17
|
+
import abc
|
|
18
|
+
import inspect
|
|
19
|
+
import os
|
|
20
|
+
from typing import TYPE_CHECKING, ClassVar, Literal
|
|
21
|
+
|
|
22
|
+
import packaging.utils
|
|
23
|
+
import platformdirs
|
|
24
|
+
import pydantic.alias_generators
|
|
25
|
+
from typing_extensions import final
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
import gandharva as gd
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class App(abc.ABC):
|
|
32
|
+
children: ClassVar["list[type[gd.Gandharva]]"]
|
|
33
|
+
run_mode: Literal["api", "cli", "gui"]
|
|
34
|
+
|
|
35
|
+
@abc.abstractmethod
|
|
36
|
+
def main(self) -> object:
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def app_description(cls) -> str:
|
|
41
|
+
if doc := cls.__doc__:
|
|
42
|
+
return inspect.cleandoc(doc)
|
|
43
|
+
# Unlike `inspect.getdoc`, we are not interested in the base
|
|
44
|
+
# classes
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def app_normalized_name(cls) -> str:
|
|
49
|
+
return normalize(cls.__name__)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def app_summary(cls) -> str:
|
|
53
|
+
if lines := cls.app_description().splitlines():
|
|
54
|
+
return lines[0].rstrip(".")
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
@final
|
|
58
|
+
def __init__(self, run_mode: Literal["api", "cli", "gui"]) -> None:
|
|
59
|
+
self.run_mode = run_mode
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def normalize(name: str) -> str:
|
|
63
|
+
return packaging.utils.canonicalize_name(
|
|
64
|
+
pydantic.alias_generators.to_snake(name).removeprefix("_"),
|
|
65
|
+
validate=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
os.environ.setdefault(
|
|
70
|
+
"HYPOTHESIS_STORAGE_DIRECTORY",
|
|
71
|
+
platformdirs.user_cache_dir("gandharva", appauthor=False),
|
|
72
|
+
)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Copyright 2026 hingebase
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied. See the License for the specific language governing
|
|
13
|
+
# permissions and limitations under the License.
|
|
14
|
+
|
|
15
|
+
__all__ = ["App"]
|
|
16
|
+
|
|
17
|
+
import importlib.metadata
|
|
18
|
+
import inspect
|
|
19
|
+
from email.message import Message
|
|
20
|
+
from typing import Annotated, Literal, no_type_check
|
|
21
|
+
|
|
22
|
+
import fastapi
|
|
23
|
+
from typing_extensions import Any, Self, final, overload
|
|
24
|
+
|
|
25
|
+
import gandharva as gd
|
|
26
|
+
from gandharva import _convert
|
|
27
|
+
|
|
28
|
+
from . import _pydantic
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class App(_pydantic.App):
|
|
32
|
+
fastapi_request: fastapi.Request
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def fastapi_apiroute_params(cls) -> gd.typing.APIRouteParameters:
|
|
36
|
+
return {
|
|
37
|
+
"summary": cls.app_summary(),
|
|
38
|
+
"description": cls.app_description(),
|
|
39
|
+
"deprecated": hasattr(cls, "__deprecated__"),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def fastapi_apirouter_params(cls) -> gd.typing.APIRouterParameters:
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def fastapi_app_params(cls, meta: Message) -> gd.typing.FastAPIParameters:
|
|
48
|
+
kwargs: gd.typing.FastAPIParameters = {
|
|
49
|
+
"summary": meta.get("Summary"),
|
|
50
|
+
}
|
|
51
|
+
for key in "description", "version":
|
|
52
|
+
if value := meta.get(key):
|
|
53
|
+
kwargs[key] = value
|
|
54
|
+
if name := meta.get("Name"):
|
|
55
|
+
kwargs["title"] = name.replace("-", " ").title()
|
|
56
|
+
if identifier := meta.get("License-Expression"):
|
|
57
|
+
kwargs["license_info"] = {
|
|
58
|
+
"name": "License",
|
|
59
|
+
"identifier": identifier,
|
|
60
|
+
}
|
|
61
|
+
return kwargs
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def fastapi_post_init(
|
|
65
|
+
cls,
|
|
66
|
+
router: fastapi.APIRouter | fastapi.FastAPI,
|
|
67
|
+
) -> None:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
@final
|
|
72
|
+
def to_router(cls) -> fastapi.APIRouter:
|
|
73
|
+
kwargs: dict[str, Any] = dict(
|
|
74
|
+
cls.fastapi_apirouter_params(),
|
|
75
|
+
prefix="/" + cls.app_normalized_name(),
|
|
76
|
+
)
|
|
77
|
+
router = fastapi.APIRouter(**kwargs)
|
|
78
|
+
cls._fastapi_routes(router)
|
|
79
|
+
return router
|
|
80
|
+
|
|
81
|
+
@overload
|
|
82
|
+
def __new__(cls, run_mode: None = ...) -> fastapi.FastAPI: ...
|
|
83
|
+
@overload
|
|
84
|
+
def __new__(cls, run_mode: Literal["api", "cli", "gui"]) -> Self: ...
|
|
85
|
+
@final
|
|
86
|
+
def __new__( # pyright: ignore[reportInconsistentConstructor]
|
|
87
|
+
cls,
|
|
88
|
+
run_mode: Literal["api", "cli", "gui"] | None = None,
|
|
89
|
+
) -> fastapi.FastAPI | Self:
|
|
90
|
+
if run_mode:
|
|
91
|
+
return super().__new__(cls)
|
|
92
|
+
distribution_name = cls.__module__.split(".", 1)[0]
|
|
93
|
+
try:
|
|
94
|
+
meta = importlib.metadata.metadata(distribution_name)
|
|
95
|
+
except ModuleNotFoundError:
|
|
96
|
+
message = Message()
|
|
97
|
+
else:
|
|
98
|
+
message = meta if isinstance(meta, Message) else Message()
|
|
99
|
+
app = fastapi.FastAPI(**cls.fastapi_app_params(message))
|
|
100
|
+
cls._fastapi_routes(app)
|
|
101
|
+
return app
|
|
102
|
+
|
|
103
|
+
def _fastapi_main(self, request: fastapi.Request) -> object:
|
|
104
|
+
self.fastapi_request = request
|
|
105
|
+
try:
|
|
106
|
+
result = self.main()
|
|
107
|
+
result = _convert.to_response(result)
|
|
108
|
+
except Exception as e: # noqa: BLE001
|
|
109
|
+
return {"code": 1, "message": str(e), "data": None}
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def _fastapi_routes(
|
|
114
|
+
cls,
|
|
115
|
+
router: fastapi.APIRouter | fastapi.FastAPI,
|
|
116
|
+
) -> None:
|
|
117
|
+
responses: dict[int | str, dict[str, Any]] = {}
|
|
118
|
+
kwargs: dict[str, Any] = dict(
|
|
119
|
+
cls.fastapi_apiroute_params(),
|
|
120
|
+
path="/",
|
|
121
|
+
responses=responses,
|
|
122
|
+
name=cls.__name__,
|
|
123
|
+
)
|
|
124
|
+
_convert.to_response_model(
|
|
125
|
+
f"__Response_{cls.__name__}",
|
|
126
|
+
inspect.signature(cls.main, eval_str=True).return_annotation,
|
|
127
|
+
kwargs,
|
|
128
|
+
)
|
|
129
|
+
model = cls.to_pydantic()
|
|
130
|
+
if model.__pydantic_fields__:
|
|
131
|
+
@no_type_check
|
|
132
|
+
@router.post(**kwargs)
|
|
133
|
+
def _(
|
|
134
|
+
request: fastapi.Request,
|
|
135
|
+
body: Annotated[model, fastapi.Body()],
|
|
136
|
+
) -> object:
|
|
137
|
+
self = cls.from_pydantic(body, run_mode="api")
|
|
138
|
+
return self._fastapi_main(request)
|
|
139
|
+
else:
|
|
140
|
+
@router.get(**kwargs)
|
|
141
|
+
def _(request: fastapi.Request) -> object:
|
|
142
|
+
self = cls(run_mode="api")
|
|
143
|
+
return self._fastapi_main(request)
|
|
144
|
+
|
|
145
|
+
for child in cls.children:
|
|
146
|
+
router.include_router(child.to_router())
|
|
147
|
+
cls.fastapi_post_init(router)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Copyright 2026 hingebase
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
12
|
+
# implied. See the License for the specific language governing
|
|
13
|
+
# permissions and limitations under the License.
|
|
14
|
+
|
|
15
|
+
__all__ = ["Gandharva"]
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import functools
|
|
19
|
+
import inspect
|
|
20
|
+
from collections.abc import Callable, Coroutine
|
|
21
|
+
|
|
22
|
+
import anyio.from_thread
|
|
23
|
+
from typing_extensions import Any, ParamSpec, TypeVar, disjoint_base
|
|
24
|
+
|
|
25
|
+
import gandharva as gd
|
|
26
|
+
|
|
27
|
+
from . import _fastapi, _panel
|
|
28
|
+
|
|
29
|
+
_GandharvaT = TypeVar("_GandharvaT", bound=type["Gandharva"])
|
|
30
|
+
_P = ParamSpec("_P")
|
|
31
|
+
_T = TypeVar("_T")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@disjoint_base
|
|
35
|
+
class Gandharva(_fastapi.App, _panel.App):
|
|
36
|
+
@classmethod
|
|
37
|
+
def register(cls, child: _GandharvaT) -> _GandharvaT:
|
|
38
|
+
if inspect.isabstract(cls) or inspect.isabstract(child):
|
|
39
|
+
message = (
|
|
40
|
+
"Can't register sub-application if either the parent or the "
|
|
41
|
+
"child is an abstract class. Please implement the main() "
|
|
42
|
+
"method."
|
|
43
|
+
)
|
|
44
|
+
raise gd.ApplicationRegisterError(message) from None
|
|
45
|
+
if cls is child or cls._registered(child):
|
|
46
|
+
message = (
|
|
47
|
+
"Can't register sub-application if a cycle would be formed "
|
|
48
|
+
"in the application structure graph"
|
|
49
|
+
)
|
|
50
|
+
raise gd.ApplicationRegisterError(message)
|
|
51
|
+
cls.children.append(child)
|
|
52
|
+
return child
|
|
53
|
+
|
|
54
|
+
def syncify(
|
|
55
|
+
self,
|
|
56
|
+
func: Callable[_P, Coroutine[Any, Any, _T]],
|
|
57
|
+
) -> Callable[_P, _T]:
|
|
58
|
+
match self.run_mode:
|
|
59
|
+
case "api":
|
|
60
|
+
@functools.wraps(func)
|
|
61
|
+
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
|
62
|
+
wrapped = functools.partial(func, *args, **kwargs)
|
|
63
|
+
return anyio.from_thread.run(wrapped)
|
|
64
|
+
case "cli":
|
|
65
|
+
@functools.wraps(func)
|
|
66
|
+
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
|
67
|
+
coro = func(*args, **kwargs)
|
|
68
|
+
return asyncio.run(coro)
|
|
69
|
+
case "gui":
|
|
70
|
+
@functools.wraps(func)
|
|
71
|
+
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
|
72
|
+
coro = func(*args, **kwargs)
|
|
73
|
+
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
74
|
+
return fut.result()
|
|
75
|
+
|
|
76
|
+
loop = self.panel_event_loop
|
|
77
|
+
return wrapper
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def _registered(cls, parent: type["Gandharva"]) -> bool:
|
|
81
|
+
children = parent.children
|
|
82
|
+
return cls in children or any(map(cls._registered, children))
|