{"id":28657754,"url":"https://github.com/coolsamson7/aspyx","last_synced_at":"2025-06-13T09:40:01.512Z","repository":{"id":297598022,"uuid":"997283842","full_name":"coolsamson7/aspyx","owner":"coolsamson7","description":"a python di and app library","archived":false,"fork":false,"pushed_at":"2025-06-06T10:25:58.000Z","size":16,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-06-06T10:37:48.274Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/coolsamson7.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-06-06T09:07:48.000Z","updated_at":"2025-06-06T10:26:01.000Z","dependencies_parsed_at":"2025-06-06T10:49:31.289Z","dependency_job_id":null,"html_url":"https://github.com/coolsamson7/aspyx","commit_stats":null,"previous_names":["coolsamson7/aspyx"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/coolsamson7/aspyx","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coolsamson7%2Faspyx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coolsamson7%2Faspyx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coolsamson7%2Faspyx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coolsamson7%2Faspyx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/coolsamson7","download_url":"https://codeload.github.com/coolsamson7/aspyx/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/coolsamson7%2Faspyx/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259607090,"owners_count":22883572,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-13T09:39:56.444Z","updated_at":"2025-06-13T09:40:01.500Z","avatar_url":"https://github.com/coolsamson7.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# aspyx\n\n![Pylint](https://github.com/coolsamson7/aspyx/actions/workflows/pylint.yml/badge.svg)\n![Build Status](https://github.com/coolsamson7/aspyx/actions/workflows/ci.yml/badge.svg)\n![Python Versions](https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11%20|%203.12-blue)\n![License](https://img.shields.io/github/license/coolsamson7/aspyx)\n![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)\n\n## Table of Contents\n\n- [Introduction](#aspyx)\n- [Installation](#installation)\n- [Registration](#registration)\n  - [Class](#class)\n  - [Class Factory](#class-factory)\n  - [Method](#method)\n- [Environment](#environment)\n  - [Definition](#definition)\n  - [Retrieval](#retrieval)\n- [Instantiation logic](#instantiation-logic)\n  - [Injection Methods](#injection-methods)\n  - [Lifecycle Methods](#lifecycle-methods)\n  - [Post Processors](#post-processors)\n- [Custom scopes](#custom-scopes)\n- [AOP](#aop)\n- [Configuration](#configuration)\n- [Reflection](#reflection)\n- [Version History](#version-history)\n\n# Introduction\n\nAspyx is a small python libary, that adds support for both dependency injection and aop.\n\nThe following features are supported \n- constructor and setter injection\n- post processors\n- factory classes and methods\n- support for eager construction\n- support for singleton and request scopes\n- possibilty to add custom scopes\n- lifecycle events methods\n- bundling of injectable object sets by environment classes including recursive imports and inheritance\n- container instances that relate to environment classes and manage the lifecylce of related objects\n- hierarchical environments\n\nThe library is thread-safe!\n\nLet's look at a simple example\n\n```python\nfrom aspyx.di import injectable, on_init, on_destroy, environment, Environment\n\n@injectable()\nclass Foo:\n    def __init__(self):\n        pass\n\n    def hello(msg: str):\n        print(f\"hello {msg}\")\n\n@injectable()  # eager and singleton by default\nclass Bar:\n    def __init__(self, foo: Foo): # will inject the Foo dependency\n        self.foo = foo\n\n    @on_init() # a lifecycle callback called after the constructor and all possible injections\n    def init(self):\n        ...\n\n\n# this class will register all - specifically decorated - classes and factories in the own module\n# In this case Foo and Bar\n\n@environment()\nclass SampleEnvironment:\n    def __init__(self):\n        pass\n\n# go, forrest\n\nenvironment = Environment(SampleEnvironment)\n\nbar = env.get(Bar)\n\nbar.foo.hello(\"world\")\n```\n\nThe concepts should be pretty familiar as well as the names which are a combination of Spring and Angular names :-)\n\nLet's add some aspects...\n\n```python\n\n@advice\nclass SampleAdvice:\n    def __init__(self): # could inject additional stuff\n        pass\n\n    @before(methods().named(\"hello\").of_type(Foo))\n    def call_before(self, invocation: Invocation):\n        print(\"before Foo.hello(...)\")\n\n    @error(methods().named(\"hello\").of_type(Foo))\n    def call_error(self, invocation: Invocation):\n        print(\"error Foo.hello(...)\")\n        print(invocation.exception)\n\n    @around(methods().named(\"hello\"))\n    def call_around(self, invocation: Invocation):\n        print(\"around Foo.hello()\")\n\n        return invocation.proceed()\n```\n\nThe invocation parameter stores the complete context of the current execution, which are\n- the method\n- args\n- kwargs\n- the result\n- the possible caught error\n\nLet's look at the details\n\n# Installation\n\n`pip install aspyx`\n\nThe library is tested with all Python version \u003e 3.9\n\nReady to go...\n\n# Registration\n\nDifferent mechanisms are available that make classes eligible for injection\n\n## Class\n\nAny class annotated with `@injectable` is eligible for injection\n\n**Example**: \n\n```python\n@injectable()\nclass Foo:\n    def __init__(self):\n        pass\n```\nPlease make sure, that the class defines a local constructor, as this is required to determine injected instances. \nAll referenced types will be injected by the environemnt. \n\nOnly eligible types are allowed, of course!\n\nThe decorator accepts the keyword arguments\n- `eager : boolean`  \n  if `True`, the container will create the instances automatically while booting the environment. This is the default.\n- `scope: str`  \n  the name of a - registered - scope which will determine how often instances will be created.\n\n The following scopes are implemented out of the box:\n - `singleton`  \n   objects are created once inside an environment and cached. This is the default.\n - `request`  \n   obejcts are created on every injection request\n - `thread`  \n   objects are cerated and cached with respect to the current thread.\n\n Other scopes - e.g. session related scopes - can be defined dynamically. Please check the corresponding chapter.\n\n## Class Factory\n\nClasses that implement the `Factory` base class and are annotated with `@factory` will register the appropriate classes returned by the `create` method.\n\n**Example**: \n```python\n@factory()\nclass TestFactory(Factory[Foo]):\n    def __init__(self):\n        pass\n\n    def create(self) -\u003e Foo:\n        return Foo()\n```\n\nAs in `@injectable`, the same arguments are possible.\n\n## Method \n\nAny `injectable` can define methods decorated with `@create()`, that will create appropriate instances.\n\n**Example**: \n```python\n@injectable()\nclass Foo:\n    def __init__(self):\n        pass\n\n    @create(scope=\"request\")\n    def create(self) -\u003e Baz:\n        return Baz()\n```\n\n The same arguments as in `@injectable` are possible.\n\n# Environment\n\n## Definition\n\nAn `Environment` is the container that manages the lifecycle of objects. The set of classes and instances is determined by a constructor argument that controls the class registry.\n\n**Example**: \n```python\n@environment()\nclass SampleEnvironment:\n    def __init__(self):\n        pass\n\nenvironment = Environment(SampleEnvironment)\n```\n\nThe default is that all eligible classes, that are implemented in the containing module or in any submodule will be managed.\n\nBy adding an `imports: list[Type]` parameter, specifying other environment types, it will register the appropriate classes recursively.\n\n**Example**: \n```python\n@environment()\nclass SampleEnvironmen(imports=[OtherEnvironment])):\n    def __init__(self):\n        pass\n```\n\nAnother possibility is to add a parent environment as an `Environment` constructor parameter\n\n**Example**: \n```python\nrootEnvironment = Environment(RootEnvironment)\nenvironment = Environment(SampleEnvironment, parent=rootEnvironment)\n```\n\nThe difference is, that in the first case, class instances of imported modules will be created in the scope of the _own_ environment, while in the second case, it will return instances managed by the parent.\n\nThe method\n\n```shutdown()```\n\nis used when a container is not needed anymore. It will call any `on_destroy()` of all created instances.\n\n## Retrieval\n\n```python\ndef get(type: Type[T]) -\u003e T\n```\n\nis used to retrieve object instances. Depending on the respective scope it will return either cached instances or newly instantiated objects.\n\nThe container knows about class hierarchies and is able to `get` base classes, as long as there is only one implementation. \n\nIn case of ambiguities, it will throw an exception.\n\nPlease be aware, that a base class are not _required_ to be annotated with `@injectable`, as this would mean, that it could be created on its own as well. ( Which is possible as well, btw. ) \n\n# Instantiation logic\n\nConstructing a new instance involves a number of steps executed in this order\n- Constructor call  \n  the constructor is called with the resolved parameters\n- Advice injection  \n  All methods involving aspects are updated\n- Lifecycle methods   \n  different decorators can mark methods that should be called during the lifecycle ( here the construction ) of an instance.\n  These are various injection possibilities as well as an optional final `on_init` call\n- PostProcessors  \n  Any custom post processors, that can add isde effects or modify the instances\n\n## Injection methods\n\nDifferent decorators are implemented, that call methods with computed values\n\n- `@inject`  \n   the method is called with all resolved parameter types ( same as the constructor call)\n- `@inject_environment`  \n   the method is called with the creating environment as a single parameter\n- `@inject_value()`  \n   the method is called with a resolved configuration value. Check the corresponding chapter\n\n**Example**:\n```python\n@injectable()\nclass Foo:\n    def __init__(self):\n        pass\n\n    @inject_environment()\n    def initEnvironment(self, env: Environment):\n        ...\n\n    @inject()\n    def set(self, baz: Baz) -\u003e None:\n        ...\n```\n\n## Lifecycle methods\n\nIt is possible to mark specific lifecyle methods. \n- `@on_init()` \n   called after the constructor and all other injections.\n- `@on_running()` \n   called an environment has initialized all eager objects.\n- `@on_destroy()` \n   called during shutdown of the environment\n\n## Post Processors\n\nAs part of the instantiation logic it is possible to define post processors that execute any side effect on newly created instances.\n\n**Example**: \n```python\n@injectable()\nclass SamplePostProcessor(PostProcessor):\n    def process(self, instance: object, environment: Environment):\n        print(f\"created a {instance}\")\n```\n\nAny implementing class of `PostProcessor` that is eligible for injection will be called by passing the new instance.\n\nPlease be aware, that a post processor will only handle instances _after_ its _own_ registration.\n\nAs injectables within a single file will be handled in the order as they are declared, a post processor will only take effect for all classes after its declaration!\n\n# Custom scopes\n\nAs explained, available scopes are \"singleton\" and \"request\".\n\nIt is easily possible to add custom scopes by inheriting the base-class `Scope`, decorating the class with `@scope(\u003cname\u003e)` and overriding the method `get`\n\n```python\ndef get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):\n```\n\nArguments are:\n- `provider` the actual provider that will create an instance\n- `environment`the requesting environment\n- `argPovider` a function that can be called to compute the required arguments recursively\n\n**Example**: The simplified code of the singleton provider ( disregarding locking logic )\n\n```python\n@scope(\"singleton\")\nclass SingletonScope(Scope):\n    # constructor\n\n    def __init__(self):\n        super().__init__()\n\n        self.value = None\n\n    # override\n\n    def get(self, provider: AbstractInstanceProvider, environment: Environment, argProvider: Callable[[],list]):\n        if self.value is None:\n            self.value = provider.create(environment, *argProvider())\n\n        return self.value\n```\n\n# AOP\n\nIt is possible to define different Aspects, that will be part of method calling flow. This logic fits nicely in the library, since the DI framework controls the instantiation logic and can handle aspects within a regular post processor. \n\nAdvice classes need to be part of classes that add a `@advice()` decorator and can define methods that add aspects.\n\n```python\n@advice()\nclass SampleAdvice:\n    def __init__(self):  # could inject dependencies\n        pass\n\n    @before(methods().named(\"hello\").of_type(Foo))\n    def call_before(self, invocation: Invocation):\n        # arguments: invocation.args\n        print(\"before Foo.hello(...)\")\n\n    @error(methods().named(\"hello\").of_type(Foo))\n    def call_error(self, invocation: Invocation):\n        print(\"error Foo.hello(...)\")\n        print(invocation.exception)\n\n    @around(methods().named(\"hello\"))\n    def call_around(self, invocation: Invocation):\n        print(\"around Foo.hello()\")\n\n        return invocation.proceed()  # will leave a result in invocation.result or invocation.exception in case of an exception\n```\n\nDifferent aspects - with the appropriate decorator - are possible:\n- `before`  \n   methods that will be executed _prior_ to the original method\n- `around`  \n   methods that will be executed _around_ to the original method giving it the possibility add side effects or even change the parameters.\n- `after`  \n    methods that will be executed _after_ to the original method\n- `error`  \n   methods that will be executed in case of a caught exception, which can be retrieved by `invocation.exception`\n\nAll methods are expected to hava single `Invocation` parameter, that stores, the function, args and kwargs, the return value and possible exceptions.\n\nIt is essential for `around` methods to call `proceed()` on the invocation, which will call the next around method in the chain and finally the original method.\nIf the `proceed` is called with parameters, they will replace the original parameters! \n\nThe argument list to the corresponding decorators control, how aspects are associated with which methods.\nA fluent interface is used describe the mapping. \nThe parameters restrict either methods or classes and are constructed by a call to either `methods()` or `classes()`.\n\nBoth add the fluent methods:\n- `of_type(type: Type)`  \n   defines the matching classes\n- `named(name: str)`  \n   defines method or class names\n- `matches(re: str)`  \n   defines regular expressions for methods or classes\n- `decorated_with(type: Type)`  \n   defines decorators on methods or classes\n\nThe fluent methods `named`, `matches` and `of_type` can be called multiple times!\n\n**Example**:\n\n```python\n@injectable()\nclass TransactionAdvice:\n    def __init__(self):\n        pass\n\n    @around(methods().decorated_with(transactional), classes().decorated_with(transactional))\n    def establish_transaction(self, invocation: Invocation):\n        ...\n```\n\n# Configuration \n\nIt is possible to inject configuration values, by decorating methods with `@inject-value(\u003cname\u003e)` given a configuration key.\n\n```python\n@injectable()\nclass Foo:\n    def __init__(self):\n        pass\n\n    @value(\"OS\")\n    def inject_value(self, os: str):\n        ...\n```\n\nThis concept relies on a central object `ConfigurationManager` that stores the overall configuration values as provided by so called configuration sources that are defined as follows.\n\n```python\nclass ConfigurationSource(ABC):\n    def __init__(self):\n        pass\n\n   ...\n\n    @abstractmethod\n    def load(self) -\u003e dict:\n        pass\n```\n\nThe `load` method is able to return a tree-like structure by returning a `dict`.\n\nAs a default environment variables are already supported.\n\nOther sources can be added dynamically by just registering them.\n\n**Example**:\n```python\n@injectable()\nclass SampleConfigurationSource(ConfigurationSource):\n    def __init__(self):\n        super().__init__()\n\n    def load(self) -\u003e dict:\n        return {\n            \"a\": 1, \n            \"b\": {\n                \"d\": \"2\", \n                \"e\": 3, \n                \"f\": 4\n                }\n            }\n```\n\n# Reflection\n\nAs the library heavily relies on type introspection of classes and methods, a utility class `TypeDescriptor` is available that covers type information on classes. \n\nAfter beeing instatiated with\n\n```python\nTypeDescriptor.for_type(\u003ctype\u003e)\n```\n\nit offers the methods\n- `get_methods(local=False)`  \n   return a list of either local or overall methods\n- `get_method(name: str, local=False)`  \n   return a single either local or overall method\n- `has_decorator(decorator: Callable) -\u003e bool`  \n   return `True`, if the class is decorated with the specified decrator\n- `get_decorator(decorator) -\u003e Optional[DecoratorDescriptor]`  \n   return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property\n\nThe returned method descriptors offer:\n- `param_types`  \n   list of arg types\n- `return_type`  \n   the retur type\n- `has_decorator(decorator: Callable) -\u003e bool` \n   return `True`, if the method is decorated with the specified decrator\n- `get_decorator(decorator: Callable) -\u003e Optional[DecoratorDescriptor]`  \n   return a descriptor covering the decorator. In addition to the callable, it also stores the supplied args in the `args` property\n\nThe management of decorators in turn relies on another utility class `Decorators` that caches decorators.\n\nWhenver you define a custom decorator, you will need to register it accordingly.\n\n**Example**:\n```python\ndef transactional():\n    def decorator(func):\n        Decorators.add(func, transactional)\n        return func\n\n    return decorator\n```\n\n\n# Version History\n\n**1.0.1**\n\n- some internal refactorings\n\n**1.1.0**\n\n- added `@on_running()` callback\n- added `thread` scope\n\n\n      \n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoolsamson7%2Faspyx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcoolsamson7%2Faspyx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcoolsamson7%2Faspyx/lists"}