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.
@@ -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,4 @@
1
+ # Gandharva
2
+ In active development. Check the [issue tracker][1] for upcoming features.
3
+
4
+ [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))