mitsuki 0.1.0__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.
- mitsuki/__init__.py +156 -0
- mitsuki/cli/__init__.py +3 -0
- mitsuki/cli/bootstrap.py +215 -0
- mitsuki/cli/templates/README.md.tpl +28 -0
- mitsuki/cli/templates/__init__.py.tpl +1 -0
- mitsuki/cli/templates/app.py.tpl +26 -0
- mitsuki/cli/templates/application-dev.yml.tpl +12 -0
- mitsuki/cli/templates/application-prod.yml.tpl +16 -0
- mitsuki/cli/templates/application-stg.yml.tpl +16 -0
- mitsuki/cli/templates/application.yml.tpl +35 -0
- mitsuki/cli/templates/controller.py.tpl +55 -0
- mitsuki/cli/templates/entity.py.tpl +16 -0
- mitsuki/cli/templates/gitignore.tpl +22 -0
- mitsuki/cli/templates/repository.py.tpl +46 -0
- mitsuki/cli/templates/service.py.tpl +37 -0
- mitsuki/config/__init__.py +18 -0
- mitsuki/config/decorators.py +157 -0
- mitsuki/config/defaults.yml +61 -0
- mitsuki/config/exceptions.py +16 -0
- mitsuki/config/properties.py +249 -0
- mitsuki/core/__init__.py +18 -0
- mitsuki/core/application.py +286 -0
- mitsuki/core/container.py +271 -0
- mitsuki/core/decorators.py +185 -0
- mitsuki/core/enums.py +137 -0
- mitsuki/core/logging.py +150 -0
- mitsuki/core/metrics.py +33 -0
- mitsuki/core/providers.py +61 -0
- mitsuki/core/scanner.py +116 -0
- mitsuki/core/scheduler.py +399 -0
- mitsuki/core/server.py +252 -0
- mitsuki/core/utils.py +96 -0
- mitsuki/data/__init__.py +115 -0
- mitsuki/data/adapters/__init__.py +0 -0
- mitsuki/data/adapters/base.py +153 -0
- mitsuki/data/adapters/sqlalchemy.py +621 -0
- mitsuki/data/entity.py +216 -0
- mitsuki/data/query.py +109 -0
- mitsuki/data/query_decorators.py +68 -0
- mitsuki/data/query_parser.py +203 -0
- mitsuki/data/repository.py +577 -0
- mitsuki/data/types.py +334 -0
- mitsuki/exceptions.py +116 -0
- mitsuki/openapi/__init__.py +109 -0
- mitsuki/openapi/decorators.py +112 -0
- mitsuki/openapi/generator.py +131 -0
- mitsuki/openapi/introspector.py +310 -0
- mitsuki/openapi/schemas.py +199 -0
- mitsuki/openapi/ui.py +182 -0
- mitsuki/version.py +21 -0
- mitsuki/web/__init__.py +56 -0
- mitsuki/web/controllers.py +81 -0
- mitsuki/web/mappings.py +315 -0
- mitsuki/web/multipart.py +188 -0
- mitsuki/web/parameter_binder.py +280 -0
- mitsuki/web/params.py +259 -0
- mitsuki/web/response.py +125 -0
- mitsuki/web/response_processor.py +104 -0
- mitsuki/web/route_builder.py +218 -0
- mitsuki/web/serialization.py +185 -0
- mitsuki/web/upload.py +96 -0
- mitsuki-0.1.0.dist-info/METADATA +483 -0
- mitsuki-0.1.0.dist-info/RECORD +67 -0
- mitsuki-0.1.0.dist-info/WHEEL +5 -0
- mitsuki-0.1.0.dist-info/entry_points.txt +2 -0
- mitsuki-0.1.0.dist-info/licenses/LICENSE.md +202 -0
- mitsuki-0.1.0.dist-info/top_level.txt +1 -0
mitsuki/__init__.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mitsuki - Spring-inspired Python web framework.
|
|
3
|
+
Enterprise-grade annotation-driven development for Python.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Core
|
|
7
|
+
# Config
|
|
8
|
+
from mitsuki.config import Profile, Value
|
|
9
|
+
from mitsuki.core.application import Application, ApplicationContext
|
|
10
|
+
from mitsuki.core.container import DIContainer, get_container
|
|
11
|
+
from mitsuki.core.decorators import (
|
|
12
|
+
Component,
|
|
13
|
+
Configuration,
|
|
14
|
+
Provider,
|
|
15
|
+
Repository,
|
|
16
|
+
Scheduled,
|
|
17
|
+
Service,
|
|
18
|
+
)
|
|
19
|
+
from mitsuki.core.enums import (
|
|
20
|
+
ASGIMessageType,
|
|
21
|
+
ASGIScopeType,
|
|
22
|
+
DatabaseAdapter,
|
|
23
|
+
DatabaseDialect,
|
|
24
|
+
ParameterKind,
|
|
25
|
+
Scope,
|
|
26
|
+
ServerType,
|
|
27
|
+
SQLOperation,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Logging
|
|
31
|
+
from mitsuki.core.logging import get_logger
|
|
32
|
+
from mitsuki.core.utils import get_active_profile
|
|
33
|
+
|
|
34
|
+
# Data
|
|
35
|
+
from mitsuki.data import (
|
|
36
|
+
UUID,
|
|
37
|
+
Column,
|
|
38
|
+
CrudRepository,
|
|
39
|
+
Entity,
|
|
40
|
+
Field,
|
|
41
|
+
Id,
|
|
42
|
+
Modifying,
|
|
43
|
+
Query,
|
|
44
|
+
UUIDv1,
|
|
45
|
+
UUIDv4,
|
|
46
|
+
UUIDv5,
|
|
47
|
+
UUIDv7,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Web
|
|
51
|
+
from mitsuki.web.controllers import Controller, RestController, RestRouter, Router
|
|
52
|
+
from mitsuki.web.mappings import (
|
|
53
|
+
Consumes,
|
|
54
|
+
Delete,
|
|
55
|
+
DeleteMapping,
|
|
56
|
+
Get,
|
|
57
|
+
GetMapping,
|
|
58
|
+
Patch,
|
|
59
|
+
PatchMapping,
|
|
60
|
+
Post,
|
|
61
|
+
PostMapping,
|
|
62
|
+
Produces,
|
|
63
|
+
Put,
|
|
64
|
+
PutMapping,
|
|
65
|
+
RequestMapping,
|
|
66
|
+
)
|
|
67
|
+
from mitsuki.web.params import (
|
|
68
|
+
FormFile,
|
|
69
|
+
FormParam,
|
|
70
|
+
PathVariable,
|
|
71
|
+
QueryParam,
|
|
72
|
+
RequestBody,
|
|
73
|
+
RequestHeader,
|
|
74
|
+
RequestParam,
|
|
75
|
+
)
|
|
76
|
+
from mitsuki.web.response import JSONResponse, JsonResponse, ResponseEntity
|
|
77
|
+
from mitsuki.web.serialization import (
|
|
78
|
+
serialize_json,
|
|
79
|
+
serialize_json_safe,
|
|
80
|
+
)
|
|
81
|
+
from mitsuki.web.upload import UploadFile
|
|
82
|
+
|
|
83
|
+
__version__ = "0.1.0"
|
|
84
|
+
__all__ = [
|
|
85
|
+
# Core
|
|
86
|
+
"Application",
|
|
87
|
+
"ApplicationContext",
|
|
88
|
+
"Component",
|
|
89
|
+
"Service",
|
|
90
|
+
"Repository",
|
|
91
|
+
"Configuration",
|
|
92
|
+
"Provider",
|
|
93
|
+
"Scheduled",
|
|
94
|
+
"DIContainer",
|
|
95
|
+
"get_container",
|
|
96
|
+
"get_active_profile",
|
|
97
|
+
# Enums
|
|
98
|
+
"ServerType",
|
|
99
|
+
"DatabaseAdapter",
|
|
100
|
+
"DatabaseDialect",
|
|
101
|
+
"SQLOperation",
|
|
102
|
+
"ASGIMessageType",
|
|
103
|
+
"ASGIScopeType",
|
|
104
|
+
"ParameterKind",
|
|
105
|
+
"Scope",
|
|
106
|
+
# Logging
|
|
107
|
+
"get_logger",
|
|
108
|
+
# Web
|
|
109
|
+
"Controller",
|
|
110
|
+
"RestController",
|
|
111
|
+
"Router",
|
|
112
|
+
"RestRouter",
|
|
113
|
+
"GetMapping",
|
|
114
|
+
"PostMapping",
|
|
115
|
+
"PutMapping",
|
|
116
|
+
"DeleteMapping",
|
|
117
|
+
"PatchMapping",
|
|
118
|
+
"RequestMapping",
|
|
119
|
+
"Produces",
|
|
120
|
+
"Consumes",
|
|
121
|
+
"Get",
|
|
122
|
+
"Post",
|
|
123
|
+
"Put",
|
|
124
|
+
"Delete",
|
|
125
|
+
"Patch",
|
|
126
|
+
"PathVariable",
|
|
127
|
+
"QueryParam",
|
|
128
|
+
"RequestParam",
|
|
129
|
+
"RequestBody",
|
|
130
|
+
"RequestHeader",
|
|
131
|
+
"FormFile",
|
|
132
|
+
"FormParam",
|
|
133
|
+
"UploadFile",
|
|
134
|
+
"ResponseEntity",
|
|
135
|
+
"JsonResponse",
|
|
136
|
+
"JSONResponse",
|
|
137
|
+
# Data
|
|
138
|
+
"Entity",
|
|
139
|
+
"CrudRepository",
|
|
140
|
+
"Id",
|
|
141
|
+
"Column",
|
|
142
|
+
"Field",
|
|
143
|
+
"UUID",
|
|
144
|
+
"UUIDv1",
|
|
145
|
+
"UUIDv4",
|
|
146
|
+
"UUIDv5",
|
|
147
|
+
"UUIDv7",
|
|
148
|
+
"Query",
|
|
149
|
+
"Modifying",
|
|
150
|
+
# Config
|
|
151
|
+
"Value",
|
|
152
|
+
"Profile",
|
|
153
|
+
# Serialization
|
|
154
|
+
"serialize_json",
|
|
155
|
+
"serialize_json_safe",
|
|
156
|
+
]
|
mitsuki/cli/__init__.py
ADDED
mitsuki/cli/bootstrap.py
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from mitsuki.core.enums import DatabaseDialect
|
|
6
|
+
from mitsuki.core.logging import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def read_template(template_name: str) -> str:
|
|
12
|
+
"""Read a template file from the templates directory."""
|
|
13
|
+
template_dir = Path(__file__).parent / "templates"
|
|
14
|
+
template_path = template_dir / template_name
|
|
15
|
+
return template_path.read_text()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_directory(path: Path) -> None:
|
|
19
|
+
"""Create a directory if it doesn't exist."""
|
|
20
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
logger.info(f"Created directory: {path}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def write_file(path: Path, content: str) -> None:
|
|
25
|
+
"""Write content to a file."""
|
|
26
|
+
path.write_text(content)
|
|
27
|
+
logger.info(f"Created file: {path}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def create_domain_files(app_dir: Path, app_name: str, domain_name: str) -> None:
|
|
31
|
+
"""Create entity, repository, service, and controller for a domain."""
|
|
32
|
+
domain_lower = domain_name.lower()
|
|
33
|
+
|
|
34
|
+
# Entity
|
|
35
|
+
entity_content = read_template("entity.py.tpl").replace(
|
|
36
|
+
"{{DOMAIN_NAME}}", domain_name
|
|
37
|
+
)
|
|
38
|
+
write_file(app_dir / "domain" / f"{domain_lower}.py", entity_content)
|
|
39
|
+
|
|
40
|
+
# Repository
|
|
41
|
+
repo_content = (
|
|
42
|
+
read_template("repository.py.tpl")
|
|
43
|
+
.replace("{{DOMAIN_NAME}}", domain_name)
|
|
44
|
+
.replace("{{domain_name}}", domain_lower)
|
|
45
|
+
)
|
|
46
|
+
write_file(app_dir / "repository" / f"{domain_lower}_repository.py", repo_content)
|
|
47
|
+
|
|
48
|
+
# Service
|
|
49
|
+
service_content = (
|
|
50
|
+
read_template("service.py.tpl")
|
|
51
|
+
.replace("{{DOMAIN_NAME}}", domain_name)
|
|
52
|
+
.replace("{{domain_name}}", domain_lower)
|
|
53
|
+
)
|
|
54
|
+
write_file(app_dir / "service" / f"{domain_lower}_service.py", service_content)
|
|
55
|
+
|
|
56
|
+
# Controller
|
|
57
|
+
controller_content = (
|
|
58
|
+
read_template("controller.py.tpl")
|
|
59
|
+
.replace("{{DOMAIN_NAME}}", domain_name)
|
|
60
|
+
.replace("{{domain_name}}", domain_lower)
|
|
61
|
+
)
|
|
62
|
+
write_file(
|
|
63
|
+
app_dir / "controller" / f"{domain_lower}_controller.py", controller_content
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@click.group()
|
|
68
|
+
def cli():
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cli.command()
|
|
73
|
+
def init():
|
|
74
|
+
"""Bootstrap a new Mitsuki application."""
|
|
75
|
+
|
|
76
|
+
# Application name
|
|
77
|
+
app_name = click.prompt("Application name")
|
|
78
|
+
app_name = app_name.lower().replace("-", "_").replace(" ", "_")
|
|
79
|
+
|
|
80
|
+
# Description
|
|
81
|
+
description = click.prompt("Description (optional)", default="", show_default=False)
|
|
82
|
+
|
|
83
|
+
# Database type
|
|
84
|
+
db_type = click.prompt(
|
|
85
|
+
"Database type",
|
|
86
|
+
type=click.Choice(["sqlite", "postgresql", "mysql"], case_sensitive=False),
|
|
87
|
+
default="sqlite",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Starter domain
|
|
91
|
+
create_domain = click.confirm(
|
|
92
|
+
"Create starter domain (i.e. models/object definitions)?", default=True
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
domains = []
|
|
96
|
+
if create_domain:
|
|
97
|
+
domain_name = click.prompt("Domain name (e.g., User, Product)")
|
|
98
|
+
domains.append(domain_name)
|
|
99
|
+
|
|
100
|
+
# Additional domains
|
|
101
|
+
while click.confirm("Add another domain object?", default=False):
|
|
102
|
+
domain_name = click.prompt("Domain name")
|
|
103
|
+
domains.append(domain_name)
|
|
104
|
+
|
|
105
|
+
# Create project structure
|
|
106
|
+
project_root = Path.cwd() / app_name
|
|
107
|
+
src_dir = project_root / "src"
|
|
108
|
+
app_dir = src_dir / app_name
|
|
109
|
+
|
|
110
|
+
if project_root.exists():
|
|
111
|
+
click.echo(f"Error: Directory {app_name} already exists")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Create directories
|
|
115
|
+
create_directory(project_root)
|
|
116
|
+
create_directory(src_dir)
|
|
117
|
+
create_directory(app_dir)
|
|
118
|
+
|
|
119
|
+
subdirs = ["domain", "repository", "service", "controller"]
|
|
120
|
+
init_content = read_template("__init__.py.tpl").replace(
|
|
121
|
+
"{{MODULE_DESCRIPTION}}", description or f"{app_name} module"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
for subdir in subdirs:
|
|
125
|
+
create_directory(app_dir / subdir)
|
|
126
|
+
write_file(app_dir / subdir / "__init__.py", init_content)
|
|
127
|
+
|
|
128
|
+
write_file(app_dir / "__init__.py", init_content)
|
|
129
|
+
|
|
130
|
+
# Create app.py with controller imports
|
|
131
|
+
controller_imports = ""
|
|
132
|
+
if domains:
|
|
133
|
+
imports = []
|
|
134
|
+
for domain_name in domains:
|
|
135
|
+
domain_lower = domain_name.lower()
|
|
136
|
+
imports.append(
|
|
137
|
+
f"from {app_name}.controller.{domain_lower}_controller import {domain_name}Controller"
|
|
138
|
+
)
|
|
139
|
+
controller_imports = "\n" + "\n".join(imports)
|
|
140
|
+
|
|
141
|
+
app_content = read_template("app.py.tpl").replace(
|
|
142
|
+
"{{CONTROLLER_IMPORT}}", controller_imports
|
|
143
|
+
)
|
|
144
|
+
write_file(src_dir / "app.py", app_content)
|
|
145
|
+
|
|
146
|
+
# Create domain files
|
|
147
|
+
for domain_name in domains:
|
|
148
|
+
create_domain_files(app_dir, app_name, domain_name)
|
|
149
|
+
|
|
150
|
+
# Create configuration files
|
|
151
|
+
db_url_map = {
|
|
152
|
+
"sqlite": f"sqlite:///{app_name}.db",
|
|
153
|
+
"postgresql": f"postgresql://localhost/{app_name}",
|
|
154
|
+
"mysql": f"mysql://localhost/{app_name}",
|
|
155
|
+
}
|
|
156
|
+
db_url = db_url_map[db_type]
|
|
157
|
+
|
|
158
|
+
app_yml = read_template("application.yml.tpl").replace("{{app_name}}", app_name)
|
|
159
|
+
app_yml = app_yml.replace("sqlite:///{{app_name}}.db", db_url)
|
|
160
|
+
write_file(project_root / "application.yml", app_yml)
|
|
161
|
+
|
|
162
|
+
for env in ["dev", "stg", "prod"]:
|
|
163
|
+
env_yml = read_template(f"application-{env}.yml.tpl").replace(
|
|
164
|
+
"{{app_name}}", app_name
|
|
165
|
+
)
|
|
166
|
+
if db_type == DatabaseDialect.SQLITE:
|
|
167
|
+
env_yml = env_yml.replace(
|
|
168
|
+
"postgresql://localhost/{{app_name}}", f"sqlite:///{app_name}.db"
|
|
169
|
+
)
|
|
170
|
+
elif db_type == DatabaseDialect.MYSQL:
|
|
171
|
+
env_yml = env_yml.replace("postgresql://localhost/", "mysql://localhost/")
|
|
172
|
+
write_file(project_root / f"application-{env}.yml", env_yml)
|
|
173
|
+
|
|
174
|
+
# Create README
|
|
175
|
+
app_title = app_name.replace("_", " ").title()
|
|
176
|
+
domain_section = ""
|
|
177
|
+
|
|
178
|
+
if domains:
|
|
179
|
+
endpoints = []
|
|
180
|
+
for domain_name in domains:
|
|
181
|
+
domain_lower = domain_name.lower()
|
|
182
|
+
endpoints.append(f"""
|
|
183
|
+
- `GET /api/{domain_lower}` - List all {domain_lower}s
|
|
184
|
+
- `GET /api/{domain_lower}/{{id}}` - Get {domain_lower} by ID
|
|
185
|
+
- `POST /api/{domain_lower}` - Create new {domain_lower}
|
|
186
|
+
- `PUT /api/{domain_lower}/{{id}}` - Update {domain_lower}
|
|
187
|
+
- `DELETE /api/{domain_lower}/{{id}}` - Delete {domain_lower}""")
|
|
188
|
+
|
|
189
|
+
domain_section = "\n## API Endpoints\n" + "\n".join(endpoints) + "\n"
|
|
190
|
+
|
|
191
|
+
readme = (
|
|
192
|
+
read_template("README.md.tpl")
|
|
193
|
+
.replace("{{APP_TITLE}}", app_title)
|
|
194
|
+
.replace("{{app_name}}", app_name)
|
|
195
|
+
.replace("{{DOMAIN_SECTION}}", domain_section)
|
|
196
|
+
)
|
|
197
|
+
write_file(project_root / "README.md", readme)
|
|
198
|
+
|
|
199
|
+
# Create .gitignore
|
|
200
|
+
gitignore = read_template("gitignore.tpl")
|
|
201
|
+
write_file(project_root / ".gitignore", gitignore)
|
|
202
|
+
|
|
203
|
+
logger.info(f"\nSuccessfully created Mitsuki application: {app_name}")
|
|
204
|
+
logger.info("\nTo get started:")
|
|
205
|
+
logger.info(f" cd {app_name}")
|
|
206
|
+
logger.info(" MITSUKI_PROFILE=development python src/app.py")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main():
|
|
210
|
+
cli()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
# For running the bootstrap script as a script
|
|
215
|
+
cli()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# {{APP_TITLE}}
|
|
2
|
+
|
|
3
|
+
Mitsuki application generated with the Mitsuki CLI.
|
|
4
|
+
{{DOMAIN_SECTION}}
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mitsuki
|
|
9
|
+
MITSUKI_PROFILE=development python src/app.py
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The server will start on http://127.0.0.1:8000
|
|
13
|
+
|
|
14
|
+
## Project Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/
|
|
18
|
+
{{app_name}}/
|
|
19
|
+
domain/ # @Entity classes
|
|
20
|
+
repository/ # @CrudRepository classes
|
|
21
|
+
service/ # @Service classes
|
|
22
|
+
controller/ # @RestController classes
|
|
23
|
+
app.py
|
|
24
|
+
application.yml
|
|
25
|
+
application-dev.yml
|
|
26
|
+
application-stg.yml
|
|
27
|
+
application-prod.yml
|
|
28
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""{{MODULE_DESCRIPTION}}"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Main application entry point.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
from mitsuki import Application, Value, get_logger{{CONTROLLER_IMPORT}}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@Application
|
|
9
|
+
class App:
|
|
10
|
+
"""
|
|
11
|
+
Mitsuki application.
|
|
12
|
+
|
|
13
|
+
Configuration is loaded from application.yml and can be overridden
|
|
14
|
+
with environment variables (MITSUKI_SERVER_PORT, etc.)
|
|
15
|
+
"""
|
|
16
|
+
port: int = Value("${server.port:8000}")
|
|
17
|
+
host: str = Value("${server.host:127.0.0.1}")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
# Get active profile from environment
|
|
22
|
+
profile = os.getenv("MITSUKI_PROFILE", "development")
|
|
23
|
+
logger = get_logger()
|
|
24
|
+
logger.info(f"Starting Mitsuki application with profile: {profile}")
|
|
25
|
+
|
|
26
|
+
App.run()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Mitsuki Application Configuration
|
|
2
|
+
|
|
3
|
+
server:
|
|
4
|
+
host: 0.0.0.0
|
|
5
|
+
port: 8000
|
|
6
|
+
type: granian # Options: uvicorn, granian
|
|
7
|
+
workers: 1 # Number of worker processes (Currently supported on Granian only)
|
|
8
|
+
access_log: true # Enable/disable access logging
|
|
9
|
+
ignore_trailing_slash: true
|
|
10
|
+
# timeout: 60 # Request timeout in seconds (optional, uses server defaults if not set)
|
|
11
|
+
# max_body_size: 10485760 # Max request body size in bytes (default: 10MB)
|
|
12
|
+
# cors:
|
|
13
|
+
# enabled: false
|
|
14
|
+
# allowed_origins:
|
|
15
|
+
# - "*"
|
|
16
|
+
|
|
17
|
+
database:
|
|
18
|
+
url: sqlite:///app.db
|
|
19
|
+
adapter: sqlalchemy
|
|
20
|
+
echo: false # SQLAlchemy logs
|
|
21
|
+
pool:
|
|
22
|
+
enabled: true
|
|
23
|
+
size: 10
|
|
24
|
+
max_overflow: 20
|
|
25
|
+
timeout: 30
|
|
26
|
+
recycle: 3600
|
|
27
|
+
|
|
28
|
+
logging:
|
|
29
|
+
level: INFO
|
|
30
|
+
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
31
|
+
sqlalchemy: false
|
|
32
|
+
|
|
33
|
+
app:
|
|
34
|
+
name: {{APP_NAME}}
|
|
35
|
+
debug: false
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{DOMAIN_NAME}}Controller - REST API endpoints for {{DOMAIN_NAME}}.
|
|
3
|
+
"""
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import List
|
|
6
|
+
from mitsuki import RestController, GetMapping, PostMapping, DeleteMapping, QueryParam
|
|
7
|
+
from ..service.{{domain_name}}_service import {{DOMAIN_NAME}}Service
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@RestController("/api/{{domain_name}}s")
|
|
11
|
+
class {{DOMAIN_NAME}}Controller:
|
|
12
|
+
"""REST API controller for {{DOMAIN_NAME}} resources."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, service: {{DOMAIN_NAME}}Service):
|
|
15
|
+
self.service = service
|
|
16
|
+
|
|
17
|
+
@GetMapping("")
|
|
18
|
+
async def list_all(
|
|
19
|
+
self,
|
|
20
|
+
page: int = QueryParam(default=0),
|
|
21
|
+
size: int = QueryParam(default=100)
|
|
22
|
+
) -> List[dict]:
|
|
23
|
+
"""GET /api/{{domain_name}}s?page=0&size=100"""
|
|
24
|
+
entities = await self.service.get_all(page=page, size=size)
|
|
25
|
+
return [self._to_dict(e) for e in entities]
|
|
26
|
+
|
|
27
|
+
@GetMapping("/{id}")
|
|
28
|
+
async def get_by_id(self, id: str) -> dict:
|
|
29
|
+
"""GET /api/{{domain_name}}s/123e4567-e89b-12d3-a456-426614174000"""
|
|
30
|
+
entity = await self.service.get_by_id(uuid.UUID(id))
|
|
31
|
+
if not entity:
|
|
32
|
+
return {"error": "Not found"}, 404
|
|
33
|
+
return self._to_dict(entity)
|
|
34
|
+
|
|
35
|
+
@PostMapping("")
|
|
36
|
+
async def create(self, body: dict) -> dict:
|
|
37
|
+
"""POST /api/{{domain_name}}s"""
|
|
38
|
+
entity = await self.service.create()
|
|
39
|
+
return self._to_dict(entity), 201
|
|
40
|
+
|
|
41
|
+
@DeleteMapping("/{id}")
|
|
42
|
+
async def delete(self, id: str) -> dict:
|
|
43
|
+
"""DELETE /api/{{domain_name}}s/123e4567-e89b-12d3-a456-426614174000"""
|
|
44
|
+
deleted = await self.service.delete(uuid.UUID(id))
|
|
45
|
+
if not deleted:
|
|
46
|
+
return {"error": "Not found"}, 404
|
|
47
|
+
return {"success": True}
|
|
48
|
+
|
|
49
|
+
def _to_dict(self, entity) -> dict:
|
|
50
|
+
"""Convert entity to dict for JSON response"""
|
|
51
|
+
return {
|
|
52
|
+
"id": str(entity.id),
|
|
53
|
+
"created_at": entity.created_at.isoformat() if entity.created_at else None,
|
|
54
|
+
"updated_at": entity.updated_at.isoformat() if entity.updated_at else None,
|
|
55
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{DOMAIN_NAME}} entity definition.
|
|
3
|
+
"""
|
|
4
|
+
import uuid
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from mitsuki import Entity, UUIDv7, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@Entity()
|
|
11
|
+
@dataclass
|
|
12
|
+
class {{DOMAIN_NAME}}:
|
|
13
|
+
"""{{DOMAIN_NAME}} domain object."""
|
|
14
|
+
id: uuid.UUID = UUIDv7()
|
|
15
|
+
created_at: datetime = Field(update_on_create=True)
|
|
16
|
+
updated_at: datetime = Field(update_on_save=True)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{DOMAIN_NAME}}Repository - Data access layer for {{DOMAIN_NAME}}.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, List
|
|
5
|
+
from mitsuki import CrudRepository, Query, Modifying
|
|
6
|
+
from ..domain.{{domain_name}} import {{DOMAIN_NAME}}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@CrudRepository(entity={{DOMAIN_NAME}})
|
|
10
|
+
class {{DOMAIN_NAME}}Repository:
|
|
11
|
+
"""
|
|
12
|
+
Repository for {{DOMAIN_NAME}} entities.
|
|
13
|
+
|
|
14
|
+
Auto-implemented CRUD methods (you can just call these as-is):
|
|
15
|
+
- save(entity) - Create or update
|
|
16
|
+
- find_by_id(id) - Find by primary key
|
|
17
|
+
- find_all(page, size, sort_by, sort_desc) - Paginated query
|
|
18
|
+
- delete(entity) - Delete entity
|
|
19
|
+
- delete_by_id(id) - Delete by ID
|
|
20
|
+
- count() - Count all entities
|
|
21
|
+
- exists_by_id(id) - Check existence
|
|
22
|
+
|
|
23
|
+
Auto-generated query methods (define signature only):
|
|
24
|
+
Examples:
|
|
25
|
+
async def find_by_name(self, name: str) -> Optional[{{DOMAIN_NAME}}]: ...
|
|
26
|
+
async def find_by_status(self, status: str) -> List[{{DOMAIN_NAME}}]: ...
|
|
27
|
+
async def find_by_created_at_greater_than(self, created_at) -> List[{{DOMAIN_NAME}}]: ...
|
|
28
|
+
|
|
29
|
+
Custom @Query methods:
|
|
30
|
+
@Query("SELECT e FROM {{DOMAIN_NAME}} e WHERE e.status = :status ORDER BY e.created_at DESC")
|
|
31
|
+
async def find_active_sorted(self, status: str) -> List[{{DOMAIN_NAME}}]: ...
|
|
32
|
+
|
|
33
|
+
Custom @Modifying queries (UPDATE/DELETE):
|
|
34
|
+
@Modifying
|
|
35
|
+
@Query("UPDATE {{DOMAIN_NAME}} SET status = :status WHERE id = :id")
|
|
36
|
+
async def update_status(self, id: int, status: str) -> int: ...
|
|
37
|
+
|
|
38
|
+
Custom methods with direct database access:
|
|
39
|
+
async def custom_complex_query(self, param: str) -> List[dict]:
|
|
40
|
+
async with self.connection as conn:
|
|
41
|
+
result = await conn.execute("SELECT * FROM table WHERE field = :param", {"param": param})
|
|
42
|
+
return [dict(row) for row in result]
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
# Add your custom query methods here
|
|
46
|
+
pass
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
{{DOMAIN_NAME}}Service - Business logic for {{DOMAIN_NAME}}.
|
|
3
|
+
"""
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from mitsuki import Service
|
|
7
|
+
from ..domain.{{domain_name}} import {{DOMAIN_NAME}}
|
|
8
|
+
from ..repository.{{domain_name}}_repository import {{DOMAIN_NAME}}Repository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@Service()
|
|
12
|
+
class {{DOMAIN_NAME}}Service:
|
|
13
|
+
"""Service layer for {{DOMAIN_NAME}} business logic."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, repo: {{DOMAIN_NAME}}Repository):
|
|
16
|
+
self.repo = repo
|
|
17
|
+
|
|
18
|
+
async def get_all(self, page: int = 0, size: int = 100) -> List[{{DOMAIN_NAME}}]:
|
|
19
|
+
"""Get all {{domain_name}}s with pagination"""
|
|
20
|
+
return await self.repo.find_all(page=page, size=size)
|
|
21
|
+
|
|
22
|
+
async def get_by_id(self, id: uuid.UUID) -> Optional[{{DOMAIN_NAME}}]:
|
|
23
|
+
"""Get {{domain_name}} by ID"""
|
|
24
|
+
return await self.repo.find_by_id(id)
|
|
25
|
+
|
|
26
|
+
async def create(self) -> {{DOMAIN_NAME}}:
|
|
27
|
+
"""Create new {{domain_name}}"""
|
|
28
|
+
entity = {{DOMAIN_NAME}}(id=None)
|
|
29
|
+
return await self.repo.save(entity)
|
|
30
|
+
|
|
31
|
+
async def delete(self, id: uuid.UUID) -> bool:
|
|
32
|
+
"""Delete {{domain_name}} by ID"""
|
|
33
|
+
exists = await self.repo.exists_by_id(id)
|
|
34
|
+
if not exists:
|
|
35
|
+
return False
|
|
36
|
+
await self.repo.delete_by_id(id)
|
|
37
|
+
return True
|