aresource 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,60 @@
1
+ Metadata-Version: 2.3
2
+ Name: aresource
3
+ Version: 0.0.1
4
+ Summary:
5
+ Author: Peter Babka
6
+ Author-email: 159peter951@gmail.com
7
+ Requires-Python: >=3.13, <4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Provides-Extra: aiohttp
11
+ Provides-Extra: hocon
12
+ Requires-Dist: aiohttp (>=3.0.0,<4.0.0) ; extra == "aiohttp"
13
+ Requires-Dist: pyhocon (>=0.3.0,<0.4.0) ; extra == "hocon"
14
+ Project-URL: Repository, https://github.com/xbabka01/aresource
15
+ Description-Content-Type: text/markdown
16
+
17
+ # aresource
18
+
19
+ A Python project for resource management.
20
+
21
+ ## Project Structure
22
+
23
+ ```
24
+ src/
25
+ aresource/
26
+ tests/
27
+ ```
28
+
29
+ ## Installation
30
+
31
+ Use [Poetry](https://python-poetry.org/) to install dependencies:
32
+
33
+ ```sh
34
+ poetry install
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Import and use the package in your Python code:
40
+
41
+ ```python
42
+ from aresource import manager
43
+ ```
44
+
45
+ ## Testing
46
+
47
+ Run tests with:
48
+
49
+ ```sh
50
+ poetry run pytest
51
+ ```
52
+
53
+ ## Code Quality
54
+
55
+ - Type checking: `poetry run mypy src/`
56
+ - Linting: `poetry run ruff src/`
57
+
58
+ ## License
59
+
60
+ MIT License
@@ -0,0 +1,44 @@
1
+ # aresource
2
+
3
+ A Python project for resource management.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ src/
9
+ aresource/
10
+ tests/
11
+ ```
12
+
13
+ ## Installation
14
+
15
+ Use [Poetry](https://python-poetry.org/) to install dependencies:
16
+
17
+ ```sh
18
+ poetry install
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Import and use the package in your Python code:
24
+
25
+ ```python
26
+ from aresource import manager
27
+ ```
28
+
29
+ ## Testing
30
+
31
+ Run tests with:
32
+
33
+ ```sh
34
+ poetry run pytest
35
+ ```
36
+
37
+ ## Code Quality
38
+
39
+ - Type checking: `poetry run mypy src/`
40
+ - Linting: `poetry run ruff src/`
41
+
42
+ ## License
43
+
44
+ MIT License
@@ -0,0 +1,66 @@
1
+ [project]
2
+ name = "aresource"
3
+ version = "0.0.1"
4
+ description = ""
5
+ authors = [
6
+ {name = "Peter Babka",email = "159peter951@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.13, <4.0"
10
+ dependencies = [
11
+ ]
12
+
13
+ [project.urls]
14
+ repository = "https://github.com/xbabka01/aresource"
15
+
16
+ [tool.poetry]
17
+ packages = [{include = "aresource", from = "src"}]
18
+
19
+ [tool.poetry.dependencies]
20
+ aiohttp = { version = "^3.0.0", optional = true }
21
+ pyhocon = { version = "^0.3.0", optional = true }
22
+
23
+
24
+ [tool.poetry.extras]
25
+ aiohttp = ["aiohttp"]
26
+ hocon = ["pyhocon"]
27
+
28
+ [tool.poetry.group.dev.dependencies]
29
+ pytest = "^8.4.1"
30
+ pytest-asyncio = "^1.0.0"
31
+ pytest-cov = "^6.2.1"
32
+ pytest-ruff = "^0.5"
33
+ pytest-mypy = "^1.0.1"
34
+ aiohttp = "^3.12.14"
35
+ pyhocon = "^0.3.61"
36
+
37
+ [tool.pytest.ini_options]
38
+ addopts = "--strict-markers --tb=short --mypy --cov=aresource --cov-report=html --cov-report=term --ruff --ruff-format"
39
+ asyncio_mode = "auto"
40
+ testpaths = ["tests", "src"]
41
+
42
+ [tool.ruff]
43
+ line-length = 99
44
+
45
+ [tool.ruff.lint]
46
+ select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "T20", "SIM", "Q", "RUF"]
47
+ ignore = []
48
+
49
+ [tool.ruff.format]
50
+ docstring-code-format = true
51
+
52
+ [tool.mypy]
53
+ python_version = "3.13"
54
+ files = ["src/aresource", "tests"]
55
+ strict = true
56
+ warn_redundant_casts = true
57
+ warn_unused_ignores = true
58
+ warn_unreachable = true
59
+
60
+ [[tool.mypy.overrides]]
61
+ module = "pyhocon"
62
+ ignore_missing_imports = true
63
+
64
+ [build-system]
65
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
66
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,3 @@
1
+ from aresource.manager import BaseResource, ResourceManager
2
+
3
+ __all__ = ["BaseResource", "ResourceManager"]
@@ -0,0 +1,102 @@
1
+ import contextlib
2
+ import copy
3
+ from abc import ABC, abstractmethod
4
+ from ast import TypeVar
5
+ from collections.abc import AsyncIterator
6
+ from typing import Any, ClassVar, Self
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class BaseResource[T](ABC):
12
+ name: str | None = None
13
+
14
+ def __get__(self, instance: "ResourceManager", owner: "type[ResourceManager]") -> T:
15
+ return instance.get_resource(self.name) # type: ignore[no-any-return]
16
+
17
+ def __set_name__(self, owner: "type[ResourceManager]", name: str) -> None:
18
+ owner.register_resource(name, self)
19
+
20
+ @abstractmethod
21
+ @contextlib.asynccontextmanager
22
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[T]:
23
+ """Acquire the resource asynchronously."""
24
+ yield None # type: ignore[misc]
25
+
26
+
27
+ class ResourceManager:
28
+ _cls: Any = None
29
+ _resources: ClassVar[dict[str, tuple[BaseResource[Any], Any]]] = {}
30
+
31
+ def __init__(self) -> None:
32
+ self._exitstack: contextlib.AsyncExitStack | None = None
33
+
34
+ @classmethod
35
+ def register_resource(cls, name: str, resource: BaseResource[Any]) -> None:
36
+ """Register a resource with the manager."""
37
+ if not isinstance(resource, BaseResource):
38
+ raise TypeError(f"{resource.__name__} is not a subclass of BaseResource")
39
+ if name in cls._resources:
40
+ raise AttributeError(f"Resource {name} is already registered in {cls.__name__}")
41
+ # Create a copy of the class resources to avoid modifying the superclass
42
+ if cls._cls is not cls:
43
+ cls._resources = copy.deepcopy(cls._resources)
44
+ cls._cls = cls
45
+
46
+ cls._resources[name] = (resource, ...)
47
+ resource.name = name
48
+
49
+ def get_resource(self, name: str | None) -> Any:
50
+ """
51
+ Get the value of a registered resource by name.
52
+ """
53
+ if name not in self._resources:
54
+ raise AttributeError(f"Resource {name} is not registered in {self.__class__.__name__}")
55
+ value = self._resources[name][1]
56
+ if value is Ellipsis:
57
+ raise AttributeError(
58
+ f"Resource {name} is not initialized in {self.__class__.__name__}"
59
+ )
60
+ return value
61
+
62
+ def set_resource(self, name: str, value: Any) -> None:
63
+ """
64
+ Set the value of a registered resource by name.
65
+ Raises AttributeError if the resource is not registered.
66
+ """
67
+ if name not in self._resources:
68
+ raise AttributeError(f"Resource {name} is not registered in {self.__class__.__name__}")
69
+
70
+ resource = self._resources[name][0]
71
+ self._resources[name] = (resource, value)
72
+
73
+ async def setup(self) -> None:
74
+ await self.__aenter__()
75
+
76
+ async def __aenter__(self) -> Self:
77
+ """Asynchronously enter the resource manager context, acquiring all resources.
78
+ Each resource is acquired using its async context manager and set in the manager.
79
+ Returns self.
80
+ """
81
+ if self._exitstack is not None:
82
+ raise RuntimeError("ResourceManager is already set up")
83
+
84
+ self._exitstack = contextlib.AsyncExitStack()
85
+ for name, (resource, _) in self._resources.items():
86
+ value = await self._exitstack.enter_async_context(resource.acquire(self))
87
+ self.set_resource(name, value)
88
+ return self
89
+
90
+ async def aclose(self) -> None:
91
+ """Asynchronously close the resource manager, releasing all resources."""
92
+ await self.__aexit__()
93
+
94
+ async def __aexit__(self, *exc: Any) -> bool | None:
95
+ """Asynchronously exit the resource manager context, releasing all resources.
96
+ Delegates to the AsyncExitStack's __aexit__ method.
97
+ Returns the result of the exit stack's __aexit__.
98
+ """
99
+ if self._exitstack is not None:
100
+ await self._exitstack.__aexit__(*exc)
101
+ self._exitstack = None
102
+ return False # Do not suppress exceptions
File without changes
@@ -0,0 +1,28 @@
1
+ import contextlib
2
+ from collections.abc import AsyncIterator, Callable
3
+ from typing import Any
4
+
5
+ from aiohttp import ClientSession
6
+
7
+ from aresource.manager import BaseResource, ResourceManager
8
+
9
+
10
+ class ClientSessionResource(BaseResource[ClientSession]):
11
+ """
12
+ Resource that provides an aiohttp ClientSession.
13
+ """
14
+
15
+ def __init__(
16
+ self, callback: Callable[[ResourceManager], dict[str, Any]] | None = None
17
+ ) -> None:
18
+ super().__init__()
19
+ self.callback = callback
20
+
21
+ @contextlib.asynccontextmanager
22
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[ClientSession]:
23
+ kwargs = self.callback(manager) if self.callback else {}
24
+ session = ClientSession(**kwargs)
25
+ try:
26
+ yield session
27
+ finally:
28
+ await session.close() # Ensure the session is closed after use
@@ -0,0 +1,42 @@
1
+ import contextlib
2
+ from collections.abc import AsyncIterator, Callable
3
+ from typing import Any
4
+
5
+ from aiohttp import web
6
+
7
+ from aresource.manager import BaseResource, ResourceManager
8
+
9
+
10
+ class WebAppResource(BaseResource[web.Application]):
11
+ """
12
+ Resource that provides an aiohttp ClientSession.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ routes: Callable[[ResourceManager], web.RouteTableDef],
18
+ callback: Callable[[ResourceManager], dict[str, Any]] | None = None,
19
+ ) -> None:
20
+ super().__init__()
21
+ self.callback = callback
22
+ self.routes = routes
23
+
24
+ @contextlib.asynccontextmanager
25
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[web.Application]:
26
+ kwargs = self.callback(manager) if self.callback else {}
27
+ app = web.Application(**kwargs)
28
+
29
+ routes = self.routes(manager)
30
+ app.add_routes(routes)
31
+
32
+ runner = web.AppRunner(app)
33
+
34
+ await runner.setup()
35
+ site = web.TCPSite(runner, "localhost", 8081)
36
+
37
+ try:
38
+ await site.start()
39
+ yield app
40
+ finally:
41
+ await site.stop()
42
+ await app.shutdown()
@@ -0,0 +1,21 @@
1
+ from .base import BasePathResource
2
+ from .bytes import BytesResource
3
+ from .ini import IniResource
4
+ from .json import JsonResource
5
+ from .path import PathResource
6
+
7
+ __all__ = [
8
+ "BasePathResource",
9
+ "BytesResource",
10
+ "HoconResource",
11
+ "IniResource",
12
+ "JsonResource",
13
+ "PathResource",
14
+ ]
15
+
16
+ try:
17
+ from .hocon import HoconResource
18
+ except ImportError:
19
+ pass
20
+ else:
21
+ __all__.append("HoconResource")
@@ -0,0 +1,36 @@
1
+ import contextlib
2
+ from abc import ABCMeta
3
+ from collections.abc import Iterator
4
+ from importlib.resources import as_file, files
5
+ from importlib.resources.abc import Traversable
6
+ from pathlib import Path
7
+ from typing import TypeVar
8
+
9
+ from aresource.manager import BaseResource
10
+
11
+ T = TypeVar("T")
12
+
13
+
14
+ class BasePathResource[T](BaseResource[T], metaclass=ABCMeta):
15
+ def __init__(self, package: str, path: str) -> None:
16
+ super().__init__()
17
+ self.package = package
18
+ self.path = path
19
+
20
+ @property
21
+ def resource(self) -> Traversable:
22
+ """
23
+ Get the path to the resource in the package.
24
+ """
25
+ return files(self.package).joinpath(self.path)
26
+
27
+ @contextlib.contextmanager
28
+ def as_file(self) -> Iterator[Path]:
29
+ """
30
+ Context manager to yield the path to the resource as a file.
31
+ """
32
+ with as_file(self.resource) as resource_path:
33
+ yield resource_path
34
+
35
+ def read(self) -> bytes:
36
+ return self.resource.read_bytes()
@@ -0,0 +1,15 @@
1
+ import contextlib
2
+ from collections.abc import AsyncIterator
3
+
4
+ from aresource.manager import ResourceManager
5
+ from aresource.resources.files.base import BasePathResource
6
+
7
+
8
+ class BytesResource(BasePathResource[bytes]):
9
+ """
10
+ Resource that provides a binary file in a package.
11
+ """
12
+
13
+ @contextlib.asynccontextmanager
14
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[bytes]:
15
+ yield self.read()
@@ -0,0 +1,17 @@
1
+ import contextlib
2
+ from collections.abc import AsyncIterator
3
+
4
+ from pyhocon import ConfigFactory, ConfigTree
5
+
6
+ from aresource.manager import ResourceManager
7
+ from aresource.resources.files.base import BasePathResource
8
+
9
+
10
+ class HoconResource(BasePathResource[ConfigTree]):
11
+ """
12
+ Resource that provides a JSON file in a package.
13
+ """
14
+
15
+ @contextlib.asynccontextmanager
16
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[ConfigTree]:
17
+ yield ConfigFactory.parse_string(self.read().decode("utf-8"))
@@ -0,0 +1,19 @@
1
+ import configparser
2
+ import contextlib
3
+ from collections.abc import AsyncIterator
4
+ from configparser import ConfigParser
5
+
6
+ from aresource.manager import ResourceManager
7
+ from aresource.resources.files.base import BasePathResource
8
+
9
+
10
+ class IniResource(BasePathResource[ConfigParser]):
11
+ """
12
+ Resource that provides a JSON file in a package.
13
+ """
14
+
15
+ @contextlib.asynccontextmanager
16
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[ConfigParser]:
17
+ config = configparser.ConfigParser()
18
+ config.read_string(self.read().decode("utf-8"))
19
+ yield config
@@ -0,0 +1,20 @@
1
+ import contextlib
2
+ import json
3
+ from collections.abc import AsyncIterator
4
+ from typing import Any
5
+
6
+ from aresource.manager import ResourceManager
7
+ from aresource.resources.files.base import BasePathResource
8
+
9
+
10
+ class JsonResource(BasePathResource[dict[str, Any] | list[Any]]):
11
+ """
12
+ Resource that provides a JSON file in a package.
13
+ """
14
+
15
+ @contextlib.asynccontextmanager
16
+ async def acquire(
17
+ self, manager: "ResourceManager"
18
+ ) -> AsyncIterator[dict[str, Any] | list[Any]]:
19
+ data = self.read()
20
+ yield json.loads(data)
@@ -0,0 +1,18 @@
1
+ import contextlib
2
+ from collections.abc import AsyncIterator
3
+ from importlib.resources import as_file
4
+ from pathlib import Path
5
+
6
+ from aresource.manager import ResourceManager
7
+ from aresource.resources.files.base import BasePathResource
8
+
9
+
10
+ class PathResource(BasePathResource[Path]):
11
+ """
12
+ Resource that provides a path to a file in a package.
13
+ """
14
+
15
+ @contextlib.asynccontextmanager
16
+ async def acquire(self, manager: "ResourceManager") -> AsyncIterator[Path]:
17
+ with as_file(self.resource) as resource_path:
18
+ yield resource_path