arcade-core 2.0.0__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.
arcade_core/toolkit.py ADDED
@@ -0,0 +1,155 @@
1
+ import importlib.metadata
2
+ import importlib.util
3
+ import logging
4
+ import os
5
+ import types
6
+ from collections import defaultdict
7
+ from pathlib import Path
8
+
9
+ from pydantic import BaseModel, ConfigDict, field_validator
10
+
11
+ from arcade_core.errors import ToolkitLoadError
12
+ from arcade_core.parse import get_tools_from_file
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class Toolkit(BaseModel):
18
+ model_config = ConfigDict(populate_by_name=True)
19
+
20
+ name: str
21
+ """Name of the toolkit"""
22
+
23
+ package_name: str
24
+ """Name of the package holding the toolkit"""
25
+
26
+ tools: dict[str, list[str]] = defaultdict(list)
27
+ """Mapping of module names to tools"""
28
+
29
+ # Other python package metadata
30
+ version: str
31
+ description: str
32
+ author: list[str] = []
33
+ repository: str | None = None
34
+ homepage: str | None = None
35
+
36
+ @field_validator("name", mode="before")
37
+ def strip_arcade_prefix(cls, value: str) -> str:
38
+ """
39
+ Validator to strip the 'arcade_' prefix from the name if it exists.
40
+ """
41
+ if value.startswith("arcade_"):
42
+ return value[len("arcade_") :]
43
+ return value
44
+
45
+ @classmethod
46
+ def from_module(cls, module: types.ModuleType) -> "Toolkit":
47
+ """
48
+ Load a toolkit from an imported python module
49
+
50
+ >>> import arcade_math
51
+ >>> toolkit = Toolkit.from_module(arcade_math)
52
+ """
53
+ return cls.from_package(module.__name__)
54
+
55
+ @classmethod
56
+ def from_package(cls, package: str) -> "Toolkit":
57
+ """
58
+ Load a Toolkit from a Python package
59
+ """
60
+ try:
61
+ metadata = importlib.metadata.metadata(package)
62
+ name = metadata["Name"]
63
+ package_name = package
64
+ version = metadata["Version"]
65
+ description = metadata.get("Summary", "") # type: ignore[attr-defined]
66
+ author = metadata.get_all("Author-email")
67
+ homepage = metadata.get("Home-page", None) # type: ignore[attr-defined]
68
+ repo = metadata.get("Repository", None) # type: ignore[attr-defined]
69
+
70
+ except importlib.metadata.PackageNotFoundError as e:
71
+ raise ToolkitLoadError(f"Package {package} not found.") from e
72
+ except KeyError as e:
73
+ raise ToolkitLoadError(f"Metadata key error for package {package}.") from e
74
+ except Exception as e:
75
+ raise ToolkitLoadError(f"Failed to load metadata for package {package}.") from e
76
+
77
+ # Get the package directory
78
+ try:
79
+ package_dir = Path(get_package_directory(package))
80
+ except (ImportError, AttributeError) as e:
81
+ raise ToolkitLoadError(f"Failed to locate package directory for {package}.") from e
82
+
83
+ # Get all python files in the package directory
84
+ try:
85
+ modules = [f for f in package_dir.glob("**/*.py") if f.is_file()]
86
+ except OSError as e:
87
+ raise ToolkitLoadError(
88
+ f"Failed to locate Python files in package directory for {package}."
89
+ ) from e
90
+
91
+ toolkit = cls(
92
+ name=name,
93
+ package_name=package_name,
94
+ version=version,
95
+ description=description,
96
+ author=author if author else [],
97
+ homepage=homepage,
98
+ repository=repo,
99
+ )
100
+
101
+ for module_path in modules:
102
+ relative_path = module_path.relative_to(package_dir)
103
+ import_path = ".".join(relative_path.with_suffix("").parts)
104
+ import_path = f"{package_name}.{import_path}"
105
+ toolkit.tools[import_path] = get_tools_from_file(str(module_path))
106
+
107
+ if not toolkit.tools:
108
+ raise ToolkitLoadError(f"No tools found in package {package}")
109
+
110
+ return toolkit
111
+
112
+ @classmethod
113
+ def find_all_arcade_toolkits(cls) -> list["Toolkit"]:
114
+ """
115
+ Find all installed packages prefixed with 'arcade_' in the current
116
+ Python interpreter's environment and load them as Toolkits.
117
+
118
+ Returns:
119
+ List[Toolkit]: A list of Toolkit instances.
120
+ """
121
+ import sysconfig
122
+
123
+ # Get the site-packages directory of the current interpreter
124
+ site_packages_dir = sysconfig.get_paths()["purelib"]
125
+ arcade_packages = [
126
+ dist.metadata["Name"]
127
+ for dist in importlib.metadata.distributions(path=[site_packages_dir])
128
+ if dist.metadata["Name"].startswith("arcade_")
129
+ ]
130
+ toolkits = []
131
+ for package in arcade_packages:
132
+ try:
133
+ toolkits.append(cls.from_package(package))
134
+ except ToolkitLoadError as e:
135
+ logger.warning(f"Warning: {e} Skipping toolkit {package}")
136
+ return toolkits
137
+
138
+
139
+ def get_package_directory(package_name: str) -> str:
140
+ """
141
+ Get the directory of a Python package
142
+ """
143
+
144
+ spec = importlib.util.find_spec(package_name)
145
+ if spec is None:
146
+ raise ImportError(f"Cannot find package named {package_name}")
147
+
148
+ if spec.origin:
149
+ # If the package has an origin, return the directory of the origin
150
+ return os.path.dirname(spec.origin)
151
+ elif spec.submodule_search_locations:
152
+ # If the package is a namespace package, return the first search location
153
+ return spec.submodule_search_locations[0]
154
+ else:
155
+ raise ImportError(f"Package {package_name} does not have a file path associated with it")
arcade_core/utils.py ADDED
@@ -0,0 +1,99 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import inspect
5
+ import re
6
+ from collections.abc import Iterable
7
+ from types import UnionType
8
+ from typing import Any, Callable, Literal, TypeVar, Union, get_args, get_origin
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ def first_or_none(_type: type[T], iterable: Iterable[Any]) -> T | None:
14
+ """
15
+ Returns the first item in the iterable that is an instance of the given type, or None if no such item is found.
16
+ """
17
+ for item in iterable:
18
+ if isinstance(item, _type):
19
+ return item
20
+ return None
21
+
22
+
23
+ def pascal_to_snake_case(name: str) -> str:
24
+ """
25
+ Converts a PascalCase name to snake_case.
26
+ """
27
+ name = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
28
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", name).lower()
29
+
30
+
31
+ def snake_to_pascal_case(name: str) -> str:
32
+ """
33
+ Converts a snake_case name to PascalCase.
34
+ """
35
+ if "_" in name:
36
+ return "".join(x.capitalize() or "_" for x in name.split("_"))
37
+ # check if first letter is uppercase
38
+ if name[0].isupper():
39
+ return name
40
+ return name.capitalize()
41
+
42
+
43
+ def is_string_literal(_type: type) -> bool:
44
+ """
45
+ Returns True if the given type is a string literal, i.e. a Literal[str] or Literal[str, str, ...] etc.
46
+ """
47
+ return get_origin(_type) is Literal and all(isinstance(arg, str) for arg in get_args(_type))
48
+
49
+
50
+ def is_union(_type: type) -> bool:
51
+ """
52
+ Returns True if the given type is a union, i.e. a Union[T1, T2, ...] or T1 | T2 | ... etc.
53
+ """
54
+ return get_origin(_type) in {Union, UnionType}
55
+
56
+
57
+ def is_strict_optional(_type: type) -> bool:
58
+ """
59
+ Returns True if the given type is a strict optional type, i.e. a union with exactly two types
60
+ where one type is None. This covers Optional[T], Union[T, None] and T | None
61
+ """
62
+ return is_union(_type) and len(get_args(_type)) == 2 and type(None) in get_args(_type)
63
+
64
+
65
+ def does_function_return_value(func: Callable) -> bool:
66
+ """
67
+ Returns True if the given function returns a value, i.e. if it has a return statement with a value.
68
+ """
69
+ try:
70
+ source: str | None = inspect.getsource(func)
71
+ except OSError:
72
+ # Workaround for parameterized unit tests that use a dynamically-generated function
73
+ source = getattr(func, "__source__", None)
74
+
75
+ if source is None:
76
+ raise ValueError("Source code not found")
77
+
78
+ tree = ast.parse(source)
79
+
80
+ class ReturnVisitor(ast.NodeVisitor):
81
+ def __init__(self) -> None:
82
+ self.returns_value = False
83
+
84
+ def visit_Return(self, node: ast.Return) -> None:
85
+ if node.value is not None:
86
+ self.returns_value = True
87
+
88
+ visitor = ReturnVisitor()
89
+ visitor.visit(tree)
90
+ return visitor.returns_value
91
+
92
+
93
+ def coerce_empty_list_to_none(lst: list[Any] | None) -> list[Any] | None:
94
+ """
95
+ Coerces empty lists to None, otherwise returns the list unchanged.
96
+ """
97
+ if isinstance(lst, list) and len(lst) == 0:
98
+ return None
99
+ return lst
arcade_core/version.py ADDED
@@ -0,0 +1 @@
1
+ VERSION = "0.1.0"
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcade-core
3
+ Version: 2.0.0
4
+ Summary: Arcade Core - Core library for Arcade platform
5
+ Author-email: Arcade <dev@arcade.dev>
6
+ License: MIT
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: loguru>=0.7.0
17
+ Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.28.2
18
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http==1.28.2
19
+ Requires-Dist: opentelemetry-instrumentation-fastapi==0.49b2
20
+ Requires-Dist: packaging>=24.1
21
+ Requires-Dist: pydantic>=2.7.0
22
+ Requires-Dist: pyjwt>=2.8.0
23
+ Requires-Dist: pyyaml>=6.0
24
+ Requires-Dist: toml>=0.10.2
25
+ Requires-Dist: types-python-dateutil==2.9.0.20241003
26
+ Requires-Dist: types-pytz==2024.2.0.20241003
27
+ Requires-Dist: types-toml==0.10.8.20240310
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.5.1; extra == 'dev'
30
+ Requires-Dist: pre-commit>=3.4.0; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.23.7; extra == 'dev'
32
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
33
+ Requires-Dist: pytest>=8.1.2; extra == 'dev'
34
+ Requires-Dist: types-python-dateutil>=2.8.2; extra == 'dev'
35
+ Requires-Dist: types-pytz>=2024.1; extra == 'dev'
36
+ Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
37
+ Description-Content-Type: text/markdown
38
+
39
+ # Arcade Core
40
+
41
+ Core library for the Arcade platform providing foundational components and utilities.
42
+
43
+ ## Overview
44
+
45
+ Arcade Core provides the essential building blocks for the Arcade platform:
46
+
47
+ - **Tool Catalog & Toolkit Management**: Core classes for managing and organizing tools
48
+ - **Configuration & Schema Handling**: Configuration management and validation
49
+ - **Authentication & Authorization**: Auth providers and security utilities
50
+ - **Error Handling**: Comprehensive error types and handling
51
+ - **Telemetry & Observability**: Monitoring and tracing capabilities
52
+ - **Utilities**: Common helper functions and validators
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install arcade-core
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ```python
63
+ from arcade_core import ToolCatalog, Toolkit, ArcadeConfig
64
+
65
+ # Create a tool catalog
66
+ catalog = ToolCatalog()
67
+
68
+ # Load a toolkit
69
+ toolkit = Toolkit.from_directory("path/to/toolkit")
70
+
71
+ # Configure Arcade
72
+ config = ArcadeConfig.from_file("config.yaml")
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT License - see LICENSE file for details.
@@ -0,0 +1,19 @@
1
+ arcade_core/__init__.py,sha256=1heu3AROAjpistehPzY2H-2nkj_IjQEh-vVlVOCRF1E,88
2
+ arcade_core/annotations.py,sha256=Nst6aejLWXlpTu7GwzWETu1gQCG1XVAUR_qcFbNvyRc,198
3
+ arcade_core/auth.py,sha256=vlbI_QLiFcBXL4odt56CfL4kz0sOuk_MZujaK-cxN68,5347
4
+ arcade_core/catalog.py,sha256=MH-o-NMvUoK_hg8Ibr8dKoF4Yu0z53D5pfUR--BhPCA,31432
5
+ arcade_core/config.py,sha256=e98XQAkYySGW9T_yrJg54BB8Wuq06GPVHp7xqe2d1vU,572
6
+ arcade_core/config_model.py,sha256=GYO37yKi7ih6EYKPpX1Kl-K1XwM2JyEJguyaJ7j9TY8,4260
7
+ arcade_core/errors.py,sha256=h4H1ck4TP-CDKPuRA5EVtRnplWwk9ofwFWE5h4AuMyg,2043
8
+ arcade_core/executor.py,sha256=tEDAM-4d-LMZJc0xAemjoL73kKCyLxTa79NK6vjWqmw,4444
9
+ arcade_core/output.py,sha256=0v0Y47NVQ40-vl9a47XZT7T-cZqcrVZrvO3xYJlcZPs,1852
10
+ arcade_core/parse.py,sha256=SURNI-B9xHCIprxTRTAR0AMT9hIJpQqHjOmrENzFBVI,1899
11
+ arcade_core/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ arcade_core/schema.py,sha256=DgVqrRDZMhndhXcb-CvLEnAtVWkseisvtAwcei5Qgmc,14358
13
+ arcade_core/telemetry.py,sha256=qDv8T-wO8nFi0Qh93WKaPH1b6asfoJoyyfA7ZOxPnbA,5566
14
+ arcade_core/toolkit.py,sha256=ufxkyRN2Qmu-TW4GHYuXrsMav3lrTusUqYf_wL-WZdw,5349
15
+ arcade_core/utils.py,sha256=Gg4na-85pY21e5Ab-yxoRlzTQu3FhlP5xQ9G1BhfrI8,2980
16
+ arcade_core/version.py,sha256=CpXi3jGlx23RvRyU7iytOMZrnspdWw4yofS8lpP1AJU,18
17
+ arcade_core-2.0.0.dist-info/METADATA,sha256=g093vgoWqzSNgBs0uExT7dlassVKXEPXc39-9zB9PhA,2542
18
+ arcade_core-2.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
19
+ arcade_core-2.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any