Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions .github/workflows/pypi-publish.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
name: release

on:
release:
types:
- published
workflow_dispatch:

name: release

jobs:
make-binaries:
name: test & build binary
name: test & build
environment: pypi
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-22.04, ubuntu-latest, macos-latest]
python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
runs-on: ${{ matrix.os }}
steps:
Expand All @@ -31,14 +31,29 @@ jobs:

- run: python --version

- name: Run tests
run: uv run --isolated manage.py test
- run: uv venv ${VIRTUAL_ENV}

- run: uv sync

- name: Run tests pre
run: uv run manage.py test

- name: Build the project with mypyc
env:
USE_MYPYC: 1
run: uv build --wheel

- name: Remove systempy sources to use installed wheel
run: rm -rf ./systempy
shell: bash

- name: Install built wheel
run: pip install -t . --no-deps dist/*
shell: bash

- name: Run tests post to check built wheel
run: uv run manage.py test

- uses: actions/upload-artifact@v4
with:
name: binary-wheel-${{ github.run_id }}-${{ matrix.os }}-${{ matrix.python }}
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ celerybeat.pid
/ENV/
/env.bak/
/venv.bak/
/.venv-wine/
/.venv-*/

# Spyder project settings
.spyderproject
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ components as mixins into the current application `App` class. Then create an
instance and pass dependencies as keyword arguments. In case it's a self-hosted
app you have to call the `instance.run_sync()` method

Note that `systempy` is **NOT** a di framework, but it may be used with any of
them. Also `systempy` is **NOT** a binding to systemd, but I was inspired by it
and `systempy` is doing similar things on a much smaller scale

## Basic principles

There are 6 the most significant stages of the application lifecycle:
Expand Down
93 changes: 51 additions & 42 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ components as mixins into the current application `App` class. Then create an
instance and pass dependencies as keyword arguments. In case it's a self-hosted
app you have to call the `instance.run_sync()` method

Note that `systempy` is **NOT** a di framework, but it may be used with any of
them. Also `systempy` is **NOT** a binding to systemd, but I was inspired by it
and `systempy` is doing similar things on a much smaller scale

It's possible to use `systemPY` in three scenarios:

- Secondary application, which is handled by another application like
Expand Down Expand Up @@ -144,45 +148,53 @@ mind that we also need to be able do a safe application reload. Just look at the

### Naming and roles

All magic happens in `TargetMeta` metaclass. `TargetMeta` is a subclass of
`abc.ABCMeta`, that's why you are able to to use `@abc.abstractmethod` decorator
All the magic happens in `TargetMeta` metaclass. The `TargetMeta` is a subclass
of `abc.ABCMeta`, that's why you are able to to use `@abc.abstractmethod`
decorator

=== "Class roles"

There are 4 roles of classes I found:
There are 6 roles of classes I found:

* `Target` &#151 the interface which defines lifecycle methods

* `Unit` &#151 component with lifecycle methods

* `Mixin` &#151 class **without** lifecycle methods. It's special optimization of
`Target` role
* `Mixin` &#151 class **without** lifecycle methods. It's special
optimization of `Target` role

* `App` &#151 the final "baked" class with composed lifecycle methods

`TargetMeta` checks `role` kwarg. If kwarg `role` is not defined, `TargetMeta`
tries to parse class name and decide what to do
* `Builtins` &#151 a special optimization to force skiping `builtins`
classes processing by `libsystempy`. Normally you wouldn't face it

* `Metaclass` &#151 the same kind of optimization as `Builtins`, but used by
`TargetMeta` and its subclasses. It happens automatically in the
`__init_subclass__` hook

`TargetMeta` checks `role` kwarg. If kwarg `role` is not defined,
`TargetMeta` tries to parse class name and decide what to do

=== "by naming"

Here we are trying to manipulate class roles by class names. It's wery similar
to idea of [tailwind-css](https://tailwindcss.com/). You don't have to do any
extra import, just keep class naming and be happy:
Here we are trying to manipulate class roles by class names. It's very
similar to the idea of [tailwind-css](https://tailwindcss.com/). You don't
have to do any extra import, just follow class naming and be happy:

* Classes with names, ends with `Target` or `TargetABC` / matches
`r'(\S*)Target(ABC)?$'`, will be interpreted as `Target` role
`r'(\S*)Target(ABC)?$'`, will be interpreted as a `Target` role

* Classes with names, ends with `Unit` or `UnitABC` / matches
`r'(\S*)Unit(ABC)?$'`, will be interpreted as `Unit` role
`r'(\S*)Unit(ABC)?$'`, will be interpreted as an `Unit` role

* Classes with names, ends with `Mixin` or `MixinABC` / matches
`r'(\S*)Mixin(ABC)?$'`, will be interpreted as `Mixin` role. Remember: the
`Mixin` role is a special optimisation of `Target` role and means the class
**does not have own lifecycle methods**
`r'(\S*)Mixin(ABC)?$'`, will be interpreted as a `Mixin` role. Remember: the
`Mixin` role is a special optimisation of the `Target` role which means that
the class **does not have own lifecycle methods**

* Classes with names, ends with `App` / matches `r'(\S*)App$'`, will be
interpreted as `App` role. Due App role does not allow subclassing, AppABC
has no sense
interpreted as an `App` role. Due App role does not allow subclassing,
AppABC has no sense

```python
from systempy import Target
Expand All @@ -204,9 +216,9 @@ All magic happens in `TargetMeta` metaclass. `TargetMeta` is a subclass of

=== "by `role` kwarg"

Sometimes you may prefer to pass to class explicit role. You can find such
examples in `systempy` code base too. When you are passing `role` kwargs,
`systempy` doesn't try to parse class name:
Sometimes you may prefer to pass to the class the explicit role. You can
find such examples in `systempy` code base too. When you are passing the
`role` kwarg, `systempy` doesn't try to parse class name:

```python
from systempy import ROLE, Target
Expand All @@ -218,9 +230,9 @@ All magic happens in `TargetMeta` metaclass. `TargetMeta` is a subclass of
```


## That's all? Nope, it's really begin
## That's all? Nope, it's the very beginning!

You are able to register own `Target` with own lifecycle methods. The first
You are able to register own `Target` with your own lifecycle methods. The first
such example is [already included](https://github.com/kai3341/systemPY/blob/main/systempy/ext/target_ext.py):

=== "Code"
Expand All @@ -243,40 +255,37 @@ such example is [already included](https://github.com/kai3341/systemPY/blob/main

=== "Methods"

Here there were registered two new lifecycle methods:
Here two new lifecycle methods there were registered:

* `post_startup` callbacks will be called exactly after finished
`Target.on_startup` in `DIRECTION.FORWARD` order

* `pre_shutdown` callbacks will be called before running `Target.on_shutdown`
in `DIRECTION.BACKWARD` order
`Target.on_startup` in a `DIRECTION.FORWARD` order

You are able to define your own lifecycle stages without any limit binding them
before or after already existing. It's like `systemd`'s `Unit` options `Before`
and `After`. Yes, [`systemPY` is a small `systemd`'s brother](https://telegra.ph/Why-does-it-systemPY-08-12)
* `pre_shutdown` callbacks will be called before running
`Target.on_shutdown` in a `DIRECTION.BACKWARD` order

You can find more examples. Interesting `Target` example is a
[daemon](./examples/self-hosted/daemon.md) example
You are able to define your own lifecycle stages without any limit by binding
them before or after already existing. It's like `systemd`'s `Unit` options
`Before` and `After`. Yes, [`systemPY` is a small `systemd`'s brother](https://telegra.ph/Why-does-it-systemPY-08-12)

Also look at the [REPL](./examples/self-hosted/repl.md) example. REPL is useful
and handy, also example has the most canonical usage example
You can find more examples. Actually the whole `systempy` itself is the example
of `libsystempy` usage. Don't be afraid to read `systempy`'s source code

## Method Relosve Order

I'll exaplin on the part of [REPL](./examples/self-hosted/repl.md) example:

```python
class MyReplApp( # INIT # SHUTDOWN
ConfigUnit, # 1 # 5
LoggerUnit, # 2 # 4
MyFirstDBUnit, # 3 # 3
RedisUnit, # 4 # 2
PTReplUnit, # 5 # 1
): ...
ConfigUnit, # 1 # 6
LoggerUnit, # 2 # 5
MyFirstDBUnit, # 3 # 4
RedisUnit, # 4 # 3
PTReplUnit, # 5 # 2
): ... # 6 # 1
```

Important: while you are implementing your `Unit` mixins, remember **NEVER** call
`super()` in lifecycle methods. These methods will be collected and called
Important: while you are implementing your `Unit` mixins, remember **NEVER**
call `super()` in lifecycle methods. These methods will be collected and called
by `systemPY` in the right order

# Installing
Expand Down
6 changes: 0 additions & 6 deletions examples/async_reload_signal.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
from pathlib import Path
from sys import path

root_dir = Path(__file__).parent.parent
path.append(str(root_dir))

from _util._cbutil import _method_async, _method_sync
from systempy import (
DIRECTION,
Expand Down
6 changes: 0 additions & 6 deletions examples/ptrepl.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
from pathlib import Path
from sys import path

root_dir = Path(__file__).parent.parent
path.append(str(root_dir))

from _util._cbutil import _method_sync
from systempy import Target
from systempy.unit.ext.ptrepl import PTReplUnit
Expand Down
6 changes: 0 additions & 6 deletions examples/repl_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,6 @@
ReplUnit is deprecated. Please use PTReplUnit instead.
"""

from pathlib import Path
from sys import path

root_dir = Path(__file__).parent.parent
path.append(str(root_dir))

from _util._cbutil import _method_sync
from systempy import ReplUnit, Target

Expand Down
5 changes: 0 additions & 5 deletions examples/sync_reload_signal.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
from pathlib import Path
from sys import path
from time import sleep

root_dir = Path(__file__).parent.parent
path.append(str(root_dir))

from _util._cbutil import _method_sync
from systempy import DaemonUnit, Target

Expand Down
11 changes: 4 additions & 7 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
from argparse import ArgumentParser, Namespace
from collections.abc import Callable, Generator
from dataclasses import dataclass
from os import environ, execv
from os import environ, execve
from pathlib import Path
from sys import executable
from sys import path as syspath
from typing import ClassVar

root_dir = Path()
src_dir = root_dir / "src"
syspath.append(str(src_dir))
root_dir = Path().absolute()

VENV_BIN = Path(executable).parent

Expand Down Expand Up @@ -92,7 +89,7 @@ def collect_args_iter(self) -> Generator[str, None, None]:

def execute(self) -> None:
args = tuple(self.collect_args_iter())
execv(args[0], args) # noqa: S606
execve(args[0], args, {}) # noqa: S606


@ManagePY.register("test")
Expand Down Expand Up @@ -125,7 +122,7 @@ def collect_args_iter(self) -> Generator[str, None, None]:

def execute(self) -> None:
args = tuple(self.collect_args_iter())
execv(args[0], args) # noqa: S606
execve(args[0], args, {"PYTHONPATH": str(root_dir)}) # noqa: S606


if __name__ == "__main__":
Expand Down
29 changes: 22 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "systempy"
version = "0.1.5"
version = "0.1.6"
description = "Python application component initialization system"
authors = [{name = "kai3341"}]
readme = "README.md"
Expand All @@ -17,6 +17,7 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Libraries :: Application Frameworks",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand All @@ -31,16 +32,21 @@ keywords = [
"manager"
]
license="MIT"
license-files = ["LICENSE"]

[project.optional-dependencies]
extra = [
"celery>=5.5.1",
"ptpython>=3.0.30",
"starlette>=0.46.2",
]

[tool.pyright]
include = ["systempy/", "tests/", "_util/"]
exclude = [
"^build/",
"^dist/",
"systempy/unit/ext/celery.py",
"systempy/unit/ext/starlette.py",
]
reportMissingModuleSource = false
reportArgumentType = false
reportCallIssue = false
Expand Down Expand Up @@ -70,15 +76,24 @@ line-ending = "auto"
python_version = "3.11"
mypy_path = "$MYPY_CONFIG_FILE_DIR/systempy"
disable_error_code = ["import-untyped", "return", "empty-body"]
exclude = ["^build/", "^dist/"]
exclude = [
"^build/",
"^dist/",
"systempy/unit/ext/celery.py",
"systempy/unit/ext/starlette.py",
]

[dependency-groups]
dev = [
"mkdocs-material>=9.6.11",
"mypy[mypyc]>=1.15.0",
"mypy[mypyc]>=1.15.0; python_version >= '3.9'",
"ruff>=0.11.5",
"wheel>=0.45.1",
"wheel>=0.45.1; python_version >= '3.14'",
]

[build-system]
requires = ["tomli; python_version < '3.11'", "mypy[mypyc]", "wheel>=0.45.1"]
requires = [
"tomli; python_version < '3.11'",
"mypy[mypyc]>=1.15.0; python_version >= '3.9'",
"wheel>=0.45.1; python_version >= '3.14'",
]
Loading
Loading