sagnos 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.
- sagnos-0.1.0/PKG-INFO +18 -0
- sagnos-0.1.0/README.md +0 -0
- sagnos-0.1.0/pyproject.toml +35 -0
- sagnos-0.1.0/sagnos/__init__.py +15 -0
- sagnos-0.1.0/sagnos/codegen.py +354 -0
- sagnos-0.1.0/sagnos/config.py +40 -0
- sagnos-0.1.0/sagnos/core.py +351 -0
- sagnos-0.1.0/sagnos/schema.py +260 -0
- sagnos-0.1.0/sagnos/server.py +383 -0
- sagnos-0.1.0/sagnos/websocket.py +9 -0
- sagnos-0.1.0/sagnos.egg-info/PKG-INFO +18 -0
- sagnos-0.1.0/sagnos.egg-info/SOURCES.txt +18 -0
- sagnos-0.1.0/sagnos.egg-info/dependency_links.txt +1 -0
- sagnos-0.1.0/sagnos.egg-info/entry_points.txt +2 -0
- sagnos-0.1.0/sagnos.egg-info/requires.txt +10 -0
- sagnos-0.1.0/sagnos.egg-info/top_level.txt +1 -0
- sagnos-0.1.0/setup.cfg +4 -0
- sagnos-0.1.0/tests/test_codegen.py +37 -0
- sagnos-0.1.0/tests/test_core.py +26 -0
- sagnos-0.1.0/tests/test_schema.py +22 -0
sagnos-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sagnos
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: The Spring Boot of Python for Flutter
|
|
5
|
+
Author: Siddhardh
|
|
6
|
+
Keywords: flutter,dart,backend,framework
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: fastapi>=0.110.0
|
|
10
|
+
Requires-Dist: uvicorn[standard]>=0.29.0
|
|
11
|
+
Requires-Dist: pydantic>=2.0.0
|
|
12
|
+
Requires-Dist: jinja2>=3.1.0
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Requires-Dist: websockets>=12.0
|
|
15
|
+
Requires-Dist: typer>=0.12.0
|
|
16
|
+
Requires-Dist: rich>=13.0.0
|
|
17
|
+
Requires-Dist: watchfiles>=0.21.0
|
|
18
|
+
Requires-Dist: psutil>=5.9.0
|
sagnos-0.1.0/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sagnos"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "The Spring Boot of Python for Flutter"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Siddhardh" }]
|
|
12
|
+
keywords = ["flutter", "dart", "backend", "framework"]
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
"fastapi>=0.110.0",
|
|
16
|
+
"uvicorn[standard]>=0.29.0",
|
|
17
|
+
"pydantic>=2.0.0",
|
|
18
|
+
"jinja2>=3.1.0",
|
|
19
|
+
"requests>=2.31.0",
|
|
20
|
+
"websockets>=12.0",
|
|
21
|
+
"typer>=0.12.0",
|
|
22
|
+
"rich>=13.0.0",
|
|
23
|
+
"watchfiles>=0.21.0",
|
|
24
|
+
"psutil>=5.9.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
sagnos = "sagnos.cli:app"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
include = ["sagnos*"]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
expose, model, stream,
|
|
3
|
+
SagnosError, NotFoundError,
|
|
4
|
+
ValidationError_, AuthError, ForbiddenError,
|
|
5
|
+
)
|
|
6
|
+
from .server import SagnosApp, SagnosAuth
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"expose", "model", "stream",
|
|
12
|
+
"SagnosApp", "SagnosAuth",
|
|
13
|
+
"SagnosError", "NotFoundError",
|
|
14
|
+
"ValidationError_", "AuthError", "ForbiddenError",
|
|
15
|
+
]
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sagnos/codegen.py
|
|
3
|
+
Reads the schema and generates Dart files.
|
|
4
|
+
This is what runs when you do: sagnos generate
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from jinja2 import Environment, BaseLoader
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ─── Dart Templates ───────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
MODELS_TEMPLATE = """// AUTO-GENERATED BY SAGNOS — DO NOT EDIT
|
|
15
|
+
// Run `sagnos generate` to regenerate
|
|
16
|
+
// Schema hash: {{ schema_hash }}
|
|
17
|
+
|
|
18
|
+
{% for model in models %}
|
|
19
|
+
class {{ model.name }} {
|
|
20
|
+
{% for f in model.fields %}
|
|
21
|
+
final {{ f.dart_type }} {{ f.dart_name }};
|
|
22
|
+
{% endfor %}
|
|
23
|
+
|
|
24
|
+
const {{ model.name }}({
|
|
25
|
+
{% for f in model.fields %}
|
|
26
|
+
{% if f.optional %}this.{{ f.dart_name }},
|
|
27
|
+
{% else %}required this.{{ f.dart_name }},
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endfor %}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
factory {{ model.name }}.fromJson(Map<String, dynamic> json) {
|
|
33
|
+
return {{ model.name }}(
|
|
34
|
+
{% for f in model.fields %}
|
|
35
|
+
{{ f.dart_name }}: {{ f.from_json }},
|
|
36
|
+
{% endfor %}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
Map<String, dynamic> toJson() {
|
|
41
|
+
return {
|
|
42
|
+
{% for f in model.fields %}
|
|
43
|
+
'{{ f.name }}': {{ f.dart_name }},
|
|
44
|
+
{% endfor %}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
{{ model.name }} copyWith({
|
|
49
|
+
{% for f in model.fields %}
|
|
50
|
+
{% if f.optional %}
|
|
51
|
+
{{ f.dart_type }} {{ f.dart_name }},
|
|
52
|
+
{% else %}
|
|
53
|
+
{{ f.dart_type }}? {{ f.dart_name }},
|
|
54
|
+
{% endif %}
|
|
55
|
+
{% endfor %}
|
|
56
|
+
}) {
|
|
57
|
+
return {{ model.name }}(
|
|
58
|
+
{% for f in model.fields %}
|
|
59
|
+
{{ f.dart_name }}: {{ f.dart_name }} ?? this.{{ f.dart_name }},
|
|
60
|
+
{% endfor %}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
String toString() {
|
|
66
|
+
return '{{ model.name }}({% for f in model.fields %}{{ f.dart_name }}: ${{ f.dart_name }}{% if not loop.last %}, {% endif %}{% endfor %})';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
{% endfor %}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
CLIENT_TEMPLATE = """// AUTO-GENERATED BY SAGNOS — DO NOT EDIT
|
|
73
|
+
// Run `sagnos generate` to regenerate
|
|
74
|
+
// Schema hash: {{ schema_hash }}
|
|
75
|
+
|
|
76
|
+
import 'dart:convert';
|
|
77
|
+
import 'package:http/http.dart' as http;
|
|
78
|
+
import 'models.dart';
|
|
79
|
+
import 'sagnos_exception.dart';
|
|
80
|
+
|
|
81
|
+
class SagnosClient {
|
|
82
|
+
final String baseUrl;
|
|
83
|
+
String? _token;
|
|
84
|
+
|
|
85
|
+
SagnosClient({
|
|
86
|
+
this.baseUrl = '{{ base_url }}',
|
|
87
|
+
String? token,
|
|
88
|
+
}) : _token = token;
|
|
89
|
+
|
|
90
|
+
void setToken(String token) => _token = token;
|
|
91
|
+
void clearToken() => _token = null;
|
|
92
|
+
|
|
93
|
+
Map<String, String> get _headers => {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
if (_token != null) 'Authorization': 'Bearer $_token',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// [FIX-3] Call this in main() to detect schema drift
|
|
99
|
+
Future<void> checkSchemaDrift() async {
|
|
100
|
+
try {
|
|
101
|
+
final uri = Uri.parse('$baseUrl/sagnos/schema-version');
|
|
102
|
+
final response = await http.get(uri, headers: _headers);
|
|
103
|
+
if (response.statusCode != 200) return;
|
|
104
|
+
final data = json.decode(response.body);
|
|
105
|
+
const clientHash = '{{ schema_hash }}';
|
|
106
|
+
final serverHash = data['schema_hash'] as String?;
|
|
107
|
+
if (serverHash != null && serverHash != clientHash) {
|
|
108
|
+
print('[Sagnos] Schema drift detected! Run sagnos generate.');
|
|
109
|
+
}
|
|
110
|
+
} catch (_) {}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Future<dynamic> _get(String path, [Map<String, String>? query]) async {
|
|
114
|
+
final uri = Uri.parse('$baseUrl$path').replace(queryParameters: query);
|
|
115
|
+
final response = await http.get(uri, headers: _headers);
|
|
116
|
+
return _handle(response);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Future<dynamic> _post(String path, Map<String, dynamic> body) async {
|
|
120
|
+
final uri = Uri.parse('$baseUrl$path');
|
|
121
|
+
final response = await http.post(
|
|
122
|
+
uri,
|
|
123
|
+
headers: _headers,
|
|
124
|
+
body: json.encode(body),
|
|
125
|
+
);
|
|
126
|
+
return _handle(response);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dynamic _handle(http.Response response) {
|
|
130
|
+
final decoded = json.decode(response.body) as Map<String, dynamic>;
|
|
131
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
132
|
+
return decoded['data'];
|
|
133
|
+
}
|
|
134
|
+
throw SagnosException(
|
|
135
|
+
statusCode: response.statusCode,
|
|
136
|
+
errorCode: decoded['error_code'] ?? 'UNKNOWN',
|
|
137
|
+
message: decoded['message'] ?? 'Unknown error',
|
|
138
|
+
detail: decoded['detail'],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Generated Methods ─────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
{% for ep in endpoints %}
|
|
145
|
+
{% if ep.deprecated %}@Deprecated('Deprecated on the backend.'){% endif %}
|
|
146
|
+
/// {{ ep.docstring or ep.dart_method }}
|
|
147
|
+
Future<{{ ep.return_dart }}> {{ ep.dart_method }}({% for p in ep.params %}{{ p.dart_type }} {{ p.name }}{% if not loop.last %}, {% endif %}{% endfor %}) async {
|
|
148
|
+
{% if ep.method == 'GET' %}
|
|
149
|
+
final data = await _get('{{ ep.path }}'{% if ep.params %}, { {% for p in ep.params %}'{{ p.name }}': {{ p.name }}.toString(){% if not loop.last %}, {% endif %}{% endfor %} }{% endif %});
|
|
150
|
+
{% else %}
|
|
151
|
+
final data = await _post('{{ ep.path }}', { {% for p in ep.params %}'{{ p.name }}': {{ p.name }}{% if not loop.last %}, {% endif %}{% endfor %} });
|
|
152
|
+
{% endif %}
|
|
153
|
+
{% set rt = ep.return_dart %}
|
|
154
|
+
{% if rt == 'void' or rt == 'dynamic' %}
|
|
155
|
+
return data;
|
|
156
|
+
{% elif rt == 'String' or rt == 'String?' %}
|
|
157
|
+
return data as {{ rt }};
|
|
158
|
+
{% elif rt == 'int' or rt == 'int?' %}
|
|
159
|
+
return data as int;
|
|
160
|
+
{% elif rt == 'double' or rt == 'double?' %}
|
|
161
|
+
return (data as num).toDouble();
|
|
162
|
+
{% elif rt == 'bool' or rt == 'bool?' %}
|
|
163
|
+
return data as bool;
|
|
164
|
+
{% elif rt == 'DateTime' or rt == 'DateTime?' %}
|
|
165
|
+
return {% if ep.return_optional %}data == null ? null : {% endif %}DateTime.parse(data as String);
|
|
166
|
+
{% elif rt.startswith('List<') %}
|
|
167
|
+
{% set inner = rt[5:-1] %}
|
|
168
|
+
{% if inner in ['int', 'double', 'String', 'bool'] %}
|
|
169
|
+
return (data as List).cast<{{ inner }}>();
|
|
170
|
+
{% else %}
|
|
171
|
+
return (data as List).map((e) => {{ inner }}.fromJson(e as Map<String, dynamic>)).toList();
|
|
172
|
+
{% endif %}
|
|
173
|
+
{% elif rt.endswith('?') %}
|
|
174
|
+
return data == null ? null : {{ rt[:-1] }}.fromJson(data as Map<String, dynamic>);
|
|
175
|
+
{% else %}
|
|
176
|
+
return {{ rt }}.fromJson(data as Map<String, dynamic>);
|
|
177
|
+
{% endif %}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
{% endfor %}
|
|
181
|
+
}
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
EXCEPTION_TEMPLATE = """// AUTO-GENERATED BY SAGNOS — DO NOT EDIT
|
|
185
|
+
|
|
186
|
+
class SagnosException implements Exception {
|
|
187
|
+
final int statusCode;
|
|
188
|
+
final String errorCode;
|
|
189
|
+
final String message;
|
|
190
|
+
final dynamic detail;
|
|
191
|
+
|
|
192
|
+
const SagnosException({
|
|
193
|
+
required this.statusCode,
|
|
194
|
+
required this.errorCode,
|
|
195
|
+
required this.message,
|
|
196
|
+
this.detail,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
bool get isNotFound => errorCode == 'NOT_FOUND';
|
|
200
|
+
bool get isUnauthorized => errorCode == 'UNAUTHORIZED';
|
|
201
|
+
bool get isForbidden => errorCode == 'FORBIDDEN';
|
|
202
|
+
bool get isValidation => errorCode == 'VALIDATION_ERROR';
|
|
203
|
+
bool get isInternal => errorCode == 'INTERNAL_ERROR';
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
String toString() => 'SagnosException[$errorCode $statusCode]: $message';
|
|
207
|
+
}
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
STREAM_TEMPLATE = """// AUTO-GENERATED BY SAGNOS — DO NOT EDIT
|
|
211
|
+
|
|
212
|
+
import 'dart:async';
|
|
213
|
+
import 'dart:convert';
|
|
214
|
+
import 'dart:io';
|
|
215
|
+
import 'models.dart';
|
|
216
|
+
import 'sagnos_exception.dart';
|
|
217
|
+
|
|
218
|
+
class SagnosStream<T> {
|
|
219
|
+
final String url;
|
|
220
|
+
final T Function(Map<String, dynamic>) fromJson;
|
|
221
|
+
final String? token;
|
|
222
|
+
|
|
223
|
+
WebSocket? _socket;
|
|
224
|
+
StreamController<T>? _controller;
|
|
225
|
+
bool _disposed = false;
|
|
226
|
+
|
|
227
|
+
SagnosStream({
|
|
228
|
+
required this.url,
|
|
229
|
+
required this.fromJson,
|
|
230
|
+
this.token,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
Stream<T> get stream {
|
|
234
|
+
_controller ??= StreamController<T>.broadcast(
|
|
235
|
+
onListen: _connect,
|
|
236
|
+
onCancel: _disconnect,
|
|
237
|
+
);
|
|
238
|
+
return _controller!.stream;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
Future<void> _connect() async {
|
|
242
|
+
try {
|
|
243
|
+
_socket = await WebSocket.connect(url);
|
|
244
|
+
_socket!.listen(
|
|
245
|
+
(raw) {
|
|
246
|
+
if (_disposed) return;
|
|
247
|
+
try {
|
|
248
|
+
final decoded = json.decode(raw as String) as Map<String, dynamic>;
|
|
249
|
+
if (decoded['status'] == 'error') {
|
|
250
|
+
_controller?.addError(SagnosException(
|
|
251
|
+
statusCode: decoded['status_code'] ?? 500,
|
|
252
|
+
errorCode: decoded['error_code'] ?? 'WS_ERROR',
|
|
253
|
+
message: decoded['message'] ?? 'Stream error',
|
|
254
|
+
));
|
|
255
|
+
} else {
|
|
256
|
+
_controller?.add(fromJson(decoded['data']));
|
|
257
|
+
}
|
|
258
|
+
} catch (e) {
|
|
259
|
+
_controller?.addError(e);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
onError: (e) => _controller?.addError(e),
|
|
263
|
+
onDone: () { if (!_disposed) _reconnect(); },
|
|
264
|
+
);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
_controller?.addError(e);
|
|
267
|
+
await Future.delayed(const Duration(seconds: 2));
|
|
268
|
+
if (!_disposed) await _connect();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
Future<void> _reconnect() async {
|
|
273
|
+
await Future.delayed(const Duration(seconds: 1));
|
|
274
|
+
if (!_disposed) await _connect();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
void _disconnect() => _socket?.close();
|
|
278
|
+
|
|
279
|
+
void dispose() {
|
|
280
|
+
_disposed = true;
|
|
281
|
+
_socket?.close();
|
|
282
|
+
_controller?.close();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ─── Generator ────────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
def generate(schema_url: str, output_dir: str, base_url: str = None):
|
|
291
|
+
"""
|
|
292
|
+
Pull schema from running Sagnos server.
|
|
293
|
+
Generate all Dart files into output_dir.
|
|
294
|
+
"""
|
|
295
|
+
print(f"Fetching schema from {schema_url} ...")
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
resp = requests.get(schema_url, timeout=5)
|
|
299
|
+
resp.raise_for_status()
|
|
300
|
+
schema = resp.json()
|
|
301
|
+
except requests.ConnectionError:
|
|
302
|
+
print("Could not connect. Is the Sagnos server running?")
|
|
303
|
+
return
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f"Error: {e}")
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
if base_url is None:
|
|
309
|
+
base_url = schema_url.replace("/sagnos/schema", "")
|
|
310
|
+
|
|
311
|
+
_write_files(schema, output_dir, base_url)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def generate_from_schema(schema: dict, output_dir: str, base_url: str = "http://127.0.0.1:8000"):
|
|
315
|
+
"""Generate Dart files directly from a schema dict — no server needed."""
|
|
316
|
+
_write_files(schema, output_dir, base_url)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _write_files(schema: dict, output_dir: str, base_url: str):
|
|
320
|
+
env = Environment(loader=BaseLoader(), trim_blocks=True, lstrip_blocks=True)
|
|
321
|
+
out = Path(output_dir)
|
|
322
|
+
hash = schema.get("schema_hash", "unknown")
|
|
323
|
+
|
|
324
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
|
|
326
|
+
ctx = {
|
|
327
|
+
"models": schema["models"],
|
|
328
|
+
"endpoints": schema["endpoints"],
|
|
329
|
+
"streams": schema.get("streams", []),
|
|
330
|
+
"base_url": base_url,
|
|
331
|
+
"schema_hash": hash,
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
files = {
|
|
335
|
+
"models.dart": MODELS_TEMPLATE,
|
|
336
|
+
"sagnos_client.dart": CLIENT_TEMPLATE,
|
|
337
|
+
"sagnos_exception.dart": EXCEPTION_TEMPLATE,
|
|
338
|
+
"sagnos_stream.dart": STREAM_TEMPLATE,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for filename, tmpl_str in files.items():
|
|
342
|
+
content = env.from_string(tmpl_str).render(**ctx)
|
|
343
|
+
(out / filename).write_text(content, encoding="utf-8")
|
|
344
|
+
print(f" Generated: {filename}")
|
|
345
|
+
|
|
346
|
+
# Write lock file
|
|
347
|
+
(out / "sagnos.lock").write_text(
|
|
348
|
+
f"schema_hash={hash}\n"
|
|
349
|
+
f"sagnos_version={schema['version']}\n",
|
|
350
|
+
encoding="utf-8"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
print(f"\nDone. {len(files)} files written to {output_dir}")
|
|
354
|
+
print(f"Schema hash: {hash}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sagnos/config.py
|
|
3
|
+
Project config — reads/writes sagnos.json
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from dataclasses import dataclass, asdict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CONFIG_FILE = "sagnos.json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class SagnosConfig:
|
|
16
|
+
name: str = "my_app"
|
|
17
|
+
version: str = "0.1.0"
|
|
18
|
+
port: int = 8000
|
|
19
|
+
host: str = "127.0.0.1"
|
|
20
|
+
backend_entry: str = "backend/main.py"
|
|
21
|
+
ui_dir: str = "ui"
|
|
22
|
+
dart_output: str = "ui/lib/sagnos"
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def load(cls, root: Path = None) -> "SagnosConfig":
|
|
26
|
+
root = root or Path.cwd()
|
|
27
|
+
config_path = root / CONFIG_FILE
|
|
28
|
+
if not config_path.exists():
|
|
29
|
+
raise FileNotFoundError(
|
|
30
|
+
f"No sagnos.json found in {root}.\n"
|
|
31
|
+
"Run `sagnos new <name>` to create a project."
|
|
32
|
+
)
|
|
33
|
+
with open(config_path) as f:
|
|
34
|
+
data = json.load(f)
|
|
35
|
+
return cls(**{k: v for k, v in data.items() if k in cls.__dataclass_fields__})
|
|
36
|
+
|
|
37
|
+
def save(self, root: Path = None):
|
|
38
|
+
root = root or Path.cwd()
|
|
39
|
+
with open(root / CONFIG_FILE, "w") as f:
|
|
40
|
+
json.dump(asdict(self), f, indent=2)
|