updated dependencies
All checks were successful
CI / build (push) Successful in 15s

added topic class
This commit is contained in:
2024-10-24 02:48:51 +08:00
parent 4bddc35633
commit ae1629c115
8 changed files with 321 additions and 52 deletions

View File

@@ -1,25 +1,29 @@
name: CI name: CI
on: on:
push: push:
branches: [ master ] tags:
- '*'
jobs: jobs:
build: build:
runs-on: woryzen runs-on: woryzen
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-python@v5 - uses: actions/setup-python@v5
with: with:
cache: 'pip' cache: 'pip'
- name: Create virtualenv - name: Create virtualenv
run: | run: |
python -m venv .venv python -m venv .venv
.venv/bin/pip install -r requirements.txt .venv/bin/pip install -r requirements-dev.txt
- name: Run unit tests - name: Run unit tests
run: .venv/bin/python -m unittest discover -s tests run: .venv/bin/python -m unittest discover -s tests
- name: Execute build - name: Execute build
run: | run: |
.venv/bin/python -m build .venv/bin/pyproject-build
- name: Publish artifacts - name: Publish artifacts
env: env:
TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }} TWINE_REPOSITORY_URL: ${{ vars.PYPI_REGISTRY_URL }}

5
.gitignore vendored
View File

@@ -1,7 +1,8 @@
dist
.idea .idea
__pycache__ __pycache__
.venv .venv
*.pyc *.pyc
*.egg-info *.egg-info
/build
/dist
src/pwo/_version.py

View File

@@ -1,10 +1,10 @@
[build-system] [build-system]
requires = ["setuptools>=61.0"] requires = ["setuptools>=61.0", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "pwo" name = "pwo"
version = "0.0.3" dynamic = ["version"]
authors = [ authors = [
{ name="Walter Oggioni", email="oggioni.walter@gmail.com" }, { name="Walter Oggioni", email="oggioni.walter@gmail.com" },
] ]
@@ -17,7 +17,12 @@ classifiers = [
"Operating System :: OS Independent", "Operating System :: OS Independent",
] ]
dependencies = [ dependencies = [
'typing_extensions==4.7.1' 'typing_extensions'
]
[project.optional-dependencies]
dev = [
"pip-tools", "build", "mypy", "ipdb", "twine"
] ]
[project.urls] [project.urls]
@@ -35,4 +40,7 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = ["scripts", "docs", "test"] exclude = ["scripts", "docs", "test"]
strict = true strict = true
[tool.setuptools_scm]
version_file = "src/pwo/_version.py"

140
requirements-dev.txt Normal file
View File

@@ -0,0 +1,140 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile --extra=dev --output-file=requirements-dev.txt pyproject.toml
#
--index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
--extra-index-url https://pypi.org/simple
asttokens==2.4.1
# via stack-data
build==1.2.2.post1
# via
# pip-tools
# pwo (pyproject.toml)
certifi==2024.8.30
# via requests
cffi==1.17.1
# via cryptography
charset-normalizer==3.4.0
# via requests
click==8.1.7
# via pip-tools
cryptography==43.0.3
# via secretstorage
decorator==5.1.1
# via
# ipdb
# ipython
docutils==0.21.2
# via readme-renderer
executing==2.1.0
# via stack-data
idna==3.10
# via requests
importlib-metadata==8.5.0
# via twine
ipdb==0.13.13
# via pwo (pyproject.toml)
ipython==8.28.0
# via ipdb
jaraco-classes==3.4.0
# via keyring
jaraco-context==6.0.1
# via keyring
jaraco-functools==4.1.0
# via keyring
jedi==0.19.1
# via ipython
jeepney==0.8.0
# via
# keyring
# secretstorage
keyring==25.4.1
# via twine
markdown-it-py==3.0.0
# via rich
matplotlib-inline==0.1.7
# via ipython
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.5.0
# via
# jaraco-classes
# jaraco-functools
mypy==1.13.0
# via pwo (pyproject.toml)
mypy-extensions==1.0.0
# via mypy
nh3==0.2.18
# via readme-renderer
packaging==24.1
# via build
parso==0.8.4
# via jedi
pexpect==4.9.0
# via ipython
pip-tools==7.4.1
# via pwo (pyproject.toml)
pkginfo==1.10.0
# via twine
prompt-toolkit==3.0.48
# via ipython
ptyprocess==0.7.0
# via pexpect
pure-eval==0.2.3
# via stack-data
pycparser==2.22
# via cffi
pygments==2.18.0
# via
# ipython
# readme-renderer
# rich
pyproject-hooks==1.2.0
# via
# build
# pip-tools
readme-renderer==44.0
# via twine
requests==2.32.3
# via
# requests-toolbelt
# twine
requests-toolbelt==1.0.0
# via twine
rfc3986==2.0.0
# via twine
rich==13.9.3
# via twine
secretstorage==3.3.3
# via keyring
six==1.16.0
# via asttokens
stack-data==0.6.3
# via ipython
traitlets==5.14.3
# via
# ipython
# matplotlib-inline
twine==5.1.1
# via pwo (pyproject.toml)
typing-extensions==4.12.2
# via
# mypy
# pwo (pyproject.toml)
urllib3==2.2.3
# via
# requests
# twine
wcwidth==0.2.13
# via prompt-toolkit
wheel==0.44.0
# via pip-tools
zipp==3.20.2
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools

View File

@@ -1,34 +1,11 @@
build==1.2.1 #
certifi==2024.6.2 # This file is autogenerated by pip-compile with Python 3.12
cffi==1.16.0 # by the following command:
charset-normalizer==3.3.2 #
cryptography==42.0.8 # pip-compile --output-file=requirements.txt pyproject.toml
docutils==0.21.2 #
idna==3.7 --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple
importlib_metadata==7.2.0 --extra-index-url https://pypi.org/simple
jaraco.classes==3.4.0
jaraco.context==5.3.0 typing-extensions==4.12.2
jaraco.functools==4.0.1 # via pwo (pyproject.toml)
jeepney==0.8.0
keyring==25.2.1
markdown-it-py==3.0.0
mdurl==0.1.2
more-itertools==10.3.0
mypy==1.10.0
mypy-extensions==1.0.0
nh3==0.2.17
packaging==24.1
pkginfo==1.11.1
pycparser==2.22
Pygments==2.18.0
pyproject_hooks==1.1.0
readme_renderer==43.0
requests==2.32.3
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==13.7.1
SecretStorage==3.3.3
twine==5.1.0
typing_extensions==4.12.2
urllib3==2.2.2
zipp==3.19.2

View File

@@ -5,7 +5,8 @@ from .private import (
async_test, async_test,
ExceptionHandlerOutcome, ExceptionHandlerOutcome,
tmpdir, tmpdir,
decorator_with_kwargs decorator_with_kwargs,
classproperty
) )
from .maybe import Maybe from .maybe import Maybe
@@ -17,5 +18,6 @@ __all__ = [
'ExceptionHandlerOutcome', 'ExceptionHandlerOutcome',
'Maybe', 'Maybe',
'tmpdir', 'tmpdir',
'decorator_with_kwargs' 'decorator_with_kwargs',
'classproperty'
] ]

View File

@@ -1,12 +1,12 @@
import math import math
from tempfile import TemporaryDirectory from asyncio import sleep as async_sleep, Runner, Queue
from pathlib import Path
from enum import Enum, auto from enum import Enum, auto
from typing import Callable
from inspect import signature
from time import sleep
from asyncio import sleep as async_sleep, Runner
from functools import wraps, partial from functools import wraps, partial
from inspect import signature
from pathlib import Path
from tempfile import TemporaryDirectory
from time import sleep
from typing import Callable
def decorator_with_kwargs(decorator: Callable) -> Callable: def decorator_with_kwargs(decorator: Callable) -> Callable:
@@ -162,7 +162,6 @@ def tmpdir(f,
dir=None, dir=None,
ignore_cleanup_errors=False, ignore_cleanup_errors=False,
delete=True): delete=True):
@wraps(f) @wraps(f)
def result(*args, **kwargs): def result(*args, **kwargs):
with TemporaryDirectory( with TemporaryDirectory(
@@ -176,3 +175,50 @@ def tmpdir(f,
}) })
return result return result
class ClassPropertyDescriptor:
def __init__(self, fget, fset=None):
self.fget = fget
self.fset = fset
def __get__(self, obj, klass=None):
if klass is None:
klass = type(obj)
return self.fget.__get__(obj, klass)()
def __set__(self, obj, value):
if not self.fset:
raise AttributeError("can't set attribute")
type_ = type(obj)
return self.fset.__get__(obj, type_)(value)
def setter(self, func):
if not isinstance(func, (classmethod, staticmethod)):
func = classmethod(func)
self.fset = func
return self
def classproperty(func):
if not isinstance(func, (classmethod, staticmethod)):
func = classmethod(func)
return ClassPropertyDescriptor(func)
class AsyncQueueIterator:
_queue: Queue
def __init__(self, queue: Queue):
self._queue = queue
def __aiter__(self):
return self
async def __anext__(self):
item = await self._queue.get()
if item is None:
raise StopAsyncIteration
return item

91
src/pwo/topic.py Normal file
View File

@@ -0,0 +1,91 @@
from .private import AsyncQueueIterator
from asyncio import Queue, AbstractEventLoop, Future, CancelledError
from typing import Callable, Optional
from logging import getLogger
log = getLogger(__name__)
class Subscriber:
_unsubscribe_callback: Callable[['Subscriber'], None]
_event: Optional[Future]
_loop: AbstractEventLoop
def __init__(self, unsubscribe: Callable[['Subscriber'], None], loop: AbstractEventLoop):
self._unsubscribe_callback = unsubscribe
self._event: Optional[Future] = None
self._loop = loop
def unsubscribe(self) -> None:
self._event.cancel()
self._unsubscribe_callback(self)
log.debug('Deleted subscriber %s', id(self))
async def wait(self, tout: float) -> bool:
self._event = self._loop.create_future()
def callback():
if not self._event.done():
self._event.set_result(False)
handle = self._loop.call_later(tout, callback)
try:
log.debug('Subscriber %s is waiting for an event', id(self))
return await self._event
except CancelledError:
return False
finally:
handle.cancel()
def notify(self) -> None:
log.debug('Subscriber %s notified', id(self))
if not self._event.done():
self._event.set_result(True)
def reset(self) -> None:
self._event = self._loop.create_future()
class Topic:
_loop: AbstractEventLoop
_queue: Queue
_subscriptions: dict[str, set[Subscriber]]
def __init__(self, loop: AbstractEventLoop):
self._subscriptions: dict[str, set[Subscriber]] = dict()
self._loop = loop
self._queue = Queue()
def subscribe(self, path: str) -> Subscriber:
subscriptions = self._subscriptions
subscriptions_per_path = subscriptions.setdefault(path, set())
def unsubscribe_callback(subscription):
subscriptions_per_path.remove(subscription)
log.debug('Unsubscribed %s from topic %s', id(result), path)
result = Subscriber(unsubscribe_callback, self._loop)
log.debug('Created subscriber %s to topic %s', id(result), path)
subscriptions_per_path.add(result)
return result
def _notify_subscriptions(self, path):
subscriptions = self._subscriptions
subscriptions_per_path = subscriptions.get(path, None)
if subscriptions_per_path:
log.debug(f"Subscribers on '{path}': {len(subscriptions_per_path)}")
for s in subscriptions_per_path:
s.notify()
async def process_events(self):
async for evt in AsyncQueueIterator(self._queue):
log.debug(f"Processed event for path '{evt}'")
self._notify_subscriptions(evt)
log.debug(f"Event processor has completed")
def post_event(self, path):
def callback():
self._queue.put_nowait(path)
log.debug(f"Posted event for topic '{path}', queue size: {self._queue.qsize()}")
self._loop.call_soon_threadsafe(callback)