framework-m-core 0.4.1__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 (87) hide show
  1. framework_m_core/__init__.py +29 -0
  2. framework_m_core/cli/__init__.py +0 -0
  3. framework_m_core/cli/config.py +128 -0
  4. framework_m_core/cli/main.py +30 -0
  5. framework_m_core/cli/plugin_loader.py +132 -0
  6. framework_m_core/cli/utility.py +58 -0
  7. framework_m_core/config.py +171 -0
  8. framework_m_core/container.py +329 -0
  9. framework_m_core/decorators.py +308 -0
  10. framework_m_core/doctypes/__init__.py +51 -0
  11. framework_m_core/doctypes/activity_log.py +123 -0
  12. framework_m_core/doctypes/api_key.py +89 -0
  13. framework_m_core/doctypes/custom_permission.py +118 -0
  14. framework_m_core/doctypes/document_share.py +142 -0
  15. framework_m_core/doctypes/email_queue.py +209 -0
  16. framework_m_core/doctypes/error_log.py +127 -0
  17. framework_m_core/doctypes/file.py +97 -0
  18. framework_m_core/doctypes/job_log.py +144 -0
  19. framework_m_core/doctypes/notification.py +146 -0
  20. framework_m_core/doctypes/print_format.py +124 -0
  21. framework_m_core/doctypes/recent_document.py +75 -0
  22. framework_m_core/doctypes/report.py +94 -0
  23. framework_m_core/doctypes/scheduled_job.py +97 -0
  24. framework_m_core/doctypes/session.py +71 -0
  25. framework_m_core/doctypes/social_account.py +88 -0
  26. framework_m_core/doctypes/system_settings.py +119 -0
  27. framework_m_core/doctypes/tenant_translation.py +133 -0
  28. framework_m_core/doctypes/todo.py +59 -0
  29. framework_m_core/doctypes/translation.py +107 -0
  30. framework_m_core/doctypes/user.py +115 -0
  31. framework_m_core/doctypes/webhook.py +127 -0
  32. framework_m_core/doctypes/webhook_log.py +114 -0
  33. framework_m_core/doctypes/workflow.py +51 -0
  34. framework_m_core/doctypes/workflow_state.py +56 -0
  35. framework_m_core/doctypes/workflow_transition.py +57 -0
  36. framework_m_core/domain/__init__.py +1 -0
  37. framework_m_core/domain/base_controller.py +224 -0
  38. framework_m_core/domain/base_doctype.py +287 -0
  39. framework_m_core/domain/mixins.py +131 -0
  40. framework_m_core/domain/naming_counter.py +50 -0
  41. framework_m_core/domain/outbox.py +85 -0
  42. framework_m_core/events/__init__.py +112 -0
  43. framework_m_core/exceptions.py +144 -0
  44. framework_m_core/interfaces/__init__.py +23 -0
  45. framework_m_core/interfaces/audit.py +216 -0
  46. framework_m_core/interfaces/auth_context.py +165 -0
  47. framework_m_core/interfaces/authentication.py +86 -0
  48. framework_m_core/interfaces/base_doctype.py +54 -0
  49. framework_m_core/interfaces/bootstrap.py +110 -0
  50. framework_m_core/interfaces/cache.py +140 -0
  51. framework_m_core/interfaces/controller.py +157 -0
  52. framework_m_core/interfaces/email_queue.py +187 -0
  53. framework_m_core/interfaces/email_sender.py +143 -0
  54. framework_m_core/interfaces/event_bus.py +167 -0
  55. framework_m_core/interfaces/i18n.py +89 -0
  56. framework_m_core/interfaces/identity.py +198 -0
  57. framework_m_core/interfaces/job_queue.py +154 -0
  58. framework_m_core/interfaces/notification.py +145 -0
  59. framework_m_core/interfaces/oauth.py +130 -0
  60. framework_m_core/interfaces/permission.py +141 -0
  61. framework_m_core/interfaces/print.py +85 -0
  62. framework_m_core/interfaces/read_model.py +142 -0
  63. framework_m_core/interfaces/report_engine.py +90 -0
  64. framework_m_core/interfaces/repository.py +228 -0
  65. framework_m_core/interfaces/schema_mapper.py +164 -0
  66. framework_m_core/interfaces/search.py +133 -0
  67. framework_m_core/interfaces/session.py +167 -0
  68. framework_m_core/interfaces/socket.py +99 -0
  69. framework_m_core/interfaces/storage.py +181 -0
  70. framework_m_core/interfaces/tenant.py +165 -0
  71. framework_m_core/interfaces/workflow.py +214 -0
  72. framework_m_core/permission_lookup.py +203 -0
  73. framework_m_core/permissions.py +228 -0
  74. framework_m_core/pii.py +215 -0
  75. framework_m_core/py.typed +0 -0
  76. framework_m_core/registry.py +312 -0
  77. framework_m_core/rls.py +201 -0
  78. framework_m_core/rpc_registry.py +90 -0
  79. framework_m_core/security.py +44 -0
  80. framework_m_core/services/__init__.py +12 -0
  81. framework_m_core/services/user_manager.py +187 -0
  82. framework_m_core/system_context.py +151 -0
  83. framework_m_core/types/job_context.py +94 -0
  84. framework_m_core/unit_of_work.py +138 -0
  85. framework_m_core-0.4.1.dist-info/METADATA +92 -0
  86. framework_m_core-0.4.1.dist-info/RECORD +87 -0
  87. framework_m_core-0.4.1.dist-info/WHEEL +4 -0
@@ -0,0 +1,29 @@
1
+ """Core module - Domain logic and interfaces."""
2
+
3
+ import importlib.metadata
4
+
5
+ from framework_m_core.container import Container
6
+ from framework_m_core.domain.base_doctype import BaseDocType as DocType
7
+ from framework_m_core.domain.base_doctype import Field
8
+ from framework_m_core.interfaces.base_doctype import BaseDocTypeProtocol
9
+ from framework_m_core.interfaces.bootstrap import BootstrapProtocol
10
+ from framework_m_core.interfaces.controller import BaseControllerProtocol
11
+ from framework_m_core.interfaces.schema_mapper import SchemaMapperProtocol
12
+ from framework_m_core.registry import MetaRegistry
13
+
14
+ try:
15
+ __version__ = importlib.metadata.version("framework-m-core")
16
+ except importlib.metadata.PackageNotFoundError:
17
+ __version__ = "0.0.0"
18
+
19
+ __all__ = [
20
+ "BaseControllerProtocol",
21
+ "BaseDocTypeProtocol",
22
+ "BootstrapProtocol",
23
+ "Container",
24
+ "DocType",
25
+ "Field",
26
+ "MetaRegistry",
27
+ "SchemaMapperProtocol",
28
+ "__version__",
29
+ ]
File without changes
@@ -0,0 +1,128 @@
1
+ """Configuration Management CLI Commands.
2
+
3
+ This module provides CLI commands for managing Framework M configuration:
4
+ - m config:show: Display current configuration
5
+ - m config:set <key> <value>: Update configuration value
6
+
7
+ Usage:
8
+ m config:show # Show all config
9
+ m config:set framework.name myapp
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+ from typing import Annotated, Any
16
+
17
+ import cyclopts
18
+
19
+ from framework_m_core.config import (
20
+ CONFIG_FILE_NAME,
21
+ DEFAULT_CONFIG,
22
+ find_config_file,
23
+ format_config,
24
+ get_nested_value,
25
+ load_config,
26
+ save_config,
27
+ set_nested_value,
28
+ )
29
+
30
+ # =============================================================================
31
+ # CLI Commands
32
+ # =============================================================================
33
+
34
+
35
+ def config_show_command(
36
+ key: Annotated[
37
+ str | None,
38
+ cyclopts.Parameter(help="Specific key to show (e.g., framework.name)"),
39
+ ] = None,
40
+ config_file: Annotated[
41
+ Path | None,
42
+ cyclopts.Parameter(name="--config", help="Path to config file"),
43
+ ] = None,
44
+ ) -> None:
45
+ """Display current configuration.
46
+
47
+ Shows the current Framework M configuration from framework_config.toml.
48
+
49
+ Examples:
50
+ m config:show # Show all config
51
+ m config:show framework.name # Show specific key
52
+ """
53
+ config_path = config_file or find_config_file()
54
+ config = load_config(config_path)
55
+
56
+ if not config:
57
+ print(f"No config found at: {config_path}")
58
+ print()
59
+ print("Create a config file with default values:")
60
+ print(" m config:set framework.name myapp")
61
+ return
62
+
63
+ print(f"Config file: {config_path}")
64
+ print()
65
+
66
+ if key:
67
+ value = get_nested_value(config, key)
68
+ if value is not None:
69
+ print(f"{key} = {value}")
70
+ else:
71
+ print(f"Key not found: {key}")
72
+ raise SystemExit(1)
73
+ else:
74
+ print(format_config(config))
75
+
76
+
77
+ def config_set_command(
78
+ key: Annotated[str, cyclopts.Parameter(help="Key to set (e.g., framework.name)")],
79
+ value: Annotated[str, cyclopts.Parameter(help="Value to set")],
80
+ config_file: Annotated[
81
+ Path | None,
82
+ cyclopts.Parameter(name="--config", help="Path to config file"),
83
+ ] = None,
84
+ ) -> None:
85
+ """Set a configuration value.
86
+
87
+ Updates a value in framework_config.toml, creating the file if needed.
88
+
89
+ Examples:
90
+ m config:set framework.name myapp
91
+ m config:set apps.installed "myapp,otherapp"
92
+ """
93
+ config_path = config_file or find_config_file()
94
+ config = load_config(config_path) or DEFAULT_CONFIG.copy()
95
+
96
+ # Parse value (handle lists)
97
+ if "," in value:
98
+ parsed_value: Any = [v.strip() for v in value.split(",")]
99
+ else:
100
+ parsed_value = value
101
+
102
+ old_value = get_nested_value(config, key)
103
+ set_nested_value(config, key, parsed_value)
104
+
105
+ save_config(config_path, config)
106
+
107
+ print(f"Config file: {config_path}")
108
+ print()
109
+ if old_value is not None:
110
+ print(f" {key}: {old_value} -> {parsed_value}")
111
+ else:
112
+ print(f" {key} = {parsed_value}")
113
+ print()
114
+ print("✓ Configuration updated")
115
+
116
+
117
+ __all__ = [
118
+ "CONFIG_FILE_NAME",
119
+ "DEFAULT_CONFIG",
120
+ "config_set_command",
121
+ "config_show_command",
122
+ "find_config_file",
123
+ "format_config",
124
+ "get_nested_value",
125
+ "load_config",
126
+ "save_config",
127
+ "set_nested_value",
128
+ ]
@@ -0,0 +1,30 @@
1
+ import cyclopts
2
+
3
+ from framework_m_core import __version__
4
+ from framework_m_core.cli.config import config_set_command, config_show_command
5
+ from framework_m_core.cli.plugin_loader import load_plugins
6
+ from framework_m_core.cli.utility import info_command
7
+
8
+ app = cyclopts.App(
9
+ name="m",
10
+ version=__version__,
11
+ help="Framework M CLI",
12
+ )
13
+
14
+ # Register core commands
15
+ app.command(info_command, name="info")
16
+
17
+ # Config commands
18
+ app.command(config_show_command, name="config:show")
19
+ app.command(config_set_command, name="config:set")
20
+
21
+ # Load plugins
22
+ load_plugins(app)
23
+
24
+
25
+ def main() -> None:
26
+ app()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
@@ -0,0 +1,132 @@
1
+ """Plugin loader for CLI extensibility.
2
+
3
+ This module provides the mechanism for 3rd-party apps to register
4
+ CLI commands via entry points. Apps can register either cyclopts Apps
5
+ (sub-command groups) or standalone functions as commands.
6
+
7
+ Entry Point Group: framework_m_core.cli_commands
8
+
9
+ Example pyproject.toml:
10
+ [project.entry-points."framework_m_core.cli_commands"]
11
+ studio = "my_app.cli:studio_app" # cyclopts.App
12
+ custom = "my_app.cli:custom_command" # Function
13
+
14
+ Example:
15
+ >>> app = cyclopts.App()
16
+ >>> load_plugins(app) # Discovers and registers all plugins
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from importlib.metadata import entry_points
23
+ from typing import TYPE_CHECKING
24
+
25
+ import cyclopts
26
+
27
+ if TYPE_CHECKING:
28
+ from importlib.metadata import EntryPoint
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Entry point group for CLI command plugins
33
+ CLI_COMMANDS_GROUP = "framework_m_core.cli_commands"
34
+
35
+
36
+ def discover_plugins() -> list[EntryPoint]:
37
+ """Discover all registered CLI command plugins.
38
+
39
+ Scans the entry points registered under the 'framework_m_core.cli_commands'
40
+ group. Each entry point should point to either:
41
+ - A `cyclopts.App` instance (registered as sub-command group)
42
+ - A callable function (registered as single command)
43
+
44
+ Returns:
45
+ List of entry points for CLI plugins.
46
+
47
+ Example:
48
+ >>> plugins = discover_plugins()
49
+ >>> for ep in plugins:
50
+ ... print(f"Found plugin: {ep.name}")
51
+ """
52
+ eps = entry_points()
53
+
54
+ # Python 3.12+ uses .select()
55
+ return list(eps.select(group=CLI_COMMANDS_GROUP))
56
+
57
+
58
+ def register_plugins(app: cyclopts.App, plugins: list[EntryPoint]) -> None:
59
+ """Register discovered plugins with the main cyclopts app.
60
+
61
+ For each plugin entry point:
62
+ - If it's a cyclopts.App: register as sub-command group
63
+ - If it's a callable: register as standalone command
64
+
65
+ Args:
66
+ app: The main cyclopts application instance.
67
+ plugins: List of entry points to register.
68
+
69
+ Example:
70
+ >>> app = cyclopts.App()
71
+ >>> plugins = discover_plugins()
72
+ >>> register_plugins(app, plugins)
73
+ """
74
+ for ep in plugins:
75
+ try:
76
+ plugin = ep.load()
77
+ except Exception as e:
78
+ logger.warning(
79
+ f"Failed to load CLI plugin '{ep.name}': {e}",
80
+ exc_info=True,
81
+ )
82
+ continue
83
+
84
+ try:
85
+ if isinstance(plugin, cyclopts.App):
86
+ # Register cyclopts App as sub-command group
87
+ app.command(plugin, name=ep.name)
88
+ logger.debug(f"Registered cyclopts App plugin: {ep.name}")
89
+ elif callable(plugin):
90
+ # Register function as command
91
+ app.command(plugin, name=ep.name)
92
+ logger.debug(f"Registered function plugin: {ep.name}")
93
+ else:
94
+ logger.warning(
95
+ f"Plugin '{ep.name}' is not a cyclopts App or callable, skipping"
96
+ )
97
+ except Exception as e:
98
+ logger.warning(
99
+ f"Failed to register CLI plugin '{ep.name}': {e}",
100
+ exc_info=True,
101
+ )
102
+
103
+
104
+ def load_plugins(app: cyclopts.App) -> None:
105
+ """Discover and register all CLI plugins.
106
+
107
+ Convenience function that combines discovery and registration.
108
+ Call this during app initialization to enable all plugins.
109
+
110
+ Args:
111
+ app: The main cyclopts application instance.
112
+
113
+ Example:
114
+ >>> app = cyclopts.App(name="m")
115
+ >>> load_plugins(app) # All plugins now registered
116
+ >>> app() # Run CLI
117
+ """
118
+ plugins = discover_plugins()
119
+
120
+ if plugins:
121
+ logger.info(f"Discovered {len(plugins)} CLI plugin(s)")
122
+ register_plugins(app, plugins)
123
+ else:
124
+ logger.debug("No CLI plugins discovered")
125
+
126
+
127
+ __all__ = [
128
+ "CLI_COMMANDS_GROUP",
129
+ "discover_plugins",
130
+ "load_plugins",
131
+ "register_plugins",
132
+ ]
@@ -0,0 +1,58 @@
1
+ """Utility commands for Framework M CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ import sys
7
+ from typing import Annotated
8
+
9
+ import cyclopts
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from framework_m_core import __version__
14
+
15
+ console = Console()
16
+
17
+
18
+ def get_python_version() -> str:
19
+ """Get Python version string."""
20
+ return platform.python_version()
21
+
22
+
23
+ def get_framework_version() -> str:
24
+ """Get Framework M version string."""
25
+ return __version__
26
+
27
+
28
+ def get_pythonpath() -> str:
29
+ """Get PYTHONPATH."""
30
+ return sys.path[0]
31
+
32
+
33
+ def info_command(
34
+ verbose: Annotated[
35
+ bool, cyclopts.Parameter(name="--verbose", help="Show detailed info")
36
+ ] = False,
37
+ ) -> None:
38
+ """Show system and framework information."""
39
+ table = Table(title="Framework M Information")
40
+ table.add_column("Key", style="cyan")
41
+ table.add_column("Value", style="green")
42
+
43
+ table.add_row("Framework M", get_framework_version())
44
+ table.add_row("Python", get_python_version())
45
+
46
+ if verbose:
47
+ table.add_row("Platform", platform.platform())
48
+ table.add_row("Services", "None (Mock)")
49
+
50
+ console.print(table)
51
+
52
+
53
+ __all__ = [
54
+ "get_framework_version",
55
+ "get_python_version",
56
+ "get_pythonpath",
57
+ "info_command",
58
+ ]
@@ -0,0 +1,171 @@
1
+ """Core Configuration Management.
2
+
3
+ This module provides utilities for finding and loading the framework configuration.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ # Default config file name
12
+ CONFIG_FILE_NAME = "framework_config.toml"
13
+
14
+ # Default config structure
15
+ DEFAULT_CONFIG = {
16
+ "framework": {
17
+ "name": "my_app",
18
+ "version": "0.1.0",
19
+ },
20
+ "apps": {
21
+ "installed": [],
22
+ },
23
+ }
24
+
25
+
26
+ def find_config_file() -> Path:
27
+ """Find the config file in current directory or parents.
28
+
29
+ Returns:
30
+ Path to config file (may not exist)
31
+ """
32
+ for path in [Path.cwd(), *Path.cwd().parents]:
33
+ config_path = path / CONFIG_FILE_NAME
34
+ if config_path.exists():
35
+ return config_path
36
+
37
+ # Default to cwd if not found
38
+ return Path.cwd() / CONFIG_FILE_NAME
39
+
40
+
41
+ def load_config(config_path: Path | None = None) -> dict[str, Any]:
42
+ """Load configuration from TOML file.
43
+
44
+ Args:
45
+ config_path: Path to config file (auto-detect if None)
46
+
47
+ Returns:
48
+ Configuration dict
49
+ """
50
+ if config_path is None:
51
+ config_path = find_config_file()
52
+
53
+ if not config_path.exists():
54
+ return {}
55
+
56
+ try:
57
+ import tomllib
58
+
59
+ return tomllib.loads(config_path.read_text())
60
+ except Exception:
61
+ return {}
62
+
63
+
64
+ def get_nested_value(config: dict[str, Any], key: str) -> Any | None:
65
+ """Get a nested value from config using dot notation.
66
+
67
+ Args:
68
+ config: Configuration dict
69
+ key: Dot-separated key (e.g., "framework.name")
70
+
71
+ Returns:
72
+ Value or None if not found
73
+ """
74
+ parts = key.split(".")
75
+ current = config
76
+
77
+ for part in parts:
78
+ if isinstance(current, dict) and part in current:
79
+ current = current[part]
80
+ else:
81
+ return None
82
+
83
+ return current
84
+
85
+
86
+ def set_nested_value(config: dict[str, Any], key: str, value: Any) -> None:
87
+ """Set a nested value in config using dot notation.
88
+
89
+ Args:
90
+ config: Configuration dict to modify
91
+ key: Dot-separated key (e.g., "framework.name")
92
+ value: Value to set
93
+ """
94
+ parts = key.split(".")
95
+ current = config
96
+
97
+ # Navigate/create nested dicts
98
+ for part in parts[:-1]:
99
+ if part not in current:
100
+ current[part] = {}
101
+ current = current[part]
102
+
103
+ # Set the final value
104
+ current[parts[-1]] = value
105
+
106
+
107
+ def save_config(config_path: Path, config: dict[str, Any]) -> None:
108
+ """Save configuration to TOML file.
109
+
110
+ Args:
111
+ config_path: Path to config file
112
+ config: Configuration dict to save
113
+ """
114
+ # Simple TOML writing (no external dependencies)
115
+ lines: list[str] = []
116
+ for section, values in config.items():
117
+ lines.append(f"[{section}]")
118
+ if isinstance(values, dict):
119
+ for key, value in values.items():
120
+ if isinstance(value, str):
121
+ lines.append(f'{key} = "{value}"')
122
+ elif isinstance(value, list):
123
+ items = ", ".join(f'"{v}"' for v in value)
124
+ lines.append(f"{key} = [{items}]")
125
+ elif isinstance(value, bool):
126
+ lines.append(f"{key} = {str(value).lower()}")
127
+ elif isinstance(value, (int, float)):
128
+ lines.append(f"{key} = {value}")
129
+ else:
130
+ lines.append(f'{key} = "{value}"')
131
+ lines.append("")
132
+
133
+ config_path.write_text("\n".join(lines))
134
+
135
+
136
+ def format_config(config: dict[str, Any], indent: int = 0) -> str:
137
+ """Format config dict for display.
138
+
139
+ Args:
140
+ config: Configuration dict
141
+ indent: Indentation level
142
+
143
+ Returns:
144
+ Formatted string
145
+ """
146
+ lines = []
147
+ prefix = " " * indent
148
+
149
+ for key, value in config.items():
150
+ if isinstance(value, dict):
151
+ lines.append(f"{prefix}[{key}]")
152
+ lines.append(format_config(value, indent + 1))
153
+ elif isinstance(value, list):
154
+ items = ", ".join(str(v) for v in value)
155
+ lines.append(f"{prefix}{key} = [{items}]")
156
+ else:
157
+ lines.append(f"{prefix}{key} = {value}")
158
+
159
+ return "\n".join(lines)
160
+
161
+
162
+ __all__ = [
163
+ "CONFIG_FILE_NAME",
164
+ "DEFAULT_CONFIG",
165
+ "find_config_file",
166
+ "format_config",
167
+ "get_nested_value",
168
+ "load_config",
169
+ "save_config",
170
+ "set_nested_value",
171
+ ]