apcore 0.1.0__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.
Files changed (58) hide show
  1. apcore-0.1.0/PKG-INFO +8 -0
  2. apcore-0.1.0/README.md +161 -0
  3. apcore-0.1.0/pyproject.toml +38 -0
  4. apcore-0.1.0/setup.cfg +4 -0
  5. apcore-0.1.0/src/apcore/__init__.py +116 -0
  6. apcore-0.1.0/src/apcore/acl.py +269 -0
  7. apcore-0.1.0/src/apcore/bindings.py +252 -0
  8. apcore-0.1.0/src/apcore/config.py +29 -0
  9. apcore-0.1.0/src/apcore/context.py +60 -0
  10. apcore-0.1.0/src/apcore/decorator.py +264 -0
  11. apcore-0.1.0/src/apcore/errors.py +380 -0
  12. apcore-0.1.0/src/apcore/executor.py +620 -0
  13. apcore-0.1.0/src/apcore/middleware/__init__.py +15 -0
  14. apcore-0.1.0/src/apcore/middleware/adapters.py +39 -0
  15. apcore-0.1.0/src/apcore/middleware/base.py +31 -0
  16. apcore-0.1.0/src/apcore/middleware/logging.py +88 -0
  17. apcore-0.1.0/src/apcore/middleware/manager.py +110 -0
  18. apcore-0.1.0/src/apcore/module.py +58 -0
  19. apcore-0.1.0/src/apcore/observability/__init__.py +37 -0
  20. apcore-0.1.0/src/apcore/observability/context_logger.py +169 -0
  21. apcore-0.1.0/src/apcore/observability/metrics.py +175 -0
  22. apcore-0.1.0/src/apcore/observability/tracing.py +261 -0
  23. apcore-0.1.0/src/apcore/registry/__init__.py +36 -0
  24. apcore-0.1.0/src/apcore/registry/dependencies.py +113 -0
  25. apcore-0.1.0/src/apcore/registry/entry_point.py +89 -0
  26. apcore-0.1.0/src/apcore/registry/metadata.py +116 -0
  27. apcore-0.1.0/src/apcore/registry/registry.py +388 -0
  28. apcore-0.1.0/src/apcore/registry/scanner.py +147 -0
  29. apcore-0.1.0/src/apcore/registry/schema_export.py +189 -0
  30. apcore-0.1.0/src/apcore/registry/types.py +51 -0
  31. apcore-0.1.0/src/apcore/registry/validation.py +46 -0
  32. apcore-0.1.0/src/apcore/schema/__init__.py +45 -0
  33. apcore-0.1.0/src/apcore/schema/annotations.py +62 -0
  34. apcore-0.1.0/src/apcore/schema/exporter.py +95 -0
  35. apcore-0.1.0/src/apcore/schema/loader.py +372 -0
  36. apcore-0.1.0/src/apcore/schema/ref_resolver.py +200 -0
  37. apcore-0.1.0/src/apcore/schema/strict.py +105 -0
  38. apcore-0.1.0/src/apcore/schema/types.py +109 -0
  39. apcore-0.1.0/src/apcore/schema/validator.py +102 -0
  40. apcore-0.1.0/src/apcore/utils/__init__.py +5 -0
  41. apcore-0.1.0/src/apcore/utils/pattern.py +46 -0
  42. apcore-0.1.0/src/apcore.egg-info/PKG-INFO +8 -0
  43. apcore-0.1.0/src/apcore.egg-info/SOURCES.txt +56 -0
  44. apcore-0.1.0/src/apcore.egg-info/dependency_links.txt +1 -0
  45. apcore-0.1.0/src/apcore.egg-info/requires.txt +3 -0
  46. apcore-0.1.0/src/apcore.egg-info/top_level.txt +1 -0
  47. apcore-0.1.0/tests/test_acl.py +541 -0
  48. apcore-0.1.0/tests/test_bindings.py +490 -0
  49. apcore-0.1.0/tests/test_decorator.py +1217 -0
  50. apcore-0.1.0/tests/test_executor.py +559 -0
  51. apcore-0.1.0/tests/test_executor_async.py +233 -0
  52. apcore-0.1.0/tests/test_executor_types.py +71 -0
  53. apcore-0.1.0/tests/test_integration_executor.py +297 -0
  54. apcore-0.1.0/tests/test_logging_middleware.py +251 -0
  55. apcore-0.1.0/tests/test_middleware.py +163 -0
  56. apcore-0.1.0/tests/test_middleware_manager.py +382 -0
  57. apcore-0.1.0/tests/test_public_api.py +288 -0
  58. apcore-0.1.0/tests/test_redaction.py +173 -0
apcore-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: apcore
3
+ Version: 0.1.0
4
+ Summary: Schema-driven module development framework for AI-perceivable interfaces
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: pydantic>=2.0
7
+ Requires-Dist: pyyaml>=6.0
8
+ Requires-Dist: pluggy>=1.0
apcore-0.1.0/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # apcore
2
+
3
+ Schema-driven module development framework for AI-perceivable interfaces.
4
+
5
+ **apcore** provides a unified task orchestration framework with strict type safety, access control, middleware pipelines, and built-in observability. It enables you to define modules with structured input/output schemas that are easily consumed by LLMs and other automated systems.
6
+
7
+ ## Features
8
+
9
+ - **Schema-driven modules** -- Define input/output contracts using Pydantic models with automatic validation
10
+ - **10-step execution pipeline** -- Context creation, safety checks, ACL enforcement, validation, middleware chains, and execution with timeout support
11
+ - **`@module` decorator** -- Turn plain functions into fully schema-aware modules with zero boilerplate
12
+ - **YAML bindings** -- Register modules declaratively without modifying source code
13
+ - **Access control (ACL)** -- Pattern-based, first-match-wins rules with wildcard support
14
+ - **Middleware system** -- Composable before/after hooks with error recovery
15
+ - **Observability** -- Tracing (spans), metrics collection, and structured context logging
16
+ - **Async support** -- Seamless sync and async module execution
17
+ - **Safety guards** -- Call depth limits, circular call detection, frequency throttling
18
+
19
+ ## Requirements
20
+
21
+ - Python >= 3.11
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install -e .
27
+ ```
28
+
29
+ For development:
30
+
31
+ ```bash
32
+ pip install -e ".[dev]"
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### Define a module with the decorator
38
+
39
+ ```python
40
+ from apcore import module
41
+
42
+ @module(description="Add two integers", tags=["math"])
43
+ def add(a: int, b: int) -> int:
44
+ return a + b
45
+ ```
46
+
47
+ ### Define a module with a class
48
+
49
+ ```python
50
+ from pydantic import BaseModel
51
+ from apcore import Context
52
+
53
+ class GreetInput(BaseModel):
54
+ name: str
55
+
56
+ class GreetOutput(BaseModel):
57
+ message: str
58
+
59
+ class GreetModule:
60
+ input_schema = GreetInput
61
+ output_schema = GreetOutput
62
+ description = "Greet a user"
63
+
64
+ def execute(self, inputs: dict, context: Context) -> dict:
65
+ return {"message": f"Hello, {inputs['name']}!"}
66
+ ```
67
+
68
+ ### Register and execute
69
+
70
+ ```python
71
+ from apcore import Registry, Executor
72
+
73
+ registry = Registry()
74
+ registry.register("greet", GreetModule())
75
+
76
+ executor = Executor(registry=registry)
77
+ result = executor.call("greet", {"name": "Alice"})
78
+ # {"message": "Hello, Alice!"}
79
+ ```
80
+
81
+ ### Add middleware
82
+
83
+ ```python
84
+ from apcore import LoggingMiddleware, TracingMiddleware
85
+
86
+ executor.use(LoggingMiddleware())
87
+ executor.use(TracingMiddleware())
88
+ ```
89
+
90
+ ### Access control
91
+
92
+ ```python
93
+ from apcore import ACL, ACLRule
94
+
95
+ acl = ACL(rules=[
96
+ ACLRule(callers=["admin.*"], targets=["*"], effect="allow", description="Admins can call anything"),
97
+ ACLRule(callers=["*"], targets=["admin.*"], effect="deny", description="Others cannot call admin modules"),
98
+ ])
99
+ executor = Executor(registry=registry, acl=acl)
100
+ ```
101
+
102
+ ## Project Structure
103
+
104
+ ```
105
+ src/apcore/
106
+ __init__.py # Public API
107
+ context.py # Execution context & identity
108
+ executor.py # Core execution engine
109
+ decorator.py # @module decorator
110
+ bindings.py # YAML binding loader
111
+ config.py # Configuration
112
+ acl.py # Access control
113
+ errors.py # Error hierarchy
114
+ module.py # Module annotations & metadata
115
+ middleware/ # Middleware system
116
+ observability/ # Tracing, metrics, logging
117
+ registry/ # Module discovery & registration
118
+ schema/ # Schema loading, validation, export
119
+ utils/ # Utilities
120
+ ```
121
+
122
+ ## Development
123
+
124
+ ### Run tests
125
+
126
+ ```bash
127
+ pytest
128
+ ```
129
+
130
+ ### Run tests with coverage
131
+
132
+ ```bash
133
+ pytest --cov=src/apcore --cov-report=html
134
+ ```
135
+
136
+ ### Lint and format
137
+
138
+ ```bash
139
+ ruff check --fix src/ tests/
140
+ ruff format src/ tests/
141
+ ```
142
+
143
+ ### Type check
144
+
145
+ ```bash
146
+ mypy src/ tests/
147
+ ```
148
+
149
+
150
+ ## 📄 License
151
+
152
+ Apache-2.0
153
+
154
+ ## 🔗 Links
155
+
156
+ - **Documentation**: [docs/apcore](https://github.com/aipartnerup/apcore) - Complete documentation
157
+ - **Website**: [aipartnerup.com](https://aipartnerup.com)
158
+ - **GitHub**: [aipartnerup/apcore](https://github.com/aipartnerup/apcore)
159
+ - **PyPI**: [apcore](https://pypi.org/project/apcore/)
160
+ - **Issues**: [GitHub Issues](https://github.com/aipartnerup/apcore/issues)
161
+ - **Discussions**: [GitHub Discussions](https://github.com/aipartnerup/apcore/discussions)
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "apcore"
7
+ version = "0.1.0"
8
+ description = "Schema-driven module development framework for AI-perceivable interfaces"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "pydantic>=2.0",
12
+ "pyyaml>=6.0",
13
+ "pluggy>=1.0",
14
+ ]
15
+
16
+ [dependency-groups]
17
+ dev = [
18
+ "pytest>=7.0",
19
+ "pytest-asyncio>=0.21",
20
+ "pytest-cov>=4.0",
21
+ "ruff>=0.1.0",
22
+ "mypy>=1.0",
23
+ ]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
27
+
28
+ [tool.ruff]
29
+ line-length = 120
30
+ target-version = "py311"
31
+
32
+ [tool.mypy]
33
+ python_version = "3.11"
34
+ strict = true
35
+
36
+ [tool.pytest.ini_options]
37
+ asyncio_mode = "auto"
38
+ testpaths = ["tests"]
apcore-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,116 @@
1
+ """apcore - Schema-driven module development framework."""
2
+
3
+ from __future__ import annotations
4
+
5
+ # Core
6
+ from apcore.context import Context, Identity
7
+ from apcore.registry import Registry
8
+ from apcore.registry.types import ModuleDescriptor
9
+ from apcore.executor import Executor, redact_sensitive, REDACTED_VALUE
10
+
11
+ # Module types
12
+ from apcore.module import ModuleAnnotations, ModuleExample, ValidationResult
13
+
14
+ # Config
15
+ from apcore.config import Config
16
+
17
+ # Errors
18
+ from apcore.errors import (
19
+ ACLDeniedError,
20
+ CallDepthExceededError,
21
+ CallFrequencyExceededError,
22
+ CircularCallError,
23
+ CircularDependencyError,
24
+ ConfigError,
25
+ InvalidInputError,
26
+ ModuleError,
27
+ ModuleNotFoundError,
28
+ ModuleTimeoutError,
29
+ SchemaValidationError,
30
+ )
31
+
32
+ # ACL
33
+ from apcore.acl import ACL, ACLRule
34
+
35
+ # Middleware
36
+ from apcore.middleware import (
37
+ AfterMiddleware,
38
+ BeforeMiddleware,
39
+ LoggingMiddleware,
40
+ Middleware,
41
+ MiddlewareManager,
42
+ )
43
+
44
+ # Decorators
45
+ from apcore.decorator import FunctionModule, module
46
+
47
+ # Bindings
48
+ from apcore.bindings import BindingLoader
49
+
50
+ # Observability
51
+ from apcore.observability import (
52
+ ContextLogger,
53
+ InMemoryExporter,
54
+ MetricsCollector,
55
+ MetricsMiddleware,
56
+ ObsLoggingMiddleware,
57
+ Span,
58
+ StdoutExporter,
59
+ TracingMiddleware,
60
+ )
61
+
62
+ __version__ = "0.1.0"
63
+
64
+ __all__ = [
65
+ # Core
66
+ "Context",
67
+ "Identity",
68
+ "Registry",
69
+ "Executor",
70
+ # Module types
71
+ "ModuleAnnotations",
72
+ "ModuleExample",
73
+ "ValidationResult",
74
+ # Registry types
75
+ "ModuleDescriptor",
76
+ # Config
77
+ "Config",
78
+ # Errors
79
+ "ModuleError",
80
+ "SchemaValidationError",
81
+ "ACLDeniedError",
82
+ "ModuleNotFoundError",
83
+ "ConfigError",
84
+ "CircularDependencyError",
85
+ "InvalidInputError",
86
+ "ModuleTimeoutError",
87
+ "CallDepthExceededError",
88
+ "CircularCallError",
89
+ "CallFrequencyExceededError",
90
+ # ACL
91
+ "ACL",
92
+ "ACLRule",
93
+ # Middleware
94
+ "Middleware",
95
+ "MiddlewareManager",
96
+ "BeforeMiddleware",
97
+ "AfterMiddleware",
98
+ "LoggingMiddleware",
99
+ # Decorators
100
+ "module",
101
+ "FunctionModule",
102
+ # Bindings
103
+ "BindingLoader",
104
+ # Utilities
105
+ "redact_sensitive",
106
+ "REDACTED_VALUE",
107
+ # Observability
108
+ "TracingMiddleware",
109
+ "ContextLogger",
110
+ "ObsLoggingMiddleware",
111
+ "MetricsMiddleware",
112
+ "MetricsCollector",
113
+ "Span",
114
+ "StdoutExporter",
115
+ "InMemoryExporter",
116
+ ]
@@ -0,0 +1,269 @@
1
+ """ACL (Access Control List) types and implementation for apcore.
2
+
3
+ This module defines the ACLRule dataclass and the ACL class that enforces
4
+ pattern-based access control between modules.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ from dataclasses import dataclass
12
+ from typing import Any
13
+
14
+ import yaml
15
+
16
+ from apcore.context import Context
17
+ from apcore.errors import ACLRuleError, ConfigNotFoundError
18
+ from apcore.utils.pattern import match_pattern
19
+
20
+ __all__ = ["ACLRule", "ACL"]
21
+
22
+
23
+ @dataclass
24
+ class ACLRule:
25
+ """A single access control rule.
26
+
27
+ Rules are evaluated in order by the ACL system. Each rule specifies
28
+ caller patterns, target patterns, and an effect (allow/deny).
29
+ """
30
+
31
+ callers: list[str]
32
+ targets: list[str]
33
+ effect: str
34
+ description: str = ""
35
+ conditions: dict[str, Any] | None = None
36
+
37
+
38
+ class ACL:
39
+ """Access Control List with pattern-based rules and first-match-wins evaluation.
40
+
41
+ Implements PROTOCOL_SPEC section 6 for module access control.
42
+
43
+ Thread safety:
44
+ - check() is read-only and fully thread-safe for concurrent calls.
45
+ - add_rule(), remove_rule(), and reload() mutate the rules list and
46
+ require external locking if called concurrently with check().
47
+ """
48
+
49
+ def __init__(self, rules: list[ACLRule], default_effect: str = "deny") -> None:
50
+ """Initialize ACL with ordered rules and a default effect.
51
+
52
+ Args:
53
+ rules: Ordered list of ACL rules (first match wins).
54
+ default_effect: Effect when no rule matches ('allow' or 'deny').
55
+ """
56
+ self._rules: list[ACLRule] = list(rules)
57
+ self._default_effect: str = default_effect
58
+ self._yaml_path: str | None = None
59
+ self.debug: bool = False
60
+ self._logger: logging.Logger = logging.getLogger("apcore.acl")
61
+
62
+ @classmethod
63
+ def load(cls, yaml_path: str) -> ACL:
64
+ """Load ACL configuration from a YAML file.
65
+
66
+ Args:
67
+ yaml_path: Path to the YAML configuration file.
68
+
69
+ Returns:
70
+ A new ACL instance configured from the YAML file.
71
+
72
+ Raises:
73
+ ConfigNotFoundError: If the file does not exist.
74
+ ACLRuleError: If the YAML is invalid or has structural errors.
75
+ """
76
+ if not os.path.isfile(yaml_path):
77
+ raise ConfigNotFoundError(config_path=yaml_path)
78
+
79
+ with open(yaml_path) as f:
80
+ try:
81
+ data = yaml.safe_load(f)
82
+ except yaml.YAMLError as e:
83
+ raise ACLRuleError(f"Invalid YAML in {yaml_path}: {e}") from e
84
+
85
+ if not isinstance(data, dict):
86
+ raise ACLRuleError(f"ACL config must be a mapping, got {type(data).__name__}")
87
+
88
+ if "rules" not in data:
89
+ raise ACLRuleError("ACL config missing required 'rules' key")
90
+
91
+ raw_rules = data["rules"]
92
+ if not isinstance(raw_rules, list):
93
+ raise ACLRuleError(f"'rules' must be a list, got {type(raw_rules).__name__}")
94
+
95
+ default_effect: str = data.get("default_effect", "deny")
96
+ rules: list[ACLRule] = []
97
+
98
+ for i, raw_rule in enumerate(raw_rules):
99
+ if not isinstance(raw_rule, dict):
100
+ raise ACLRuleError(f"Rule {i} must be a mapping, got {type(raw_rule).__name__}")
101
+
102
+ for key in ("callers", "targets", "effect"):
103
+ if key not in raw_rule:
104
+ raise ACLRuleError(f"Rule {i} missing required key '{key}'")
105
+
106
+ effect = raw_rule["effect"]
107
+ if effect not in ("allow", "deny"):
108
+ raise ACLRuleError(f"Rule {i} has invalid effect '{effect}', must be 'allow' or 'deny'")
109
+
110
+ callers = raw_rule["callers"]
111
+ if not isinstance(callers, list):
112
+ raise ACLRuleError(f"Rule {i} 'callers' must be a list, got {type(callers).__name__}")
113
+
114
+ targets = raw_rule["targets"]
115
+ if not isinstance(targets, list):
116
+ raise ACLRuleError(f"Rule {i} 'targets' must be a list, got {type(targets).__name__}")
117
+
118
+ rules.append(
119
+ ACLRule(
120
+ callers=callers,
121
+ targets=targets,
122
+ effect=effect,
123
+ description=raw_rule.get("description", ""),
124
+ conditions=raw_rule.get("conditions"),
125
+ )
126
+ )
127
+
128
+ acl = cls(rules=rules, default_effect=default_effect)
129
+ acl._yaml_path = yaml_path
130
+ return acl
131
+
132
+ def check(
133
+ self,
134
+ caller_id: str | None,
135
+ target_id: str,
136
+ context: Context | None = None,
137
+ ) -> bool:
138
+ """Check if a call from caller_id to target_id is allowed.
139
+
140
+ Args:
141
+ caller_id: The calling module ID, or None for external calls.
142
+ target_id: The target module ID being called.
143
+ context: Optional execution context for conditional rules.
144
+
145
+ Returns:
146
+ True if the call is allowed, False if denied.
147
+ """
148
+ effective_caller = "@external" if caller_id is None else caller_id
149
+
150
+ for rule in self._rules:
151
+ if self._matches_rule(rule, effective_caller, target_id, context):
152
+ decision = rule.effect == "allow"
153
+ self._logger.debug(
154
+ "ACL check: caller=%s target=%s decision=%s rule=%s",
155
+ caller_id,
156
+ target_id,
157
+ "allow" if decision else "deny",
158
+ rule.description or "(no description)",
159
+ )
160
+ return decision
161
+
162
+ default_decision = self._default_effect == "allow"
163
+ self._logger.debug(
164
+ "ACL check: caller=%s target=%s decision=%s rule=default",
165
+ caller_id,
166
+ target_id,
167
+ "allow" if default_decision else "deny",
168
+ )
169
+ return default_decision
170
+
171
+ def _match_pattern(self, pattern: str, value: str, context: Context | None = None) -> bool:
172
+ """Match a single pattern against a value, with special pattern handling.
173
+
174
+ Handles @external and @system patterns locally, delegates all
175
+ other patterns to the foundation match_pattern() utility.
176
+ """
177
+ if pattern == "@external":
178
+ return value == "@external"
179
+ if pattern == "@system":
180
+ return context is not None and context.identity is not None and context.identity.type == "system"
181
+ return match_pattern(pattern, value)
182
+
183
+ def _matches_rule(
184
+ self,
185
+ rule: ACLRule,
186
+ caller: str,
187
+ target: str,
188
+ context: Context | None,
189
+ ) -> bool:
190
+ """Check if a single rule matches the caller and target.
191
+
192
+ All of the following must be true for a match:
193
+ 1. At least one caller pattern matches the caller (OR logic).
194
+ 2. At least one target pattern matches the target (OR logic).
195
+ 3. If conditions are present, they must all be satisfied.
196
+ """
197
+ caller_match = any(self._match_pattern(p, caller, context) for p in rule.callers)
198
+ if not caller_match:
199
+ return False
200
+
201
+ target_match = any(self._match_pattern(p, target, context) for p in rule.targets)
202
+ if not target_match:
203
+ return False
204
+
205
+ if rule.conditions is not None:
206
+ if not self._check_conditions(rule.conditions, context):
207
+ return False
208
+
209
+ return True
210
+
211
+ def _check_conditions(self, conditions: dict[str, Any], context: Context | None) -> bool:
212
+ """Evaluate conditional rule parameters against the execution context.
213
+
214
+ Returns False if any condition is not satisfied.
215
+ """
216
+ if context is None:
217
+ return False
218
+
219
+ if "identity_types" in conditions:
220
+ if context.identity is None or context.identity.type not in conditions["identity_types"]:
221
+ return False
222
+
223
+ if "roles" in conditions:
224
+ if context.identity is None:
225
+ return False
226
+ if not set(context.identity.roles) & set(conditions["roles"]):
227
+ return False
228
+
229
+ if "max_call_depth" in conditions:
230
+ if len(context.call_chain) > conditions["max_call_depth"]:
231
+ return False
232
+
233
+ return True
234
+
235
+ def add_rule(self, rule: ACLRule) -> None:
236
+ """Add a rule at position 0 (highest priority).
237
+
238
+ Args:
239
+ rule: The ACLRule to add.
240
+ """
241
+ self._rules.insert(0, rule)
242
+
243
+ def remove_rule(self, callers: list[str], targets: list[str]) -> bool:
244
+ """Remove the first rule matching the given callers and targets.
245
+
246
+ Args:
247
+ callers: The caller patterns to match.
248
+ targets: The target patterns to match.
249
+
250
+ Returns:
251
+ True if a rule was found and removed, False otherwise.
252
+ """
253
+ for i, rule in enumerate(self._rules):
254
+ if rule.callers == callers and rule.targets == targets:
255
+ self._rules.pop(i)
256
+ return True
257
+ return False
258
+
259
+ def reload(self) -> None:
260
+ """Re-read the ACL from the original YAML file.
261
+
262
+ Only works if the ACL was created via ACL.load().
263
+ Raises ACLRuleError if no YAML path was stored.
264
+ """
265
+ if self._yaml_path is None:
266
+ raise ACLRuleError("Cannot reload: ACL was not loaded from a YAML file")
267
+ reloaded = ACL.load(self._yaml_path)
268
+ self._rules = reloaded._rules
269
+ self._default_effect = reloaded._default_effect