in-layers-core 0.1.1__tar.gz
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.
- in_layers_core-0.1.1/PKG-INFO +27 -0
- in_layers_core-0.1.1/README.md +10 -0
- in_layers_core-0.1.1/pyproject.toml +191 -0
- in_layers_core-0.1.1/src/in_layers/__init__.py +0 -0
- in_layers_core-0.1.1/src/in_layers/core/__init__.py +54 -0
- in_layers_core-0.1.1/src/in_layers/core/entries.py +57 -0
- in_layers_core-0.1.1/src/in_layers/core/globals/__init__.py +10 -0
- in_layers_core-0.1.1/src/in_layers/core/globals/features.py +34 -0
- in_layers_core-0.1.1/src/in_layers/core/globals/libs.py +77 -0
- in_layers_core-0.1.1/src/in_layers/core/globals/logging.py +526 -0
- in_layers_core-0.1.1/src/in_layers/core/globals/services.py +81 -0
- in_layers_core-0.1.1/src/in_layers/core/layers/__init__.py +10 -0
- in_layers_core-0.1.1/src/in_layers/core/layers/features.py +270 -0
- in_layers_core-0.1.1/src/in_layers/core/layers/services.py +25 -0
- in_layers_core-0.1.1/src/in_layers/core/libs.py +213 -0
- in_layers_core-0.1.1/src/in_layers/core/protocols.py +377 -0
- in_layers_core-0.1.1/src/in_layers/core/utils.py +40 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: in-layers-core
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: The Python port of Node in Layers Core.
|
|
5
|
+
License: GPLv3
|
|
6
|
+
Keywords: layers,python,in-layers,node,domains
|
|
7
|
+
Author: Mike Cornwell
|
|
8
|
+
Author-email: mike@mikecornwell.com
|
|
9
|
+
Requires-Python: >=3.13
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: python-box (>=7,<9)
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# In Layers Core
|
|
18
|
+
|
|
19
|
+
Python port of the Node-in-Layers core framework.
|
|
20
|
+
Supports Domains, config and layers loading, and cross-layer logging.
|
|
21
|
+
|
|
22
|
+
Key points:
|
|
23
|
+
- Domains explicitly provided in config (no convention discovery)
|
|
24
|
+
- Layers are loaded in configured order (supports composite layers)
|
|
25
|
+
- Cross-layer logging with automatic id propagation and function wraps
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# In Layers Core
|
|
2
|
+
|
|
3
|
+
Python port of the Node-in-Layers core framework.
|
|
4
|
+
Supports Domains, config and layers loading, and cross-layer logging.
|
|
5
|
+
|
|
6
|
+
Key points:
|
|
7
|
+
- Domains explicitly provided in config (no convention discovery)
|
|
8
|
+
- Layers are loaded in configured order (supports composite layers)
|
|
9
|
+
- Cross-layer logging with automatic id propagation and function wraps
|
|
10
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "in-layers-core"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "The Python port of Node in Layers Core."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Mike Cornwell",email = "mike@mikecornwell.com"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "GPLv3"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
homepage = "https://github.com/node-in-layers/python-in-layers-core"
|
|
11
|
+
repository = "https://github.com/node-in-layers/python-in-layers-core"
|
|
12
|
+
keywords = ["layers", "python", "in-layers", "node", "domains"]
|
|
13
|
+
requires-python = ">=3.13"
|
|
14
|
+
dependencies = [
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.poetry]
|
|
18
|
+
name = "in-layers-core"
|
|
19
|
+
version = "0.1.0"
|
|
20
|
+
description = ""
|
|
21
|
+
authors = []
|
|
22
|
+
packages = [
|
|
23
|
+
{ include = "in_layers", from = "src" }
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
30
|
+
build-backend = "poetry.core.masonry.api"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"black>=24.0.0",
|
|
35
|
+
"ruff>=0.7.0",
|
|
36
|
+
"isort>=5.12.0",
|
|
37
|
+
"mypy>=1.12.0",
|
|
38
|
+
"pytest>=8.0.0",
|
|
39
|
+
"commitizen (>=3.13,<4.0)",
|
|
40
|
+
"pytest-cov (>=7.0.0,<8.0.0)",
|
|
41
|
+
"build (>=1.3.0,<2.0.0)",
|
|
42
|
+
"twine (>=6.2.0,<7.0.0)",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[tool.poetry.dependencies]
|
|
46
|
+
python = ">=3.13"
|
|
47
|
+
python-box = ">=7,<9"
|
|
48
|
+
|
|
49
|
+
[tool.isort]
|
|
50
|
+
profile = "black"
|
|
51
|
+
multi_line_output = 3
|
|
52
|
+
include_trailing_comma = true
|
|
53
|
+
force_grid_wrap = 0
|
|
54
|
+
lines_after_imports = 2
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ================================================================
|
|
58
|
+
# BLACK (code formatting)
|
|
59
|
+
# ================================================================
|
|
60
|
+
[tool.black]
|
|
61
|
+
line-length = 88
|
|
62
|
+
target-version = ["py313"]
|
|
63
|
+
include = '\.pyi?$'
|
|
64
|
+
exclude = '''
|
|
65
|
+
/(
|
|
66
|
+
\.git
|
|
67
|
+
| \.venv
|
|
68
|
+
| venv
|
|
69
|
+
| build
|
|
70
|
+
| dist
|
|
71
|
+
| __pycache__
|
|
72
|
+
| \.mypy_cache
|
|
73
|
+
| \.ruff_cache
|
|
74
|
+
)/
|
|
75
|
+
'''
|
|
76
|
+
|
|
77
|
+
# ================================================================
|
|
78
|
+
# RUFF (linting + import sorting + auto-fixes)
|
|
79
|
+
# ================================================================
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
target-version = "py313"
|
|
82
|
+
line-length = 88
|
|
83
|
+
# Run Ruff from the root; it will traverse all subdirs except excluded.
|
|
84
|
+
exclude = [
|
|
85
|
+
".git",
|
|
86
|
+
".venv",
|
|
87
|
+
"venv",
|
|
88
|
+
"__pycache__",
|
|
89
|
+
"build",
|
|
90
|
+
"dist",
|
|
91
|
+
".mypy_cache",
|
|
92
|
+
".ruff_cache",
|
|
93
|
+
"tests",
|
|
94
|
+
"*/tests",
|
|
95
|
+
"**/tests"
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[tool.ruff.lint]
|
|
99
|
+
select = [
|
|
100
|
+
"E", # pycodestyle errors
|
|
101
|
+
"W", # pycodestyle warnings
|
|
102
|
+
"F", # Pyflakes
|
|
103
|
+
"B", # flake8-bugbear (common bugs / bad patterns)
|
|
104
|
+
"I", # isort (import sorting)
|
|
105
|
+
"UP", # pyupgrade (modern syntax)
|
|
106
|
+
"N", # pep8-naming (function, class, etc. naming)
|
|
107
|
+
"C90", # mccabe (complexity)
|
|
108
|
+
"SIM", # flake8-simplify
|
|
109
|
+
"S", # flake8-bandit (basic security checks)
|
|
110
|
+
"DTZ", # flake8-datetimez (timezone-aware datetimes)
|
|
111
|
+
"T20", # flake8-print (no stray prints in non-test code)
|
|
112
|
+
"ARG", # flake8-unused-arguments
|
|
113
|
+
"PL", # pylint-like checks (subset)
|
|
114
|
+
"RUF", # Ruff-specific rules
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
# Rules we choose to ignore or relax:
|
|
118
|
+
ignore = [
|
|
119
|
+
"E501", # Let Black handle line length
|
|
120
|
+
"W291", # Trailing whitespace (Black fixes this)
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
# Per-file ignore examples (customize as desired):
|
|
124
|
+
[tool.ruff.lint.per-file-ignores]
|
|
125
|
+
"**/tests/**" = [
|
|
126
|
+
"S101", # Allow 'assert' in tests
|
|
127
|
+
"T201", # Allow 'print' in tests if you wish
|
|
128
|
+
]
|
|
129
|
+
"orchestration/scripts/**" = [
|
|
130
|
+
"T201", # Allow print in CLI-style scripts
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
[tool.ruff.lint.mccabe]
|
|
134
|
+
max-complexity = 10
|
|
135
|
+
|
|
136
|
+
# Optionally enable Ruff as a formatter (complementing Black):
|
|
137
|
+
[tool.ruff.format]
|
|
138
|
+
quote-style = "double"
|
|
139
|
+
indent-style = "space"
|
|
140
|
+
line-ending = "auto"
|
|
141
|
+
|
|
142
|
+
# -------------------------------------------------------------------
|
|
143
|
+
# Mypy configuration (type checking / "code lensing")
|
|
144
|
+
# -------------------------------------------------------------------
|
|
145
|
+
[tool.mypy]
|
|
146
|
+
ignore_missing_imports = true
|
|
147
|
+
warn_unused_ignores = true
|
|
148
|
+
warn_redundant_casts = true
|
|
149
|
+
warn_unreachable = true
|
|
150
|
+
disallow_untyped_defs = true # Functions must be typed
|
|
151
|
+
disallow_incomplete_defs = true # Arguments & returns must be fully typed
|
|
152
|
+
disallow_untyped_calls = false # Relaxed for now; can tighten later
|
|
153
|
+
disallow_untyped_decorators = false
|
|
154
|
+
no_implicit_optional = true
|
|
155
|
+
strict_equality = true
|
|
156
|
+
show_error_codes = true
|
|
157
|
+
pretty = true
|
|
158
|
+
exclude = '''
|
|
159
|
+
/(
|
|
160
|
+
\.git
|
|
161
|
+
| \.venv
|
|
162
|
+
| build
|
|
163
|
+
| dist
|
|
164
|
+
| __pycache__
|
|
165
|
+
| \.mypy_cache
|
|
166
|
+
| tmp
|
|
167
|
+
)/
|
|
168
|
+
'''
|
|
169
|
+
|
|
170
|
+
# Optional: per-module/per-package overrides
|
|
171
|
+
[[tool.mypy.overrides]]
|
|
172
|
+
module = "tests.*"
|
|
173
|
+
ignore_errors = true
|
|
174
|
+
|
|
175
|
+
# -------------------------------------------------------------------
|
|
176
|
+
# Pytest configuration (unit tests)
|
|
177
|
+
# -------------------------------------------------------------------
|
|
178
|
+
[tool.pytest.ini_options]
|
|
179
|
+
minversion = "8.0"
|
|
180
|
+
addopts = "-ra -q"
|
|
181
|
+
python_files = [
|
|
182
|
+
"test_*.py",
|
|
183
|
+
"*_test.py",
|
|
184
|
+
"test_*.py",
|
|
185
|
+
"*-test.py",
|
|
186
|
+
]
|
|
187
|
+
[tool.commitizen]
|
|
188
|
+
name = "cz_conventional_commits"
|
|
189
|
+
tag_format = "$version"
|
|
190
|
+
version_scheme = "pep440"
|
|
191
|
+
version_provider = "pep621"
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from .entries import load_system
|
|
2
|
+
from .globals.libs import ( # noqa: F401
|
|
3
|
+
extract_cross_layer_props,
|
|
4
|
+
)
|
|
5
|
+
from .globals.logging import (
|
|
6
|
+
composite_logger,
|
|
7
|
+
console_log_full,
|
|
8
|
+
console_log_json,
|
|
9
|
+
console_log_simple,
|
|
10
|
+
log_tcp,
|
|
11
|
+
standard_logger,
|
|
12
|
+
)
|
|
13
|
+
from .libs import ( # noqa: F401
|
|
14
|
+
combine_cross_layer_props,
|
|
15
|
+
create_error_object,
|
|
16
|
+
get_layers_unavailable,
|
|
17
|
+
get_log_level_name,
|
|
18
|
+
get_log_level_number,
|
|
19
|
+
is_config,
|
|
20
|
+
validate_config,
|
|
21
|
+
)
|
|
22
|
+
from .protocols import ( # noqa: F401
|
|
23
|
+
App,
|
|
24
|
+
AppLayer,
|
|
25
|
+
CommonContext,
|
|
26
|
+
Config,
|
|
27
|
+
CoreConfig,
|
|
28
|
+
CoreNamespace,
|
|
29
|
+
CrossLayerProps,
|
|
30
|
+
FeaturesContext,
|
|
31
|
+
FunctionLogger,
|
|
32
|
+
HighLevelLogger,
|
|
33
|
+
LayerContext,
|
|
34
|
+
LayerDescription,
|
|
35
|
+
LayerLogger,
|
|
36
|
+
LogFormat,
|
|
37
|
+
Logger,
|
|
38
|
+
LogId,
|
|
39
|
+
LogLevel,
|
|
40
|
+
LogLevelNames,
|
|
41
|
+
LogMessage,
|
|
42
|
+
LogMethod,
|
|
43
|
+
RootLogger,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"composite_logger",
|
|
48
|
+
"console_log_full",
|
|
49
|
+
"console_log_json",
|
|
50
|
+
"console_log_simple",
|
|
51
|
+
"load_system",
|
|
52
|
+
"log_tcp",
|
|
53
|
+
"standard_logger",
|
|
54
|
+
]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from box import Box
|
|
8
|
+
|
|
9
|
+
from .globals import features as globals_features
|
|
10
|
+
from .globals import name as globals_name
|
|
11
|
+
from .globals import services as globals_services
|
|
12
|
+
from .layers import features as layers_features
|
|
13
|
+
from .layers import name as layers_name
|
|
14
|
+
from .layers import services as layers_services
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def load_system(args: Mapping[str, Any]):
|
|
18
|
+
environment: str = args["environment"]
|
|
19
|
+
config: Mapping[str, Any] | None = args.get("config")
|
|
20
|
+
|
|
21
|
+
global_services = globals_services.create(
|
|
22
|
+
{
|
|
23
|
+
"environment": environment,
|
|
24
|
+
"working_directory": os.getcwd(),
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
global_features = globals_features.create(
|
|
28
|
+
Box(
|
|
29
|
+
{
|
|
30
|
+
"services": {
|
|
31
|
+
globals_name: global_services,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
globals_context = await global_features.load_globals(config or environment)
|
|
37
|
+
|
|
38
|
+
# layers
|
|
39
|
+
|
|
40
|
+
the_layers_services = layers_services.create()
|
|
41
|
+
the_layers_features = layers_features.create(
|
|
42
|
+
Box(
|
|
43
|
+
{
|
|
44
|
+
**globals_context,
|
|
45
|
+
"services": {
|
|
46
|
+
layers_name: the_layers_services,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
layers_loaded = await the_layers_features.load_layers()
|
|
52
|
+
try:
|
|
53
|
+
if "services" in layers_loaded and layers_name in layers_loaded.services:
|
|
54
|
+
del layers_loaded.services[layers_name]
|
|
55
|
+
except Exception: # noqa: S110
|
|
56
|
+
pass
|
|
57
|
+
return layers_loaded
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from ..libs import is_config, validate_config
|
|
6
|
+
from ..protocols import CommonContext, CoreNamespace, FeaturesContext
|
|
7
|
+
|
|
8
|
+
globals_name = CoreNamespace.globals.value
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GlobalsFeatures:
|
|
12
|
+
def __init__(self, context: FeaturesContext):
|
|
13
|
+
self.context = context
|
|
14
|
+
|
|
15
|
+
async def load_globals(self, environment_or_config: Any):
|
|
16
|
+
services = self.context.services[globals_name]
|
|
17
|
+
if not services:
|
|
18
|
+
raise RuntimeError(f"Services for {globals_name} not found")
|
|
19
|
+
config = (
|
|
20
|
+
environment_or_config
|
|
21
|
+
if is_config(environment_or_config)
|
|
22
|
+
else services.load_config()
|
|
23
|
+
)
|
|
24
|
+
validate_config(config)
|
|
25
|
+
common_globals: CommonContext = {
|
|
26
|
+
"config": config,
|
|
27
|
+
"root_logger": services.get_root_logger(),
|
|
28
|
+
"constants": services.get_constants(),
|
|
29
|
+
}
|
|
30
|
+
return common_globals
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def create(context: FeaturesContext) -> GlobalsFeatures:
|
|
34
|
+
return GlobalsFeatures(context)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..libs import combine_cross_layer_props
|
|
8
|
+
from ..protocols import CrossLayerProps, Logger, LogLevelNames
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def default_get_function_wrap_log_level(layer_name: str) -> LogLevelNames:
|
|
12
|
+
if layer_name in ("features", "entries"):
|
|
13
|
+
return LogLevelNames.info
|
|
14
|
+
if layer_name == "services":
|
|
15
|
+
return LogLevelNames.trace
|
|
16
|
+
return LogLevelNames.debug
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def combine_logging_props(
|
|
20
|
+
logger: Logger, cross_layer_props: CrossLayerProps | None = None
|
|
21
|
+
) -> Mapping[str, Any]:
|
|
22
|
+
|
|
23
|
+
base: CrossLayerProps = {"logging": {"ids": logger.get_ids()}}
|
|
24
|
+
final = combine_cross_layer_props(base, cross_layer_props or {}) # type: ignore[arg-type]
|
|
25
|
+
return final["logging"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_cross_layer_logging_props(maybe: CrossLayerProps | None) -> bool:
|
|
29
|
+
return bool(maybe and isinstance(maybe, Mapping) and "logging" in maybe and isinstance(maybe.get("logging"), Mapping) and isinstance(maybe["logging"].get("ids"), list)) # type: ignore[index]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cap_for_logging(input: Any, max_size: int = 50000) -> Any:
|
|
33
|
+
def safe_stringify(obj: Any) -> str:
|
|
34
|
+
try:
|
|
35
|
+
return json.dumps(obj, default=str)
|
|
36
|
+
except Exception:
|
|
37
|
+
return "[Unserializable]"
|
|
38
|
+
|
|
39
|
+
if not isinstance(input, (list, dict)):
|
|
40
|
+
return input
|
|
41
|
+
if len(safe_stringify(input)) <= max_size:
|
|
42
|
+
return input
|
|
43
|
+
if isinstance(input, list):
|
|
44
|
+
build: list[Any] = []
|
|
45
|
+
for item in input:
|
|
46
|
+
candidate = [*build, item]
|
|
47
|
+
if (
|
|
48
|
+
len(
|
|
49
|
+
safe_stringify(
|
|
50
|
+
[*candidate, f"[truncated, original length: {len(input)}]"]
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
> max_size
|
|
54
|
+
):
|
|
55
|
+
return [*build, f"[truncated, original length: {len(input)}]"]
|
|
56
|
+
build.append(item)
|
|
57
|
+
return build
|
|
58
|
+
keys = list(input.keys())
|
|
59
|
+
out: dict[str, Any] = {}
|
|
60
|
+
for k in keys:
|
|
61
|
+
out[k] = input[k]
|
|
62
|
+
truncated = dict(out)
|
|
63
|
+
truncated["[truncated]"] = f"original keys: {len(keys)}"
|
|
64
|
+
if len(safe_stringify(truncated)) > max_size:
|
|
65
|
+
return truncated
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def extract_cross_layer_props(
|
|
70
|
+
args: list[Any],
|
|
71
|
+
) -> tuple[list[Any], CrossLayerProps | None]:
|
|
72
|
+
if not args:
|
|
73
|
+
return [], None
|
|
74
|
+
last = args[-1]
|
|
75
|
+
if is_cross_layer_logging_props(last): # type: ignore[arg-type]
|
|
76
|
+
return args[:-1], last # type: ignore[return-value]
|
|
77
|
+
return args, None
|