Skip to content

Commit 87debf3

Browse files
authored
feat: add async run_subprocess with timeout and flag features and more utilities (#24)
1 parent 992edf7 commit 87debf3

18 files changed

Lines changed: 209 additions & 52 deletions

File tree

.github/workflows/cd.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
- name: version
2323
run: sed -i "s/__version__ = '.*'/__version__ = '$VERSION'/g" aioddd/__init__.py
2424
- name: deps
25-
run: python3 run-script dev-install
25+
run: python3 -m pip install .[dev,deploy]
2626
- name: deploy
2727
env:
2828
TWINE_USERNAME: ${{ secrets.POETRY_HTTP_BASIC_PYPI_USERNAME }}

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
python-version: ${{ matrix.python-version }}
2323
- name: deps
2424
timeout-minutes: 5
25-
run: python3 run-script dev-install
25+
run: python3 -m pip install .[dev,fmt,security-analysis,static-analysis,test]
2626
- name: security-analysis
2727
timeout-minutes: 1
2828
run: python3 run-script security-analysis

Dockerfile

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
1-
FROM docker.io/library/python:3.7-slim AS production
1+
FROM docker.io/library/python:3.7-slim AS base
22

3-
WORKDIR /app
3+
RUN apt update -y && python3 -m pip install --upgrade pip
44

5-
COPY LICENSE README.md pyproject.toml ./
5+
WORKDIR /app
66

7-
RUN apt update -y && \
8-
python3 -m pip install --upgrade pip && \
9-
python3 -m pip install .
7+
FROM base AS production
108

9+
COPY LICENSE README.md pyproject.toml ./
1110
COPY aioddd ./aioddd/
1211

12+
RUN python3 -m pip install .
13+
1314
ENTRYPOINT ["python3"]
1415
CMD []
1516

1617
FROM production AS development
1718

18-
RUN apt install -y gcc
19-
2019
COPY .pre-commit-config.yaml run-script ./
2120

22-
RUN python3 run-script dev-install
21+
RUN python3 -m pip install .[dev,deploy,docs,fmt,security-analysis,static-analysis,test]
2322

2423
COPY docs_src ./docs_src
2524
COPY tests ./tests

aioddd/__init__.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,17 @@
4343
find_event_mapper_by_name,
4444
find_event_mapper_by_type,
4545
)
46-
from .utils import env, get_env, get_simple_logger
46+
from .subprocess import SubprocessResult, run_subprocess # nosec
47+
from .utils import (
48+
env,
49+
get_bool_env,
50+
get_env,
51+
get_float_env,
52+
get_int_env,
53+
get_list_str_env,
54+
get_simple_logger,
55+
get_str_env,
56+
)
4757
from .value_objects import Id, StrDateTime, Timestamp
4858

4959
__version__ = '1.3.7'
@@ -92,10 +102,18 @@
92102
'InternalEventPublisher',
93103
# utils
94104
'get_env',
105+
'get_str_env',
106+
'get_bool_env',
107+
'get_int_env',
108+
'get_float_env',
109+
'get_list_str_env',
95110
'get_simple_logger',
96111
'env',
97112
# value_objects
98113
'Id',
99114
'Timestamp',
100115
'StrDateTime',
116+
# subprocess,
117+
'SubprocessResult',
118+
'run_subprocess',
101119
)

aioddd/subprocess/__init__.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from asyncio import TimeoutError as AsyncTimeoutError
2+
from asyncio import create_subprocess_exec, create_subprocess_shell, sleep, wait_for
3+
from typing import Any, Dict, NamedTuple, Optional
4+
5+
_flag_subprocess_running: Dict[str, bool] = {}
6+
7+
DEFAULT_WAIT_FLAG_SLEEP: float = 0.1
8+
9+
10+
async def _wait_flagged_subprocess_running(wait_flag: str) -> None:
11+
if not _flag_subprocess_running[wait_flag]:
12+
return
13+
await sleep(DEFAULT_WAIT_FLAG_SLEEP)
14+
15+
16+
class SubprocessResult(NamedTuple):
17+
return_code: int
18+
stdout: Optional[str] = None
19+
stderr: Optional[str] = None
20+
21+
22+
def _create_subprocess( # type: ignore
23+
*args: Any,
24+
shell: bool,
25+
stdout: Optional[int],
26+
stderr: Optional[int],
27+
limit: int,
28+
**kwds: Any,
29+
):
30+
return (
31+
create_subprocess_shell(*args, stdout=stdout, stderr=stderr, limit=limit, **kwds) # type: ignore
32+
if shell
33+
else create_subprocess_exec(*args, stdout=stdout, stderr=stderr, limit=limit, **kwds)
34+
)
35+
36+
37+
async def run_subprocess(
38+
*args: str,
39+
shell: bool = False,
40+
encoding: str = 'utf8',
41+
timeout: Optional[float] = None,
42+
wait_flag: Optional[str] = None,
43+
wait_flag_timeout: Optional[float] = None,
44+
stdout: Optional[int] = -1, # see asyncio.subprocess.PIPE,
45+
stderr: Optional[int] = -1, # see asyncio.subprocess.PIPE,
46+
limit: int = 2**64, # see streams._DEFAULT_LIMIT
47+
**kwds: Dict[str, Any],
48+
) -> SubprocessResult:
49+
"""
50+
Creates and runs a subprocess with or without shell.
51+
52+
Provides to time out the Python managed subprocess using asyncio.wait_for.
53+
Provides to flag Python managed subprocess with timeout as well using unique keys and asyncio.wait_for.
54+
55+
stdin not supported!
56+
57+
When shell=True *args will be the "cmd" arg for asyncio.subprocess.create_subprocess_shell
58+
When shell=False *args will be the "*args" arg (not "program" arg) for asyncio.subprocess.create_subprocess_exec
59+
60+
Return (return_code, stdout, stderr)
61+
"""
62+
_ = [kwds.pop(key, None) for key in ['shell', 'encoding', 'timeout', 'wait_flag', 'wait_flag_timeout']]
63+
if wait_flag:
64+
if wait_flag not in _flag_subprocess_running:
65+
_flag_subprocess_running[wait_flag] = True
66+
elif _flag_subprocess_running.get(wait_flag, True):
67+
await wait_for(fut=_wait_flagged_subprocess_running(wait_flag=wait_flag), timeout=wait_flag_timeout)
68+
_flag_subprocess_running[wait_flag] = True
69+
proc = await _create_subprocess(
70+
*args,
71+
shell=shell, # nosec
72+
stdout=stdout,
73+
stderr=stderr,
74+
limit=limit,
75+
**kwds,
76+
)
77+
78+
try:
79+
return_code = await wait_for(fut=proc.wait(), timeout=timeout)
80+
except AsyncTimeoutError:
81+
proc.terminate()
82+
return_code = await proc.wait()
83+
finally:
84+
if wait_flag and wait_flag in _flag_subprocess_running:
85+
del _flag_subprocess_running[wait_flag]
86+
87+
stdout_: Optional[bytes] = await proc.stdout.read()
88+
stderr_: Optional[bytes] = await proc.stderr.read()
89+
90+
return SubprocessResult(
91+
return_code=return_code,
92+
stdout=stdout_.strip().decode(encoding) if stdout_ else None,
93+
stderr=stderr_.strip().decode(encoding) if stderr_ else None,
94+
)

aioddd/utils.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from logging import NOTSET, Formatter, Logger, StreamHandler, getLogger
22
from os import getenv
3-
from typing import Any, Dict, Optional, Type, TypeVar, Union
3+
from typing import Any, Dict, List, Optional, Type, TypeVar, Union, cast
44

55

66
def get_env(key: str, default: Optional[str] = None, cast_default_to_str: bool = True) -> Optional[str]:
@@ -11,6 +11,38 @@ def get_env(key: str, default: Optional[str] = None, cast_default_to_str: bool =
1111
return value
1212

1313

14+
def get_str_env(key: str, default: str = '') -> str:
15+
return cast(str, get_env(key=key, default=default, cast_default_to_str=True))
16+
17+
18+
_boolean_positive_values: List[str] = ['True', 'true', 'yes', 'Y', 'y', '1']
19+
20+
21+
def get_bool_env(key: str, default: Union[bool, int] = False) -> bool:
22+
return get_env(key=key, default=str(int(default)), cast_default_to_str=False) in _boolean_positive_values
23+
24+
25+
def get_int_env(key: str, default: Union[bool, int] = 0) -> int:
26+
val = cast(str, get_env(key=key, default=str(int(default)), cast_default_to_str=False))
27+
return int(val) if val.isdigit() else default
28+
29+
30+
def get_float_env(key: str, default: Union[bool, int, float] = 0) -> float:
31+
val = cast(str, get_env(key=key, default=str(float(default)), cast_default_to_str=False))
32+
return float(val) if val.isdigit() or val.replace('.', '').isdigit() else default
33+
34+
35+
def get_list_str_env(
36+
key: str,
37+
default: Optional[List[str]] = None,
38+
*,
39+
delimiter: str = ',',
40+
allow_empty: bool = True,
41+
) -> List[str]:
42+
val = cast(str, get_env(key=key, default='' if allow_empty else delimiter.join(default or [])))
43+
return [] if allow_empty and (val is None or len(val) == 0) else val.split(delimiter)
44+
45+
1446
def get_simple_logger(
1547
name: Optional[str] = None,
1648
level: Union[str, int] = NOTSET,

docs_src/docs/en/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Key Features:
1212

1313
## Requirements
1414

15-
- Python 3.6+
15+
- Python 3.7+
1616

1717
## Installation
1818

@@ -84,4 +84,4 @@ if __name__ == '__main__':
8484
[MIT](https://github.com/aiopy/python-aioddd/blob/master/LICENSE)
8585

8686

87-
### WIP
87+
### WIP

docs_src/docs/es/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## Requisitos
1414

15-
- Python 3.6+
15+
- Python 3.7+
1616

1717
## Instalación
1818

@@ -84,4 +84,4 @@ if __name__ == '__main__':
8484
[MIT](https://github.com/aiopy/python-aioddd/blob/master/LICENSE)
8585

8686

87-
### WIP
87+
### WIP

docs_src/generated/en/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ <h1 id="async-python-ddd-utilities-library">Async Python DDD utilities library</
433433
</ul>
434434
<h2 id="requirements">Requirements</h2>
435435
<ul>
436-
<li>Python 3.6+</li>
436+
<li>Python 3.7+</li>
437437
</ul>
438438
<h2 id="installation">Installation</h2>
439439
<div class="highlight"><pre><span></span><code>python3 -m pip install aioddd
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Async Python DDD utilities library Key Features: Aggregates : Aggregate & AggregateRoot ValueObjects : Id, Timestamp & StrDateTime CQRS : Command, CommandBus, SimpleCommandBus, Query, Response, QueryHandler, QueryBus & SimpleQueryBus EventSourcing : Event, EventMapper, EventPublisher, EventHandler, EventBus, SimpleEventBus & InternalEventPublisher Errors : raise_, BaseError, NotFoundError, ConflictError, BadRequestError, UnauthorizedError, ForbiddenError, UnknownError, IdInvalidError, TimestampInvalidError, DateTimeInvalidError, EventMapperNotFoundError, EventNotPublishedError, CommandNotRegisteredError & QueryNotRegisteredError Tests : AsyncMock & mock Utils : get_env & get_simple_logger Requirements Python 3.6+ Installation python3 -m pip install aioddd Example from asyncio import get_event_loop from dataclasses import dataclass from typing import Type from aioddd import NotFoundError , \\ Command , CommandHandler , SimpleCommandBus , \\ Query , QueryHandler , OptionalResponse , SimpleQueryBus , Event _products = [] class ProductStored ( Event ): @dataclass class Attributes : ref : str attributes : Attributes class StoreProductCommand ( Command ): def __init__ ( self , ref : str ): self . ref = ref class StoreProductCommandHandler ( CommandHandler ): def subscribed_to ( self ) -> Type [ Command ]: return StoreProductCommand async def handle ( self , command : StoreProductCommand ) -> None : _products . append ( command . ref ) class ProductNotFoundError ( NotFoundError ): _code = 'product_not_found' _title = 'Product not found' class FindProductQuery ( Query ): def __init__ ( self , ref : str ): self . ref = ref class FindProductQueryHandler ( QueryHandler ): def subscribed_to ( self ) -> Type [ Query ]: return FindProductQuery async def handle ( self , query : FindProductQuery ) -> OptionalResponse : if query . ref != '123' : raise ProductNotFoundError . create ( detail = { 'ref' : query . ref }) return { 'ref' : query . ref } async def main () -> None : commands_bus = SimpleCommandBus ([ StoreProductCommandHandler ()]) await commands_bus . dispatch ( StoreProductCommand ( '123' )) query_bus = SimpleQueryBus ([ FindProductQueryHandler ()]) response = await query_bus . ask ( FindProductQuery ( '123' )) print ( response ) if __name__ == '__main__' : get_event_loop () . run_until_complete ( main ()) License MIT WIP","title":"aioddd"},{"location":"#async-python-ddd-utilities-library","text":"Key Features: Aggregates : Aggregate & AggregateRoot ValueObjects : Id, Timestamp & StrDateTime CQRS : Command, CommandBus, SimpleCommandBus, Query, Response, QueryHandler, QueryBus & SimpleQueryBus EventSourcing : Event, EventMapper, EventPublisher, EventHandler, EventBus, SimpleEventBus & InternalEventPublisher Errors : raise_, BaseError, NotFoundError, ConflictError, BadRequestError, UnauthorizedError, ForbiddenError, UnknownError, IdInvalidError, TimestampInvalidError, DateTimeInvalidError, EventMapperNotFoundError, EventNotPublishedError, CommandNotRegisteredError & QueryNotRegisteredError Tests : AsyncMock & mock Utils : get_env & get_simple_logger","title":"Async Python DDD utilities library"},{"location":"#requirements","text":"Python 3.6+","title":"Requirements"},{"location":"#installation","text":"python3 -m pip install aioddd","title":"Installation"},{"location":"#example","text":"from asyncio import get_event_loop from dataclasses import dataclass from typing import Type from aioddd import NotFoundError , \\ Command , CommandHandler , SimpleCommandBus , \\ Query , QueryHandler , OptionalResponse , SimpleQueryBus , Event _products = [] class ProductStored ( Event ): @dataclass class Attributes : ref : str attributes : Attributes class StoreProductCommand ( Command ): def __init__ ( self , ref : str ): self . ref = ref class StoreProductCommandHandler ( CommandHandler ): def subscribed_to ( self ) -> Type [ Command ]: return StoreProductCommand async def handle ( self , command : StoreProductCommand ) -> None : _products . append ( command . ref ) class ProductNotFoundError ( NotFoundError ): _code = 'product_not_found' _title = 'Product not found' class FindProductQuery ( Query ): def __init__ ( self , ref : str ): self . ref = ref class FindProductQueryHandler ( QueryHandler ): def subscribed_to ( self ) -> Type [ Query ]: return FindProductQuery async def handle ( self , query : FindProductQuery ) -> OptionalResponse : if query . ref != '123' : raise ProductNotFoundError . create ( detail = { 'ref' : query . ref }) return { 'ref' : query . ref } async def main () -> None : commands_bus = SimpleCommandBus ([ StoreProductCommandHandler ()]) await commands_bus . dispatch ( StoreProductCommand ( '123' )) query_bus = SimpleQueryBus ([ FindProductQueryHandler ()]) response = await query_bus . ask ( FindProductQuery ( '123' )) print ( response ) if __name__ == '__main__' : get_event_loop () . run_until_complete ( main ())","title":"Example"},{"location":"#license","text":"MIT","title":"License"},{"location":"#wip","text":"","title":"WIP"}]}
1+
{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Async Python DDD utilities library Key Features: Aggregates : Aggregate & AggregateRoot ValueObjects : Id, Timestamp & StrDateTime CQRS : Command, CommandBus, SimpleCommandBus, Query, Response, QueryHandler, QueryBus & SimpleQueryBus EventSourcing : Event, EventMapper, EventPublisher, EventHandler, EventBus, SimpleEventBus & InternalEventPublisher Errors : raise_, BaseError, NotFoundError, ConflictError, BadRequestError, UnauthorizedError, ForbiddenError, UnknownError, IdInvalidError, TimestampInvalidError, DateTimeInvalidError, EventMapperNotFoundError, EventNotPublishedError, CommandNotRegisteredError & QueryNotRegisteredError Tests : AsyncMock & mock Utils : get_env & get_simple_logger Requirements Python 3.7+ Installation python3 -m pip install aioddd Example from asyncio import get_event_loop from dataclasses import dataclass from typing import Type from aioddd import NotFoundError , \\ Command , CommandHandler , SimpleCommandBus , \\ Query , QueryHandler , OptionalResponse , SimpleQueryBus , Event _products = [] class ProductStored ( Event ): @dataclass class Attributes : ref : str attributes : Attributes class StoreProductCommand ( Command ): def __init__ ( self , ref : str ): self . ref = ref class StoreProductCommandHandler ( CommandHandler ): def subscribed_to ( self ) -> Type [ Command ]: return StoreProductCommand async def handle ( self , command : StoreProductCommand ) -> None : _products . append ( command . ref ) class ProductNotFoundError ( NotFoundError ): _code = 'product_not_found' _title = 'Product not found' class FindProductQuery ( Query ): def __init__ ( self , ref : str ): self . ref = ref class FindProductQueryHandler ( QueryHandler ): def subscribed_to ( self ) -> Type [ Query ]: return FindProductQuery async def handle ( self , query : FindProductQuery ) -> OptionalResponse : if query . ref != '123' : raise ProductNotFoundError . create ( detail = { 'ref' : query . ref }) return { 'ref' : query . ref } async def main () -> None : commands_bus = SimpleCommandBus ([ StoreProductCommandHandler ()]) await commands_bus . dispatch ( StoreProductCommand ( '123' )) query_bus = SimpleQueryBus ([ FindProductQueryHandler ()]) response = await query_bus . ask ( FindProductQuery ( '123' )) print ( response ) if __name__ == '__main__' : get_event_loop () . run_until_complete ( main ()) License MIT WIP","title":"aioddd"},{"location":"#async-python-ddd-utilities-library","text":"Key Features: Aggregates : Aggregate & AggregateRoot ValueObjects : Id, Timestamp & StrDateTime CQRS : Command, CommandBus, SimpleCommandBus, Query, Response, QueryHandler, QueryBus & SimpleQueryBus EventSourcing : Event, EventMapper, EventPublisher, EventHandler, EventBus, SimpleEventBus & InternalEventPublisher Errors : raise_, BaseError, NotFoundError, ConflictError, BadRequestError, UnauthorizedError, ForbiddenError, UnknownError, IdInvalidError, TimestampInvalidError, DateTimeInvalidError, EventMapperNotFoundError, EventNotPublishedError, CommandNotRegisteredError & QueryNotRegisteredError Tests : AsyncMock & mock Utils : get_env & get_simple_logger","title":"Async Python DDD utilities library"},{"location":"#requirements","text":"Python 3.7+","title":"Requirements"},{"location":"#installation","text":"python3 -m pip install aioddd","title":"Installation"},{"location":"#example","text":"from asyncio import get_event_loop from dataclasses import dataclass from typing import Type from aioddd import NotFoundError , \\ Command , CommandHandler , SimpleCommandBus , \\ Query , QueryHandler , OptionalResponse , SimpleQueryBus , Event _products = [] class ProductStored ( Event ): @dataclass class Attributes : ref : str attributes : Attributes class StoreProductCommand ( Command ): def __init__ ( self , ref : str ): self . ref = ref class StoreProductCommandHandler ( CommandHandler ): def subscribed_to ( self ) -> Type [ Command ]: return StoreProductCommand async def handle ( self , command : StoreProductCommand ) -> None : _products . append ( command . ref ) class ProductNotFoundError ( NotFoundError ): _code = 'product_not_found' _title = 'Product not found' class FindProductQuery ( Query ): def __init__ ( self , ref : str ): self . ref = ref class FindProductQueryHandler ( QueryHandler ): def subscribed_to ( self ) -> Type [ Query ]: return FindProductQuery async def handle ( self , query : FindProductQuery ) -> OptionalResponse : if query . ref != '123' : raise ProductNotFoundError . create ( detail = { 'ref' : query . ref }) return { 'ref' : query . ref } async def main () -> None : commands_bus = SimpleCommandBus ([ StoreProductCommandHandler ()]) await commands_bus . dispatch ( StoreProductCommand ( '123' )) query_bus = SimpleQueryBus ([ FindProductQueryHandler ()]) response = await query_bus . ask ( FindProductQuery ( '123' )) print ( response ) if __name__ == '__main__' : get_event_loop () . run_until_complete ( main ())","title":"Example"},{"location":"#license","text":"MIT","title":"License"},{"location":"#wip","text":"","title":"WIP"}]}

0 commit comments

Comments
 (0)