oe-python-template-example 0.3.5__py3-none-any.whl → 0.3.7__py3-none-any.whl

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.
Files changed (38) hide show
  1. oe_python_template_example/__init__.py +4 -18
  2. oe_python_template_example/api.py +42 -148
  3. oe_python_template_example/cli.py +13 -141
  4. oe_python_template_example/constants.py +6 -9
  5. oe_python_template_example/hello/__init__.py +17 -0
  6. oe_python_template_example/hello/_api.py +94 -0
  7. oe_python_template_example/hello/_cli.py +47 -0
  8. oe_python_template_example/hello/_constants.py +4 -0
  9. oe_python_template_example/hello/_models.py +28 -0
  10. oe_python_template_example/hello/_service.py +96 -0
  11. oe_python_template_example/{settings.py → hello/_settings.py} +6 -4
  12. oe_python_template_example/system/__init__.py +19 -0
  13. oe_python_template_example/system/_api.py +116 -0
  14. oe_python_template_example/system/_cli.py +165 -0
  15. oe_python_template_example/system/_service.py +186 -0
  16. oe_python_template_example/system/_settings.py +31 -0
  17. oe_python_template_example/utils/__init__.py +59 -0
  18. oe_python_template_example/utils/_api.py +18 -0
  19. oe_python_template_example/utils/_cli.py +68 -0
  20. oe_python_template_example/utils/_console.py +14 -0
  21. oe_python_template_example/utils/_constants.py +48 -0
  22. oe_python_template_example/utils/_di.py +70 -0
  23. oe_python_template_example/utils/_health.py +107 -0
  24. oe_python_template_example/utils/_log.py +122 -0
  25. oe_python_template_example/utils/_logfire.py +68 -0
  26. oe_python_template_example/utils/_process.py +41 -0
  27. oe_python_template_example/utils/_sentry.py +97 -0
  28. oe_python_template_example/utils/_service.py +39 -0
  29. oe_python_template_example/utils/_settings.py +80 -0
  30. oe_python_template_example/utils/boot.py +86 -0
  31. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.7.dist-info}/METADATA +78 -51
  32. oe_python_template_example-0.3.7.dist-info/RECORD +35 -0
  33. oe_python_template_example/models.py +0 -44
  34. oe_python_template_example/service.py +0 -68
  35. oe_python_template_example-0.3.5.dist-info/RECORD +0 -12
  36. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.7.dist-info}/WHEEL +0 -0
  37. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.7.dist-info}/entry_points.txt +0 -0
  38. {oe_python_template_example-0.3.5.dist-info → oe_python_template_example-0.3.7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,59 @@
1
+ """Utilities module."""
2
+
3
+ from ._api import VersionedAPIRouter
4
+ from ._cli import prepare_cli
5
+ from ._console import console
6
+ from ._constants import (
7
+ __author_email__,
8
+ __author_name__,
9
+ __documentation__url__,
10
+ __env__,
11
+ __env_file__,
12
+ __is_development_mode__,
13
+ __is_running_in_container__,
14
+ __project_name__,
15
+ __project_path__,
16
+ __repository_url__,
17
+ __version__,
18
+ )
19
+ from ._di import locate_implementations, locate_subclasses
20
+ from ._health import Health
21
+ from ._log import LogSettings, get_logger
22
+ from ._logfire import LogfireSettings
23
+ from ._process import ProcessInfo, get_process_info
24
+ from ._sentry import SentrySettings
25
+ from ._service import BaseService
26
+ from ._settings import UNHIDE_SENSITIVE_INFO, OpaqueSettings, load_settings
27
+ from .boot import boot
28
+
29
+ __all__ = [
30
+ "UNHIDE_SENSITIVE_INFO",
31
+ "BaseService",
32
+ "Health",
33
+ "LogSettings",
34
+ "LogSettings",
35
+ "LogfireSettings",
36
+ "OpaqueSettings",
37
+ "ProcessInfo",
38
+ "SentrySettings",
39
+ "VersionedAPIRouter",
40
+ "__author_email__",
41
+ "__author_name__",
42
+ "__documentation__url__",
43
+ "__env__",
44
+ "__env_file__",
45
+ "__is_development_mode__",
46
+ "__is_running_in_container__",
47
+ "__project_name__",
48
+ "__project_path__",
49
+ "__repository_url__",
50
+ "__version__",
51
+ "boot",
52
+ "console",
53
+ "get_logger",
54
+ "get_process_info",
55
+ "load_settings",
56
+ "locate_implementations",
57
+ "locate_subclasses",
58
+ "prepare_cli",
59
+ ]
@@ -0,0 +1,18 @@
1
+ from fastapi import APIRouter
2
+
3
+
4
+ class VersionedAPIRouter(APIRouter):
5
+ """APIRouter with version attribute.
6
+
7
+ - Use this class to create versioned routers for your FastAPI application
8
+ that are automatically registered into the FastAPI app.
9
+ - The version attribute is used to identify the version of the API
10
+ that the router corresponds to.
11
+ - See constants.por versions defined for this system.
12
+ """
13
+
14
+ version: str
15
+
16
+ def __init__(self, version: str, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
17
+ super().__init__(*args, **kwargs)
18
+ self.version = version
@@ -0,0 +1,68 @@
1
+ """Command-line interface utilities."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from ._di import locate_implementations
9
+
10
+
11
+ def prepare_cli(cli: typer.Typer, epilog: str) -> None:
12
+ """
13
+ Dynamically locate, register and prepare subcommands.
14
+
15
+ Args:
16
+ cli (typer.Typer): Typer instance
17
+ epilog (str): Epilog to add
18
+ """
19
+ for _cli in locate_implementations(typer.Typer):
20
+ if _cli != cli:
21
+ cli.add_typer(_cli)
22
+
23
+ cli.info.epilog = epilog
24
+ cli.info.no_args_is_help = True
25
+ if not any(arg.endswith("typer") for arg in Path(sys.argv[0]).parts):
26
+ for command in cli.registered_commands:
27
+ command.epilog = cli.info.epilog
28
+
29
+ # add epilog for all subcommands
30
+ if not any(arg.endswith("typer") for arg in Path(sys.argv[0]).parts):
31
+ _add_epilog_recursively(cli, epilog)
32
+
33
+ # add no_args_is_help for all subcommands
34
+ _no_args_is_help_recursively(cli)
35
+
36
+
37
+ def _add_epilog_recursively(cli: typer.Typer, epilog: str) -> None:
38
+ """
39
+ Add epilog to all typers in the tree.
40
+
41
+ Args:
42
+ cli (typer.Typer): Typer instance
43
+ epilog (str): Epilog to add
44
+ """
45
+ cli.info.epilog = epilog
46
+ for group in cli.registered_groups:
47
+ if isinstance(group, typer.models.TyperInfo):
48
+ typer_instance = group.typer_instance
49
+ if (typer_instance is not cli) and typer_instance:
50
+ _add_epilog_recursively(typer_instance, epilog)
51
+ for command in cli.registered_commands:
52
+ if isinstance(command, typer.models.CommandInfo):
53
+ command.epilog = cli.info.epilog
54
+
55
+
56
+ def _no_args_is_help_recursively(cli: typer.Typer) -> None:
57
+ """
58
+ Show help if no command is given by the user.
59
+
60
+ Args:
61
+ cli (typer.Typer): Typer instance
62
+ """
63
+ for group in cli.registered_groups:
64
+ if isinstance(group, typer.models.TyperInfo):
65
+ group.no_args_is_help = True
66
+ typer_instance = group.typer_instance
67
+ if (typer_instance is not cli) and typer_instance:
68
+ _no_args_is_help_recursively(typer_instance)
@@ -0,0 +1,14 @@
1
+ """Themed rich console."""
2
+
3
+ from rich.console import Console
4
+ from rich.theme import Theme
5
+
6
+ console = Console(
7
+ theme=Theme({
8
+ "logging.level.info": "purple4",
9
+ "debug": "light_cyan3",
10
+ "info": "purple4",
11
+ "warning": "yellow1",
12
+ "error": "red1",
13
+ }),
14
+ )
@@ -0,0 +1,48 @@
1
+ """Constants used throughout."""
2
+
3
+ import os
4
+ import sys
5
+ from importlib import metadata
6
+ from pathlib import Path
7
+
8
+ __project_name__ = __name__.split(".")[0]
9
+ __project_path__ = str(Path(__file__).parent.parent.parent)
10
+ __version__ = metadata.version(__project_name__)
11
+ __is_development_mode__ = "uvx" not in sys.argv[0].lower()
12
+ __is_running_in_container__ = os.getenv(f"{__project_name__.upper()}_RUNNING_IN_CONTAINER")
13
+ __env__ = os.getenv("ENV", os.getenv("VERCEL_ENV", "local"))
14
+ __env_file__ = [
15
+ Path.home() / f".{__project_name__}" / ".env",
16
+ Path.home() / f".{__project_name__}" / f".env.{__env__}",
17
+ Path(".env"),
18
+ Path(f".env.{__env__}"),
19
+ ]
20
+ env_file_path = os.getenv(f"{__project_name__.upper()}_ENV_FILE")
21
+ if env_file_path:
22
+ __env_file__.insert(2, Path(env_file_path))
23
+
24
+
25
+ def get_project_url_by_label(prefix: str) -> str:
26
+ """Get labeled Project-URL.
27
+
28
+ See https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata-project-url
29
+
30
+ Args:
31
+ prefix(str): The prefix to match at the beginning of URL entries.
32
+
33
+ Returns:
34
+ The extracted URL string if found, or an empty string if not found.
35
+ """
36
+ for url_entry in metadata.metadata(__project_name__).get_all("Project-URL", []):
37
+ if url_entry.startswith(prefix):
38
+ return str(url_entry.split(", ", 1)[1])
39
+
40
+ return ""
41
+
42
+
43
+ _authors = metadata.metadata(__project_name__).get_all("Author-email", [])
44
+ _author = _authors[0] if _authors else None
45
+ __author_name__ = _author.split("<")[0].strip() if _author else None
46
+ __author_email__ = _author.split("<")[1].strip(" >") if _author else None
47
+ __repository_url__ = get_project_url_by_label("Source")
48
+ __documentation__url__ = get_project_url_by_label("Documentation")
@@ -0,0 +1,70 @@
1
+ """Module for dynamic import and discovery of implementations and subclasses."""
2
+
3
+ import importlib
4
+ import pkgutil
5
+ from inspect import isclass
6
+ from typing import Any
7
+
8
+ from ._constants import __project_name__
9
+
10
+ _implementation_cache: dict[Any, list[Any]] = {}
11
+ _subclass_cache: dict[Any, list[Any]] = {}
12
+
13
+
14
+ def locate_implementations(_class: type[Any]) -> list[Any]:
15
+ """
16
+ Dynamically discover all instances of some class.
17
+
18
+ Args:
19
+ _class (type[Any]): Class to search for.
20
+
21
+ Returns:
22
+ list[Any]: List of discovered implementations of the given class.
23
+ """
24
+ if _class in _implementation_cache:
25
+ return _implementation_cache[_class]
26
+
27
+ implementations = []
28
+ package = importlib.import_module(__project_name__)
29
+
30
+ for _, name, _ in pkgutil.iter_modules(package.__path__):
31
+ module = importlib.import_module(f"{__project_name__}.{name}")
32
+ # Check all members of the module
33
+ for member_name in dir(module):
34
+ member = getattr(module, member_name)
35
+ if isinstance(member, _class):
36
+ implementations.append(member)
37
+
38
+ _implementation_cache[_class] = implementations
39
+ return implementations
40
+
41
+
42
+ def locate_subclasses(_class: type[Any]) -> list[Any]:
43
+ """
44
+ Dynamically discover all classes that are subclasses of some type.
45
+
46
+ Args:
47
+ _class (type[Any]): Parent class of subclasses to search for.
48
+
49
+ Returns:
50
+ list[type[Any]]: List of discovered subclasses of the given class.
51
+ """
52
+ if _class in _subclass_cache:
53
+ return _subclass_cache[_class]
54
+
55
+ subclasses = []
56
+ package = importlib.import_module(__project_name__)
57
+
58
+ for _, name, _ in pkgutil.iter_modules(package.__path__):
59
+ try:
60
+ module = importlib.import_module(f"{__project_name__}.{name}")
61
+ # Check all members of the module
62
+ for member_name in dir(module):
63
+ member = getattr(module, member_name)
64
+ if isclass(member) and issubclass(member, _class) and member != _class:
65
+ subclasses.append(member)
66
+ except ImportError:
67
+ continue
68
+
69
+ _subclass_cache[_class] = subclasses
70
+ return subclasses
@@ -0,0 +1,107 @@
1
+ """Health models and status definitions for service health checks."""
2
+
3
+ from enum import StrEnum
4
+ from typing import ClassVar, Self
5
+
6
+ from pydantic import BaseModel, Field, model_validator
7
+
8
+
9
+ class _HealthStatus(StrEnum):
10
+ UP = "UP"
11
+ DOWN = "DOWN"
12
+
13
+
14
+ class Health(BaseModel):
15
+ """Represents the health status of a service with optional components and failure reasons.
16
+
17
+ - A health object can have child components, i.e. health forms a tree.
18
+ - Any node in the tree can set itself to DOWN. In this case the node is required
19
+ to set the reason attribute. If reason is not set when DOWN,
20
+ automatic model validation of the tree will fail.
21
+ - DOWN'ness is propagated to parent health objects. I.e. the health of a parent
22
+ node is automatically set to DOWN if any of its child components are DOWN. The
23
+ child components leading to this will be listed in the reason.
24
+ - The root of the health tree is computed in the system module. The health of other
25
+ modules is automatically picked up by the system module.
26
+ """
27
+
28
+ Code: ClassVar[type[_HealthStatus]] = _HealthStatus
29
+ status: _HealthStatus
30
+ reason: str | None = None
31
+ components: dict[str, "Health"] = Field(default_factory=dict)
32
+
33
+ def compute_health_from_components(self) -> Self:
34
+ """Recursively compute health status from components.
35
+
36
+ - If health is already DOWN, it remains DOWN with its original reason.
37
+ - If health is UP but any component is DOWN, health becomes DOWN with
38
+ a reason listing all failed components.
39
+
40
+ Returns:
41
+ Self: The updated health instance with computed status.
42
+ """
43
+ # Skip recomputation if already known to be DOWN
44
+ if self.status == _HealthStatus.DOWN:
45
+ return self
46
+
47
+ # No components means we keep the existing status
48
+ if not self.components:
49
+ return self
50
+
51
+ # Find all DOWN components
52
+ down_components = []
53
+ for component_name, component in self.components.items():
54
+ # Recursively compute health for each component
55
+ component.compute_health_from_components()
56
+ if component.status == _HealthStatus.DOWN:
57
+ down_components.append(component_name)
58
+
59
+ # If any components are DOWN, mark the parent as DOWN
60
+ if down_components:
61
+ self.status = _HealthStatus.DOWN
62
+ if len(down_components) == 1:
63
+ self.reason = f"Component '{down_components[0]}' is DOWN"
64
+ else:
65
+ component_list = "', '".join(down_components)
66
+ self.reason = f"Components '{component_list}' are DOWN"
67
+
68
+ return self
69
+
70
+ @model_validator(mode="after")
71
+ def validate_health_state(self) -> Self:
72
+ """Validate the health state and ensure consistency.
73
+
74
+ - Compute overall health based on component health
75
+ - Ensure UP status has no associated reason
76
+ - Ensure DOWN status always has a reason
77
+
78
+ Returns:
79
+ Self: The validated model instance with correct health status.
80
+
81
+ Raises:
82
+ ValueError: If validation fails due to inconsistency.
83
+ """
84
+ # First compute health from components
85
+ self.compute_health_from_components()
86
+
87
+ # Validate that UP status has no reason
88
+ if (self.status == _HealthStatus.UP) and self.reason:
89
+ msg = f"Health {self.status} must not have reason"
90
+ raise ValueError(msg)
91
+
92
+ # Validate that DOWN status always has a reason
93
+ if (self.status == _HealthStatus.DOWN) and not self.reason:
94
+ msg = "Health DOWN must have a reason"
95
+ raise ValueError(msg)
96
+
97
+ return self
98
+
99
+ def __str__(self) -> str:
100
+ """Return string representation of health status with optional reason for DOWN state.
101
+
102
+ Returns:
103
+ str: The health status value, with reason appended if status is DOWN.
104
+ """
105
+ if self.status == _HealthStatus.DOWN and self.reason:
106
+ return f"{self.status.value}: {self.reason}"
107
+ return self.status.value
@@ -0,0 +1,122 @@
1
+ """Logging configuration and utilities."""
2
+
3
+ import logging as python_logging
4
+ import typing as t
5
+ from logging import FileHandler
6
+ from typing import Annotated, Literal
7
+
8
+ import click
9
+ import logfire
10
+ from pydantic import Field
11
+ from pydantic_settings import BaseSettings, SettingsConfigDict
12
+ from rich.console import Console
13
+ from rich.logging import RichHandler
14
+
15
+ from ._constants import __env_file__, __project_name__
16
+ from ._settings import load_settings
17
+
18
+
19
+ def get_logger(name: str | None) -> python_logging.Logger:
20
+ """
21
+ Get a logger instance with the given name or project name as default.
22
+
23
+ Args:
24
+ name(str): The name for the logger. If None, uses project name.
25
+
26
+ Returns:
27
+ Logger: Configured logger instance.
28
+ """
29
+ if (name is None) or (name == __project_name__):
30
+ return python_logging.getLogger(__project_name__)
31
+ return python_logging.getLogger(f"{__project_name__}.{name}")
32
+
33
+
34
+ class LogSettings(BaseSettings):
35
+ """Settings for configuring logging behavior."""
36
+
37
+ model_config = SettingsConfigDict(
38
+ env_prefix=f"{__project_name__.upper()}_LOG_",
39
+ extra="ignore",
40
+ env_file=__env_file__,
41
+ env_file_encoding="utf-8",
42
+ )
43
+
44
+ level: Annotated[
45
+ Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
46
+ Field(description="Logging level", default="INFO"),
47
+ ]
48
+ file_enabled: Annotated[
49
+ bool,
50
+ Field(description="Enable logging to file", default=False),
51
+ ]
52
+ file_name: Annotated[
53
+ str,
54
+ Field(description="Name of the log file", default=f"{__project_name__}.log"),
55
+ ]
56
+ console_enabled: Annotated[
57
+ bool,
58
+ Field(description="Enable logging to console", default=False),
59
+ ]
60
+
61
+
62
+ class CustomFilter(python_logging.Filter):
63
+ """Custom filter for log records."""
64
+
65
+ @staticmethod
66
+ def filter(_record: python_logging.LogRecord) -> bool:
67
+ """
68
+ Filter log records based on custom criteria.
69
+
70
+ Args:
71
+ record: The log record to filter.
72
+
73
+ Returns:
74
+ bool: True if record should be logged, False otherwise.
75
+ """
76
+ return True
77
+
78
+
79
+ def logging_initialize(log_to_logfire: bool = False) -> None:
80
+ """Initialize logging configuration."""
81
+ log_filter = CustomFilter()
82
+
83
+ handlers = []
84
+
85
+ settings = load_settings(LogSettings)
86
+
87
+ if settings.file_enabled:
88
+ file_handler = python_logging.FileHandler(settings.file_name)
89
+ file_formatter = python_logging.Formatter(
90
+ fmt="%(asctime)s %(process)d %(levelname)s %(name)s %(message)s",
91
+ datefmt="%Y-%m-%d %H:%M:%S",
92
+ )
93
+ file_handler.setFormatter(file_formatter)
94
+ file_handler.addFilter(log_filter)
95
+ handlers.append(file_handler)
96
+
97
+ if settings.console_enabled:
98
+ rich_handler = RichHandler(
99
+ console=Console(stderr=True),
100
+ markup=True,
101
+ rich_tracebacks=True,
102
+ tracebacks_suppress=[click],
103
+ show_time=True,
104
+ omit_repeated_times=True,
105
+ show_path=True,
106
+ show_level=True,
107
+ enable_link_path=True,
108
+ )
109
+ rich_handler.addFilter(log_filter)
110
+ handlers.append(t.cast("FileHandler", rich_handler))
111
+
112
+ if log_to_logfire:
113
+ logfire_handler = logfire.LogfireLoggingHandler()
114
+ logfire_handler.addFilter(log_filter)
115
+ handlers.append(t.cast("FileHandler", logfire_handler))
116
+
117
+ python_logging.basicConfig(
118
+ level=settings.level,
119
+ format="%(name)s %(message)s",
120
+ datefmt="%Y-%m-%d %H:%M:%S",
121
+ handlers=handlers,
122
+ )
@@ -0,0 +1,68 @@
1
+ """Logfire integration for logging and instrumentation."""
2
+
3
+ from typing import Annotated
4
+
5
+ import logfire
6
+ from pydantic import Field, PlainSerializer, SecretStr
7
+ from pydantic_settings import SettingsConfigDict
8
+
9
+ from ._constants import __env__, __env_file__, __project_name__, __repository_url__, __version__
10
+ from ._settings import OpaqueSettings, load_settings
11
+
12
+
13
+ class LogfireSettings(OpaqueSettings):
14
+ """Configuration settings for Logfire integration."""
15
+
16
+ model_config = SettingsConfigDict(
17
+ env_prefix=f"{__project_name__.upper()}_LOGFIRE_",
18
+ env_file=__env_file__,
19
+ env_file_encoding="utf-8",
20
+ extra="ignore",
21
+ )
22
+
23
+ token: Annotated[
24
+ SecretStr | None,
25
+ PlainSerializer(func=OpaqueSettings.serialize_sensitive_info, return_type=str, when_used="always"),
26
+ Field(description="Logfire token. Leave empty to disable logfire.", examples=["YOUR_TOKEN"], default=None),
27
+ ]
28
+ instrument_system_metrics: Annotated[
29
+ bool,
30
+ Field(description="Enable system metrics instrumentation", default=False),
31
+ ]
32
+
33
+
34
+ def logfire_initialize(modules: list["str"]) -> bool:
35
+ """Initialize Logfire integration.
36
+
37
+ Args:
38
+ modules(list["str"]): List of modules to be instrumented.
39
+
40
+ Returns:
41
+ bool: True if initialized successfully False otherwise
42
+ """
43
+ settings = load_settings(LogfireSettings)
44
+
45
+ if settings.token is None:
46
+ return False
47
+
48
+ logfire.configure(
49
+ send_to_logfire="if-token-present",
50
+ token=settings.token.get_secret_value(),
51
+ environment=__env__,
52
+ service_name=__project_name__,
53
+ console=False,
54
+ code_source=logfire.CodeSource(
55
+ repository=__repository_url__,
56
+ revision=__version__,
57
+ root_path="",
58
+ ),
59
+ )
60
+
61
+ if settings.instrument_system_metrics:
62
+ logfire.instrument_system_metrics(base="full")
63
+
64
+ logfire.instrument_pydantic()
65
+
66
+ logfire.install_auto_tracing(modules=modules, min_duration=0.0)
67
+
68
+ return True
@@ -0,0 +1,41 @@
1
+ """Process related utilities."""
2
+
3
+ from pathlib import Path
4
+
5
+ import psutil
6
+ from pydantic import BaseModel
7
+
8
+
9
+ class ParentProcessInfo(BaseModel):
10
+ """Information about a parent process."""
11
+
12
+ name: str | None = None
13
+ pid: int | None = None
14
+
15
+
16
+ class ProcessInfo(BaseModel):
17
+ """Information about the current process."""
18
+
19
+ project_root: str
20
+ pid: int
21
+ parent: ParentProcessInfo
22
+
23
+
24
+ def get_process_info() -> ProcessInfo:
25
+ """
26
+ Get information about the current process and its parent.
27
+
28
+ Returns:
29
+ ProcessInfo: Object containing process information.
30
+ """
31
+ current_process = psutil.Process()
32
+ parent = current_process.parent()
33
+
34
+ return ProcessInfo(
35
+ project_root=str(Path(__file__).parent.parent.parent.parent),
36
+ pid=current_process.pid,
37
+ parent=ParentProcessInfo(
38
+ name=parent.name() if parent else None,
39
+ pid=parent.pid if parent else None,
40
+ ),
41
+ )