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.
@@ -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,10 @@
1
+ from __future__ import annotations
2
+
3
+ from ..protocols import CoreNamespace
4
+ from . import (
5
+ features,
6
+ services,
7
+ )
8
+
9
+ name = CoreNamespace.globals.value
10
+ __all__ = ["features", "name", "services"]
@@ -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