framework-m-studio 0.2.3__py3-none-any.whl → 0.3.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.
@@ -0,0 +1,435 @@
1
+ """Protocol Scanner - Extract Protocol definitions for documentation.
2
+
3
+ This module scans Python files for Protocol class definitions and extracts
4
+ their method signatures, docstrings, and adapter implementations.
5
+
6
+ Features:
7
+ - Parse Protocol classes using LibCST
8
+ - Extract method signatures and docstrings
9
+ - Parse adapter names from docstrings
10
+ - Generate markdown documentation
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import libcst as cst
21
+
22
+ # =============================================================================
23
+ # Data Models
24
+ # =============================================================================
25
+
26
+
27
+ @dataclass
28
+ class MethodInfo:
29
+ """Parsed method information from a Protocol class."""
30
+
31
+ name: str
32
+ signature: str
33
+ docstring: str | None = None
34
+
35
+
36
+ @dataclass
37
+ class ProtocolInfo:
38
+ """Parsed Protocol information."""
39
+
40
+ name: str
41
+ file_path: str
42
+ docstring: str | None = None
43
+ methods: list[MethodInfo] = field(default_factory=list)
44
+ adapters: list[str] = field(default_factory=list)
45
+
46
+
47
+ # =============================================================================
48
+ # LibCST Visitor for Protocol Parsing
49
+ # =============================================================================
50
+
51
+
52
+ class ProtocolParserVisitor(cst.CSTVisitor):
53
+ """LibCST visitor to extract Protocol information."""
54
+
55
+ def __init__(self) -> None:
56
+ self.protocols: list[ProtocolInfo] = []
57
+ self._current_class: str | None = None
58
+ self._current_docstring: str | None = None
59
+ self._current_methods: list[MethodInfo] = []
60
+ self._is_protocol: bool = False
61
+
62
+ def visit_ClassDef(self, node: cst.ClassDef) -> bool:
63
+ """Visit class definition."""
64
+ class_name = node.name.value
65
+
66
+ # Check if this is a Protocol class
67
+ is_protocol = False
68
+ for arg in node.bases:
69
+ base_name = _get_base_name(arg)
70
+ if "Protocol" in base_name:
71
+ is_protocol = True
72
+ break
73
+
74
+ if is_protocol:
75
+ self._current_class = class_name
76
+ self._is_protocol = True
77
+ self._current_methods = []
78
+ self._current_docstring = _extract_docstring(node)
79
+
80
+ return True
81
+
82
+ def leave_ClassDef(self, node: cst.ClassDef) -> None:
83
+ """Leave class definition."""
84
+ if self._current_class == node.name.value and self._is_protocol:
85
+ # Extract adapters from docstring
86
+ adapters = _extract_adapters(self._current_docstring)
87
+
88
+ protocol = ProtocolInfo(
89
+ name=self._current_class,
90
+ file_path="",
91
+ docstring=self._current_docstring,
92
+ methods=self._current_methods,
93
+ adapters=adapters,
94
+ )
95
+ self.protocols.append(protocol)
96
+
97
+ self._current_class = None
98
+ self._is_protocol = False
99
+ self._current_methods = []
100
+ self._current_docstring = None
101
+
102
+ def visit_FunctionDef(self, node: cst.FunctionDef) -> bool:
103
+ """Visit function/method definition."""
104
+ if self._is_protocol and self._current_class:
105
+ method_name = node.name.value
106
+ if not method_name.startswith("_"):
107
+ # Get full signature
108
+ signature = _build_signature(node)
109
+ method_docstring = _extract_function_docstring(node)
110
+
111
+ method = MethodInfo(
112
+ name=method_name,
113
+ signature=signature,
114
+ docstring=method_docstring,
115
+ )
116
+ self._current_methods.append(method)
117
+
118
+ return False
119
+
120
+
121
+ # =============================================================================
122
+ # Helper Functions
123
+ # =============================================================================
124
+
125
+
126
+ def _get_base_name(arg: cst.Arg) -> str:
127
+ """Get base class name from Arg node."""
128
+ if isinstance(arg.value, cst.Name):
129
+ return arg.value.value
130
+ if isinstance(arg.value, cst.Subscript) and isinstance(arg.value.value, cst.Name):
131
+ return arg.value.value.value
132
+ return ""
133
+
134
+
135
+ def _extract_docstring(node: cst.ClassDef) -> str | None:
136
+ """Extract docstring from class definition."""
137
+ if not node.body or not node.body.body:
138
+ return None
139
+
140
+ first_stmt = node.body.body[0]
141
+ if not isinstance(first_stmt, cst.SimpleStatementLine):
142
+ return None
143
+
144
+ if not first_stmt.body or not isinstance(first_stmt.body[0], cst.Expr):
145
+ return None
146
+
147
+ expr = first_stmt.body[0].value
148
+ if isinstance(expr, cst.SimpleString):
149
+ value = expr.value
150
+ if value.startswith('"""') or value.startswith("'''"):
151
+ return value[3:-3].strip()
152
+ elif value.startswith('"') or value.startswith("'"):
153
+ return value[1:-1].strip()
154
+
155
+ return None
156
+
157
+
158
+ def _extract_function_docstring(node: cst.FunctionDef) -> str | None:
159
+ """Extract docstring from function definition."""
160
+ if not node.body or not node.body.body:
161
+ return None
162
+
163
+ first_stmt = node.body.body[0]
164
+ if not isinstance(first_stmt, cst.SimpleStatementLine):
165
+ return None
166
+
167
+ if not first_stmt.body or not isinstance(first_stmt.body[0], cst.Expr):
168
+ return None
169
+
170
+ expr = first_stmt.body[0].value
171
+ if isinstance(expr, cst.SimpleString):
172
+ value = expr.value
173
+ if value.startswith('"""') or value.startswith("'''"):
174
+ return value[3:-3].strip()
175
+ elif value.startswith('"') or value.startswith("'"):
176
+ return value[1:-1].strip()
177
+
178
+ return None
179
+
180
+
181
+ def _build_signature(node: cst.FunctionDef) -> str:
182
+ """Build method signature string."""
183
+ # Get async prefix
184
+ is_async = node.asynchronous is not None
185
+ prefix = "async def " if is_async else "def "
186
+
187
+ # Get function name and parameters
188
+ name = node.name.value
189
+ params = cst.Module(body=[]).code_for_node(node.params)
190
+
191
+ # Get return type if available
192
+ return_type = ""
193
+ if node.returns:
194
+ return_type = " -> " + cst.Module(body=[]).code_for_node(
195
+ node.returns.annotation
196
+ )
197
+
198
+ return f"{prefix}{name}({params}){return_type}"
199
+
200
+
201
+ def _extract_adapters(docstring: str | None) -> list[str]:
202
+ """Extract adapter names from Protocol docstring.
203
+
204
+ Looks for patterns like:
205
+ - Implementations:
206
+ - SomeAdapter: Description
207
+ """
208
+ if not docstring:
209
+ return []
210
+
211
+ adapters: list[str] = []
212
+
213
+ # Look for adapter names after "Implementations:" line
214
+ in_implementations = False
215
+ for line in docstring.split("\n"):
216
+ line = line.strip()
217
+
218
+ if "Implementations:" in line or "Implementation:" in line:
219
+ in_implementations = True
220
+ continue
221
+
222
+ if in_implementations:
223
+ # Check for "- AdapterName: description" pattern
224
+ match = re.match(r"-\s*(\w+)(?:Adapter|Cache|Repository)?:", line)
225
+ if match:
226
+ # Extract the full adapter name
227
+ adapter_match = re.match(r"-\s*(\w+):", line)
228
+ if adapter_match:
229
+ adapters.append(adapter_match.group(1))
230
+ elif line.startswith("-"):
231
+ # Simple "- AdapterName" pattern
232
+ adapter_match = re.match(r"-\s*(\w+)", line)
233
+ if adapter_match:
234
+ adapters.append(adapter_match.group(1))
235
+ elif not line:
236
+ # Empty line may end the list
237
+ if adapters:
238
+ break
239
+
240
+ return adapters
241
+
242
+
243
+ # =============================================================================
244
+ # Public API
245
+ # =============================================================================
246
+
247
+
248
+ def parse_protocol_file(file_path: Path) -> list[dict[str, Any]]:
249
+ """Parse a Python file and extract Protocol definitions.
250
+
251
+ Args:
252
+ file_path: Path to the Python file
253
+
254
+ Returns:
255
+ List of protocol info dictionaries
256
+ """
257
+ source = file_path.read_text(encoding="utf-8")
258
+
259
+ try:
260
+ tree = cst.parse_module(source)
261
+ except cst.ParserSyntaxError:
262
+ return []
263
+
264
+ visitor = ProtocolParserVisitor()
265
+ tree.visit(visitor)
266
+
267
+ result = []
268
+ for proto in visitor.protocols:
269
+ proto.file_path = str(file_path.absolute())
270
+ result.append(protocol_to_dict(proto))
271
+
272
+ return result
273
+
274
+
275
+ def scan_protocols(interfaces_dir: Path) -> list[dict[str, Any]]:
276
+ """Scan a directory for Protocol definitions.
277
+
278
+ Args:
279
+ interfaces_dir: Path to interfaces directory
280
+
281
+ Returns:
282
+ List of all protocol info dictionaries
283
+ """
284
+ protocols: list[dict[str, Any]] = []
285
+
286
+ for py_file in interfaces_dir.glob("*.py"):
287
+ if py_file.name.startswith("_"):
288
+ continue
289
+
290
+ file_protocols = parse_protocol_file(py_file)
291
+ protocols.extend(file_protocols)
292
+
293
+ return protocols
294
+
295
+
296
+ def generate_protocol_markdown(protocol_info: dict[str, Any]) -> str:
297
+ """Generate markdown documentation for a Protocol.
298
+
299
+ Args:
300
+ protocol_info: Protocol information dictionary
301
+
302
+ Returns:
303
+ Markdown string
304
+ """
305
+ from pathlib import Path
306
+
307
+ name = protocol_info.get("name", "Unknown")
308
+ docstring = protocol_info.get("docstring", "")
309
+ methods = protocol_info.get("methods", [])
310
+ adapters = protocol_info.get("adapters", [])
311
+ file_path = protocol_info.get("file_path", "")
312
+
313
+ lines = [f"# {name}", ""]
314
+
315
+ if docstring:
316
+ # Clean up the docstring - remove the "Implementations:" section
317
+ clean_docstring = _clean_docstring_for_markdown(docstring)
318
+ if clean_docstring:
319
+ lines.extend([clean_docstring, ""])
320
+
321
+ # Source file link
322
+ if file_path:
323
+ filename = Path(file_path).name
324
+ file_uri = f"file://{file_path}"
325
+ lines.extend([f"**Source**: [{filename}]({file_uri})", ""])
326
+
327
+ # Methods section
328
+ if methods:
329
+ lines.extend(["## Methods", ""])
330
+ for method in methods:
331
+ lines.append(f"### `{method['name']}`")
332
+ lines.append("")
333
+ lines.append("```python")
334
+ lines.append(method["signature"])
335
+ lines.append("```")
336
+ lines.append("")
337
+ if method.get("docstring"):
338
+ lines.extend([method["docstring"], ""])
339
+
340
+ # Adapters section
341
+ if adapters:
342
+ lines.extend(["## Adapters", ""])
343
+ for adapter in adapters:
344
+ lines.append(f"- `{adapter}`")
345
+ lines.append("")
346
+
347
+ return "\n".join(lines)
348
+
349
+
350
+ def _clean_docstring_for_markdown(docstring: str) -> str:
351
+ """Remove Implementations section from docstring for markdown."""
352
+ lines = docstring.split("\n")
353
+ result_lines = []
354
+ skip_until_empty = False
355
+
356
+ for line in lines:
357
+ if "Implementations:" in line or "Implementation:" in line:
358
+ skip_until_empty = True
359
+ continue
360
+
361
+ if skip_until_empty:
362
+ if line.strip().startswith("-"):
363
+ continue
364
+ if not line.strip():
365
+ skip_until_empty = False
366
+ continue
367
+
368
+ result_lines.append(line)
369
+
370
+ return "\n".join(result_lines).strip()
371
+
372
+
373
+ def protocol_to_dict(protocol: ProtocolInfo) -> dict[str, Any]:
374
+ """Convert ProtocolInfo to dictionary."""
375
+ return {
376
+ "name": protocol.name,
377
+ "file_path": protocol.file_path,
378
+ "docstring": protocol.docstring,
379
+ "methods": [
380
+ {
381
+ "name": m.name,
382
+ "signature": m.signature,
383
+ "docstring": m.docstring,
384
+ }
385
+ for m in protocol.methods
386
+ ],
387
+ "adapters": protocol.adapters,
388
+ }
389
+
390
+
391
+ def generate_comparison_table(
392
+ protocols: list[dict[str, Any]],
393
+ indie_adapters: set[str],
394
+ ) -> str:
395
+ """Generate Indie vs Enterprise comparison table.
396
+
397
+ Args:
398
+ protocols: List of protocol info dicts
399
+ indie_adapters: Set of adapter names considered "Indie" (simple, local)
400
+
401
+ Returns:
402
+ Markdown table string
403
+ """
404
+ lines = [
405
+ "# Protocol Adapter Comparison",
406
+ "",
407
+ "| Protocol | Indie | Enterprise |",
408
+ "|----------|-------|------------|",
409
+ ]
410
+
411
+ for proto in sorted(protocols, key=lambda p: p["name"]):
412
+ name = proto["name"]
413
+ adapters = proto.get("adapters", [])
414
+
415
+ indie_list = [a for a in adapters if a in indie_adapters]
416
+ enterprise_list = [a for a in adapters if a not in indie_adapters]
417
+
418
+ indie_str = ", ".join(f"`{a}`" for a in indie_list) or "-"
419
+ enterprise_str = ", ".join(f"`{a}`" for a in enterprise_list) or "-"
420
+
421
+ lines.append(f"| {name} | {indie_str} | {enterprise_str} |")
422
+
423
+ lines.append("")
424
+ return "\n".join(lines)
425
+
426
+
427
+ __all__ = [
428
+ "MethodInfo",
429
+ "ProtocolInfo",
430
+ "generate_comparison_table",
431
+ "generate_protocol_markdown",
432
+ "parse_protocol_file",
433
+ "protocol_to_dict",
434
+ "scan_protocols",
435
+ ]
@@ -47,6 +47,10 @@ class FieldSchema(BaseModel):
47
47
  description: str | None = None
48
48
  hidden: bool = False
49
49
  read_only: bool = False
50
+ link_doctype: str | None = None # For Link field type - which DocType to link to
51
+ table_doctype: str | None = (
52
+ None # For Table field type - which DocType for child table
53
+ )
50
54
  validators: ValidatorSchema | None = None
51
55
 
52
56
 
@@ -193,6 +197,8 @@ class DocTypeController(Controller):
193
197
  "required": f.required,
194
198
  "label": f.label,
195
199
  "description": f.description,
200
+ "link_doctype": f.link_doctype, # Include Link field metadata
201
+ "table_doctype": f.table_doctype, # Include Table field metadata
196
202
  "validators": clean_validators(f.validators),
197
203
  }
198
204
  for f in doctype.fields
@@ -255,19 +261,37 @@ class DocTypeController(Controller):
255
261
  old_controller.unlink()
256
262
  break
257
263
 
258
- # Determine target directory
259
- if data.app:
260
- # Use specified app directory
261
- target_dir = project_root / data.app / "src"
262
- if not target_dir.exists():
263
- target_dir = project_root / data.app
264
+ # Determine target directory - use namespaced structure: src/<app>/doctypes/
265
+ app_name = data.app
266
+
267
+ # Try to detect app name from pyproject.toml if not provided
268
+ if not app_name:
269
+ pyproject = project_root / "pyproject.toml"
270
+ if pyproject.exists():
271
+ import re
272
+
273
+ content = pyproject.read_text()
274
+ match = re.search(r'name\s*=\s*["\']([^"\']+)["\']', content)
275
+ if match:
276
+ app_name = match.group(1).replace("-", "_")
277
+
278
+ if app_name:
279
+ # Namespaced structure: src/<app>/doctypes/<doctype>/
280
+ namespace_dir = project_root / "src" / app_name / "doctypes"
281
+ if (
282
+ namespace_dir.exists()
283
+ or not (project_root / "src" / "doctypes").exists()
284
+ ):
285
+ target_dir = project_root / "src" / app_name
286
+ else:
287
+ # Fallback to legacy flat structure if it exists
288
+ target_dir = project_root / "src"
264
289
  else:
265
- # Default to src/
266
290
  target_dir = project_root / "src"
267
291
  if not target_dir.exists():
268
292
  target_dir = project_root
269
293
 
270
- # Create folder structure: src/doctypes/<name>/
294
+ # Create folder structure: src/<app>/doctypes/<name>/ (namespaced)
271
295
  doctype_folder = target_dir / "doctypes" / sanitized_folder_name
272
296
  doctype_folder.mkdir(parents=True, exist_ok=True)
273
297
 
@@ -283,9 +307,11 @@ class DocTypeController(Controller):
283
307
  init_code = _generate_init_code(sanitized_class_name)
284
308
  (doctype_folder / "__init__.py").write_text(init_code, encoding="utf-8")
285
309
 
286
- # Generate and write test file
310
+ # Generate and write test file in separate tests/ directory
287
311
  test_code = _generate_test_code(sanitized_class_name, sanitized_folder_name)
288
- (doctype_folder / f"test_{sanitized_folder_name}.py").write_text(
312
+ tests_dir = project_root / "tests" / "doctypes" / sanitized_folder_name
313
+ tests_dir.mkdir(parents=True, exist_ok=True)
314
+ (tests_dir / f"test_{sanitized_folder_name}.py").write_text(
289
315
  test_code, encoding="utf-8"
290
316
  )
291
317
 
@@ -399,6 +425,8 @@ def _generate_doctype_code(schema: CreateDocTypeRequest) -> str:
399
425
  "label": f.label,
400
426
  "default": f.default,
401
427
  "description": f.description,
428
+ "link_doctype": f.link_doctype, # Preserve Link field metadata
429
+ "table_doctype": f.table_doctype, # Preserve Table field metadata
402
430
  "validators": (
403
431
  {
404
432
  "min_length": f.validators.min_length,
@@ -432,7 +460,7 @@ def _generate_controller_code(name: str) -> str:
432
460
  "",
433
461
  "from __future__ import annotations",
434
462
  "",
435
- "from framework_m.core.domain.base_controller import BaseController",
463
+ "from framework_m_core.domain.base_controller import BaseController",
436
464
  f"from .doctype import {name}",
437
465
  "",
438
466
  "",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: framework-m-studio
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: Framework M Studio - Visual DocType Builder & Developer Tools
5
5
  Project-URL: Homepage, https://gitlab.com/castlecraft/framework-m
6
6
  Project-URL: Documentation, https://gitlab.com/castlecraft/framework-m#readme
@@ -17,7 +17,9 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.12
20
- Requires-Dist: framework-m
20
+ Requires-Dist: framework-m-core>=0.5.0
21
+ Requires-Dist: framework-m>=0.4.2
22
+ Requires-Dist: honcho>=1.1.0
21
23
  Requires-Dist: jinja2>=3.1.0
22
24
  Requires-Dist: libcst>=1.0.0
23
25
  Description-Content-Type: text/markdown
@@ -27,7 +29,10 @@ Description-Content-Type: text/markdown
27
29
  Visual DocType builder and developer tools for Framework M.
28
30
 
29
31
  [![PyPI version](https://badge.fury.io/py/framework-m-studio.svg)](https://badge.fury.io/py/framework-m-studio)
32
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
33
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
34
  [![GitLab Pipeline Status](https://gitlab.com/castlecraft/framework-m/badges/main/pipeline.svg)](https://gitlab.com/castlecraft/framework-m/-/pipelines)
35
+ [![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)
31
36
 
32
37
  ## Overview
33
38
 
@@ -0,0 +1,32 @@
1
+ framework_m_studio/__init__.py,sha256=9c5EY-sChvaWoZt4FS38xsdckvapt4hO3SsziAb23io,589
2
+ framework_m_studio/app.py,sha256=rzN7R-YzjysRVFDVVTX__K9zZ7fCMDmuYT75rvgVyp0,10412
3
+ framework_m_studio/checklist_parser.py,sha256=IdTtr1hMUd19ySx2hxknEcBQP-B08TfK7k0mgPdaKNc,12079
4
+ framework_m_studio/discovery.py,sha256=bqs9KZDzNhyAMYTgCa9FAce0OUYys3c_gN6i2Xr__BI,6123
5
+ framework_m_studio/docs_generator.py,sha256=okxMvD2BOqyLyAo52m6ZqUIovbcnTq3v2D9u9ZEjHDo,19074
6
+ framework_m_studio/protocol_scanner.py,sha256=oNlxU3wInWjeVkZwJM-hDuRZneU69Y9cXtLLPIg5BDs,12923
7
+ framework_m_studio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ framework_m_studio/routes.py,sha256=ViYaRdS9NxBuUwXIE_Fdg7mzu_fH2TTyQnMReykV5Yk,18979
9
+ framework_m_studio/sdk_generator.py,sha256=L48vURRZauURDmB_F_yYs01kauvnt2SvWkSv3EgzrD8,7283
10
+ framework_m_studio/workspace.py,sha256=VSJfX3WWXLGKBZ-lmQmw3IBtrLUjtI0W8vxJoZa1CNY,8234
11
+ framework_m_studio/cli/__init__.py,sha256=b-MsMBgRaYOBBFiL_DBKJXpHVeN6-dYhd4RvjsZnN7U,21589
12
+ framework_m_studio/cli/build.py,sha256=b3RftIvTW9pyj0CSRj_XMpRewYzI1ZVjcLywHlh2X8g,12360
13
+ framework_m_studio/cli/dev.py,sha256=lPTI4Ou5g3HGF6JugNpYg5TtelnLNeOjXip5FMbtMes,6135
14
+ framework_m_studio/cli/new.py,sha256=piUeWmidT0Ff4LW_jRZ9ZkWTCOFvjZJxIm83cB3N5js,21146
15
+ framework_m_studio/cli/quality.py,sha256=vuGAavMKlDymk4oel3jHLVKONCf-Sn17NWRjkTSkd6s,3796
16
+ framework_m_studio/cli/studio.py,sha256=-44TRjvb9OMQiHWTWW-_7Gi_7jOoLJ97__pf_8tKfCU,3798
17
+ framework_m_studio/cli/utility.py,sha256=XQahBScWf6F9RdojTatI_MjtClNvpVo7aC1tbku2nVg,1051
18
+ framework_m_studio/codegen/__init__.py,sha256=t7DB_zR1DEovL9TYXbsDyQuMyMBTG6YLrVt5wK1OeLI,814
19
+ framework_m_studio/codegen/generator.py,sha256=EbyPlcffJgDIHi7tFFMNOkXRolQj-YHHTA0ZLPE21HQ,9103
20
+ framework_m_studio/codegen/parser.py,sha256=R5w51eQaG2BBPvWBDdLQMdNNs39ybzeojj68fTdd1jU,23415
21
+ framework_m_studio/codegen/test_generator.py,sha256=vPlIsdekM-Siok7nSiMGVug5eEZM141B0WSAuXRVYXQ,10928
22
+ framework_m_studio/codegen/transformer.py,sha256=Ux8-A6UqRaVWKcf_y079WhTchciUunv-CocPXjC326I,12144
23
+ framework_m_studio/codegen/templates/doctype.py.jinja2,sha256=rEYseoOrj7U1Wrqc1ATa0JLHZCGbe9FC6nOnC6xaN7A,2839
24
+ framework_m_studio/codegen/templates/test_doctype.py.jinja2,sha256=Zeo1Mj67pkVIVfcJWo3VgPtaZDOoWeRKAAyHag1nW0I,1614
25
+ framework_m_studio/git/__init__.py,sha256=wsRJ77pUCS081CCN2hx6EpgU9DfY-Uel6cnQqcwBpU0,35
26
+ framework_m_studio/git/adapter.py,sha256=hdpsRPS4wbOA6gX7V9Q4bsmuykmLI2UnTUuwATKhBOU,8973
27
+ framework_m_studio/git/github_provider.py,sha256=Lw_-1Pxm19VKArH4f5e0-la0Vi1HGVCuXnrTbEyR_9s,8522
28
+ framework_m_studio/git/protocol.py,sha256=GyswO1ZlL1BzqM9pAQA7XRj8X1tw04E3jRYaUhdz-Ew,5665
29
+ framework_m_studio-0.3.0.dist-info/METADATA,sha256=mWbWZfp5TqgGKjnqA-FTxxqom9arj7GyiZ3if83D4YU,2463
30
+ framework_m_studio-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
31
+ framework_m_studio-0.3.0.dist-info/entry_points.txt,sha256=hc1m9Lf-_4GQcugLyBjWg56SpyhJTaOHKDc1bgkxFfI,885
32
+ framework_m_studio-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,18 @@
1
+ [framework_m_core.cli_commands]
2
+ codegen = framework_m_studio.cli:codegen_app
3
+ console = framework_m_studio.cli.utility:console_command
4
+ dev = framework_m_studio.cli.dev:dev_command
5
+ docs = framework_m_studio.cli:docs_app
6
+ docs:adr = framework_m_studio.cli:docs_adr
7
+ docs:export = framework_m_studio.cli:docs_export
8
+ docs:generate = framework_m_studio.cli:docs_generate
9
+ docs:rfc = framework_m_studio.cli:docs_rfc
10
+ format = framework_m_studio.cli.quality:format_command
11
+ lint = framework_m_studio.cli.quality:lint_command
12
+ new = framework_m_studio.cli.new:new_app_command
13
+ new:app = framework_m_studio.cli.new:new_app_command
14
+ new:doctype = framework_m_studio.cli.new:new_doctype_command
15
+ routes = framework_m_studio.cli.utility:routes_command
16
+ studio = framework_m_studio.cli:studio_app
17
+ test = framework_m_studio.cli.quality:test_command
18
+ typecheck = framework_m_studio.cli.quality:typecheck_command