codeplain 0.3.4__py3-none-any.whl → 0.3.6__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.
_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.4"
1
+ __version__ = "0.3.6"
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.4
2
+ Name: codeplain
3
+ Version: 0.3.6
4
+ Summary: Transform plain language specifications into working code
5
+ License-File: LICENSE
6
+ Classifier: Environment :: Console
7
+ Classifier: Intended Audience :: Developers
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Software Development :: Code Generators
10
+ Requires-Python: ==3.11.*
11
+ Requires-Dist: gitpython==3.1.50
12
+ Requires-Dist: mistletoe==1.3.0
13
+ Requires-Dist: networkx==3.6.1
14
+ Requires-Dist: python-frontmatter==1.3.0
15
+ Requires-Dist: python-liquid2==0.4.0
16
+ Requires-Dist: pyyaml==6.0.3
17
+ Requires-Dist: requests==2.34.2
18
+ Requires-Dist: rich==15.0.0
19
+ Requires-Dist: sentry-sdk==2.63.0
20
+ Requires-Dist: textual>=7.5.0
21
+ Requires-Dist: tiktoken==0.13.0
22
+ Requires-Dist: transitions==0.9.3
23
+ Provides-Extra: dev
24
+ Requires-Dist: black==26.3.1; extra == 'dev'
25
+ Requires-Dist: flake8==7.3.0; extra == 'dev'
26
+ Requires-Dist: isort==5.13.2; extra == 'dev'
27
+ Requires-Dist: mypy==2.1.0; extra == 'dev'
28
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # *codeplain renderer
32
+
33
+ ## What is \*codeplain?
34
+
35
+ \*codeplain platform makes coding agents better by having them write and maintain specifications instead of code. It extends coding agents with agentic skills for working at the specification layer, and with tools that convert specifications into tested and validated implementation code.
36
+
37
+ This repository is the renderer that powers that last step, turning \*\*\*plain specs into software using the *codeplain API.
38
+
39
+ The result is a software development workflow centered on specifications, which are faster and more efficient for AI to produce and easier for developers to review and maintain than raw code.
40
+
41
+ Schematic overview of the *codeplain's code rendering:
42
+
43
+ <img src="https://raw.githubusercontent.com/Codeplain-ai/plain2code_client/main/resources/codeplain_overview.png">
44
+
45
+ ## Getting started
46
+
47
+ Get started in 15 minutes.
48
+
49
+ ### 1. Install \*codeplain in the terminal
50
+
51
+ macOS and Linux:
52
+
53
+ ```bash
54
+ curl -fsSL https://codeplain.ai/install.sh | bash
55
+ ```
56
+
57
+ Windows PowerShell:
58
+
59
+ ```powershell
60
+ irm https://codeplain.ai/install.ps1 | iex
61
+ ```
62
+
63
+ **Windows users:** Please install WSL (Windows Subsystem for Linux) as this is currently the supported environment for running plain code on Windows.
64
+
65
+ Follow the installation instructions to complete setup. The installer installs the `codeplain` CLI, walks you through setting your `CODEPLAIN_API_KEY`, and offers to set up [plain-forge](#2-let-your-agent-work-in-specs-not-code) and the [plyn editor extension](#vs-code--cursor-extension) for you in the same session.
66
+
67
+ If you'd rather not run the installer, sign up at [platform.codeplain.ai](https://platform.codeplain.ai) to get your API key and export it yourself:
68
+
69
+ ```bash
70
+ export CODEPLAIN_API_KEY="your_actual_api_key_here"
71
+ ```
72
+
73
+ ### 2. Let your agent work in specs, not code
74
+
75
+ You describe what you want in plain English, an agent helps you write a spec in \*\*\*plain language, and \*codeplain renders the actual code. This is what [plain-forge](https://github.com/Codeplain-ai/plain-forge) sets up: a toolkit of skills, rules, and docs that plugs into your AI coding agent of choice — Claude Code, Codex, ForgeCode, OpenCode, or any other agent that reads from a standard skills directory — and turns a conversation into a complete `.plain` spec, then keeps maintaining it across the lifetime of the project.
76
+
77
+ If you didn't install it during step 1, run it any time:
78
+
79
+ ```bash
80
+ npx plain-forge install
81
+ ```
82
+
83
+ ### 3. Convert specs into tested and validated code
84
+
85
+ \*codeplain extends coding agents with agentic skills for working at the specification layer, and with tools that convert specifications into tested and validated implementation code.
86
+
87
+ ```bash
88
+ codeplain your-project.plain
89
+ ```
90
+
91
+ ### Try it now
92
+
93
+ You can immediately test the system with a simple "Hello World" example:
94
+
95
+ - Change to the example folder and run the example:
96
+
97
+ ```bash
98
+ cd examples/example_hello_world_python
99
+ codeplain hello_world_python.plain
100
+ ```
101
+
102
+ _Note: Rendering will take a few minutes to complete._
103
+
104
+ - The system will generate a Python application in the `build` directory. You can run it with:
105
+ ```bash
106
+ cd build
107
+ python hello_world.py
108
+ ```
109
+
110
+ ### VS Code / Cursor extension
111
+
112
+ If you're using Visual Studio Code or Cursor IDE, we also recommend installing the [plyn extension](https://marketplace.visualstudio.com/items?itemName=Codeplain.plyn) for `***plain` syntax highlighting. The installer in step 1 offers to install it automatically if it detects `code` or `cursor` on your `PATH`.
113
+
114
+ ### Community and support
115
+
116
+ Share what you're building! Join the [\*codeplain Discord](https://discord.gg/cgbynb9hFq) if you have questions, ideas, or want to share what you built.
117
+
118
+ Hit an edge case or have feedback? Reach us at [support@codeplain.ai](mailto:support@codeplain.ai).
119
+
120
+ ## Abstracting Away Code Generation Complexity with \*\*\*plain
121
+
122
+ \*\*\*plain is the language of spec-driven development — a specification language that combines the efficiency of natural language with the control and precision of code.
123
+
124
+ Structured sections for definitions, functional specs, implementation requirements, test requirements, and acceptance tests let your specs (not scattered docs or ad hoc prompts) become the source of truth for your software's behavior, abstracting away the complexity of using large language models for code generation.
125
+
126
+ Specs are plain text, so they fit into the workflows you already use — version control, pull requests, and code review.
127
+
128
+ See the [\*\*\*plain language guide](https://www.plainlang.org/docs/language-guide/) to learn the full syntax.
129
+
130
+ An example application in \*\*\*plain
131
+
132
+ <img src="resources/plain_example.png" width="70%" height="70%">
133
+
134
+ ## Additional Resources
135
+
136
+ ### Examples and Sample Projects
137
+
138
+ - See the [examples](examples) folder for sample projects in Golang, Python, React, and Flutter.
139
+ - For an example of how to implement Pong in \*\*\*plain, see the [pong](https://github.com/Codeplain-ai/plainlang-examples/tree/main/pong) example.
140
+ - For an example of how to implement a task manager in \*\*\*plain, see the [task-manager](https://github.com/Codeplain-ai/plainlang-examples/tree/main/task-manager) example.
141
+
142
+ ### Documentation
143
+
144
+ - For more details on the ***plain format, see the [***plain language specification](https://www.plainlang.org/docs/).
@@ -1,28 +1,28 @@
1
- _version.py,sha256=oYLGMpySamd16KLiaBTfRyrAS7_oyp-TOEHmzmeumwg,22
1
+ _version.py,sha256=W_9dCm49nLvZulVAvvsafxLJjVBSKDBHz9K7szFZllo,22
2
2
  change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
3
3
  codeplain_REST_api.py,sha256=h_XoNws8fvf7OTtoVXhHh6OfLGg77UNFuggMBwaqBwE,21533
4
4
  concept_utils.py,sha256=D4qVHFBRvfJZwiIq1AD_SV0Ev-RnOMg2yGWo6Cal7PM,7913
5
5
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
6
6
  event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
7
- file_utils.py,sha256=grPx2Rl6-q2ijSRoaAOZcsrpOjTqb4i_mtF-jzwhKcQ,10953
7
+ file_utils.py,sha256=q-ApwiLLJCgmmoy6-nf1QM9DaFXwzCJek6B-jQTI9ak,11665
8
8
  git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
9
9
  memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
10
10
  module_renderer.py,sha256=pXMXKKGMhUwLLlSxppE_hpZlwpJ3WSHSfR9FQxt6rSI,6860
11
11
  partial_rendering.py,sha256=0ChOaGytogBUEGISpbWkbjT9Hqy8pnOkJdJXcitJDgk,12219
12
- plain2code.py,sha256=Yi6W9R3HNedbaeIgfFffQnfauwTXmJTi8ZRD0LdmfvA,14042
12
+ plain2code.py,sha256=7VUE_q06VQinFHNfyh1I2u2Nt3EOa6WPkiPfqEF6x58,15452
13
13
  plain2code_arguments.py,sha256=J05NwaY-O04rIWVF2yIXysNB_ofZS0wJFn0u1WaCCSA,15399
14
14
  plain2code_console.py,sha256=YdE-Kz9qRAzIIAg7aYA1bnXRcwy_aTpbFtkR2zHbd-I,4918
15
15
  plain2code_events.py,sha256=AM8KJS68uiVXRMLt861KaGhs2H4yzujlMB_xiYPfHQc,1478
16
- plain2code_exceptions.py,sha256=Fj1pwCffn06aivEplsTJJGdg6lAOR8gmWd4ctLl9cj4,1745
17
- plain2code_logger.py,sha256=kNKIfkS6Ydtut7Ptw5jmv5bFPgXlxB-_Ds5nd-j4d9o,4609
16
+ plain2code_exceptions.py,sha256=wkkEXw2k4LCd09PIH3_Hu0HyZvtXYjFaA653Sfey4FA,2287
17
+ plain2code_logger.py,sha256=LdCaS29gWA7e1VhQijyXagsrFAnEi7VKU62b1dnlKnc,5114
18
18
  plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
19
19
  plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
20
- plain2code_state.py,sha256=gIHOmJe9Ml_Xifi6uV5VBMK3zXRpaRoqQNoMhjD1dvQ,2187
21
- plain2code_telemetry.py,sha256=ZtBgdpWmPZQ_SnC0O9A2XSG30kRJ9kWitQPN4mmT2S8,4245
22
- plain2code_utils.py,sha256=HYDJ4lTC3RwBgcJadPvcoU3-Y6krNiuDP4_t2xYKA2Q,874
23
- plain_file.py,sha256=VUfMCskTJZnHrFYgBlK8DXbJGnk7peuC6YY22zRNAek,29968
20
+ plain2code_state.py,sha256=1VwyxgKdA13fAJNKSiPBfBb3A20TKfGCNnOm06YdcS4,2233
21
+ plain2code_telemetry.py,sha256=7yuTH7mQE_wGVW2H1oiVZ3UJg98ArB5HcUUm9isnv9g,4354
22
+ plain2code_utils.py,sha256=id04t9Iez5pYygrE3fKEyrcfWDcniKFT2iQoF2CzTJs,1409
23
+ plain_file.py,sha256=xdaDm5BL2dIhwMQeyq9nTXsugS4PDnR25CyrKBtHlLU,34386
24
24
  plain_modules.py,sha256=uvRPfxBJwstaWi5lgYKgtQdhhf0Naiy2mS41tLw_TjY,16186
25
- plain_spec.py,sha256=E2Yp2eXMExE7kx7GPQ7v9boHN8XhFUcqprn1DGMaFMs,15669
25
+ plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
26
26
  system_config.py,sha256=trgytfXKXoCn0Pgy9u00x2D0ZRHlh_tADlbkwZnF8QE,1038
27
27
  cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
28
28
  cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
@@ -36,10 +36,10 @@ render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
37
37
  render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
38
38
  render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
39
- render_machine/render_context.py,sha256=5W_PVrlPZMPlWmXlHQtYUiCIFvjNFsjNiJ42z5Hidv8,28447
39
+ render_machine/render_context.py,sha256=fjU_S5W5HoAX3-WlEM8hp4DKMX_NDgVlfwQUAp0yaf8,25633
40
40
  render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
41
41
  render_machine/render_utils.py,sha256=ao0e64VSOGYfb8hwJXIZlbqer5HjNMYJWHlLDoHnJyc,9123
42
- render_machine/state_machine_config.py,sha256=F-pSrNPF31eYMmJMo0STj85q7rp-4qFyy7kEmJ8iVc4,26944
42
+ render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
43
43
  render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
44
44
  render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
45
45
  render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySxbpKCgt11nMpZhOK0sN-J66M42T5Y,2847
@@ -49,17 +49,16 @@ render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgW
49
49
  render_machine/actions/create_dist.py,sha256=xMPPyws7swVkaUSQg3dwMGScCi1BuII9y2C_XSP0N80,1067
50
50
  render_machine/actions/exit_with_error.py,sha256=-LkQsFhmV-Nr741APTx38dqu473b6OSiIvKayFFsXxI,1270
51
51
  render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
52
- render_machine/actions/fix_conformance_test.py,sha256=ePfxz9Xm0IXht0ch7H4tLPdRzoKnVdlbcDC9EgeDAAQ,8786
52
+ render_machine/actions/fix_conformance_test.py,sha256=jtVeL11J8K-a5YvoR3uW6TVytQZkiBgXlDyFR3wXYEs,10022
53
53
  render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
54
54
  render_machine/actions/prepare_repositories.py,sha256=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
55
- render_machine/actions/prepare_testing_environment.py,sha256=GQNVzc_Ap5n74OAmeQuAhXXHK2FVVY374Z0lutrN2UQ,1867
56
- render_machine/actions/refactor_code.py,sha256=_1C16bQbpxdzJkEwl8jDhEKVyXcyGTd-5YYDNTq2vA8,1782
55
+ render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
56
+ render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
57
57
  render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
58
- render_machine/actions/render_functional_requirement.py,sha256=2SbPGsLbrrQx3kasXpSpphzaMsq9GD8ASnSzvIdodT4,3748
58
+ render_machine/actions/render_functional_requirement.py,sha256=uiEwrMdArRdPX7eWysYMTI5y2pXacUySTpIgL2O-lOI,4602
59
59
  render_machine/actions/run_conformance_tests.py,sha256=4sJTORl6YLFXQIdLMBQHDeJd2hztGyt3tsVvrCviBko,4008
60
60
  render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
61
61
  render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
62
- standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
63
62
  standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
64
63
  standard_template_library/python-console-app-template.plain,sha256=HW-REeF8Crk0r_w4-MxnleVHeW6PdOB2C8gO8FDG61g,931
65
64
  standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOzWnDHdVpFj-XBTq6s4_2dy7LKV3lVkCzv8i4,114
@@ -68,17 +67,18 @@ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
68
67
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
69
68
  tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
70
69
  tests/test_cli_output.py,sha256=owDP9pLHdErtPAr1paNKp1o044kuH-HxInHLIBLZQFI,18420
71
- tests/test_file_utils.py,sha256=-O5zImiFVz9JJ4xi8TbsUwNgPFQoQUOl9Frls4BHCBI,1358
70
+ tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
72
71
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
73
- tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
72
+ tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
74
73
  tests/test_partial_rendering.py,sha256=r0RUUd1pI4-2ZkRrM1j8_kUqv-70hrOGjuKBhZoRmH0,22758
74
+ tests/test_plain2code.py,sha256=DQYFc0JVSkMI8LZCNaQ__2Nph-DcZZW17rprJA54eyw,3851
75
75
  tests/test_plain_modules.py,sha256=S7sd4ZYNotXOgCyZzmBd714QFLih69nlZ3tV8WNtVPo,13766
76
76
  tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
77
- tests/test_plainfileparser.py,sha256=7AassxH7AKX4lfW6E_58FYxRe-IzIiA16BMVWPjWqCM,18750
78
- tests/test_plainspec.py,sha256=JrzjJ4VFYDmWTZDq-8WFyt2oMx76IEuO1CAUPQeGOsw,2404
77
+ tests/test_plainfileparser.py,sha256=kXWwaGKtYnTDn5yDoYTqumlwW0iSu441wxj3WZVQOP0,22807
78
+ tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
79
79
  tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
80
80
  tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
81
- tests/test_telemetry.py,sha256=04mboTP6iXedSF7E4ZL6MPRYCw__-5qlVerjX1Ve7VI,6627
81
+ tests/test_telemetry.py,sha256=hjm81aRQQZcZ0RB6aVbS3WO46_nIAii_csmbbtnUh74,7110
82
82
  tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
83
83
  tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
84
84
  tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
@@ -91,8 +91,8 @@ tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
91
91
  tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
92
92
  tui/styles.css,sha256=c13WY1nOgmtkT_FH9Ad2PjXrfi8LYwr6zzD6rBOh9tU,6685
93
93
  tui/widget_helpers.py,sha256=mWDPrALsxLOzJg8KD9MXXafJKRiqG-EIp797jbOT77o,5972
94
- codeplain-0.3.4.dist-info/METADATA,sha256=FxZHLPsuVS6_3QE8vziDqspvfX4f1JfIvQGQIjBZE5U,5381
95
- codeplain-0.3.4.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
96
- codeplain-0.3.4.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
97
- codeplain-0.3.4.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
98
- codeplain-0.3.4.dist-info/RECORD,,
94
+ codeplain-0.3.6.dist-info/METADATA,sha256=tX12onr0DJU5GgIbceMo3m8Cys7eptunE_ISO681FqA,6352
95
+ codeplain-0.3.6.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
96
+ codeplain-0.3.6.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
97
+ codeplain-0.3.6.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
98
+ codeplain-0.3.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.30.1
2
+ Generator: hatchling 1.31.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
file_utils.py CHANGED
@@ -8,8 +8,9 @@ from liquid2.exceptions import UndefinedError
8
8
 
9
9
  import plain_spec
10
10
  from plain2code_console import console
11
- from plain2code_exceptions import UnsupportedResourceType
11
+ from plain2code_exceptions import UnsupportedBase64Content, UnsupportedResourceType
12
12
  from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
13
+ from plain2code_utils import find_large_base64_blob
13
14
  from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER, CODEPLAIN_METADATA_FOLDER
14
15
 
15
16
  BINARY_FILE_EXTENSIONS = [".pyc"]
@@ -157,7 +158,7 @@ def store_response_files(target_folder, response_files, existing_files):
157
158
 
158
159
  os.makedirs(os.path.dirname(full_file_name), exist_ok=True)
159
160
 
160
- with open(full_file_name, "w") as f:
161
+ with open(full_file_name, "w", encoding="utf-8") as f:
161
162
  f.write(response_files[file_name])
162
163
 
163
164
  if file_name not in existing_files:
@@ -206,6 +207,16 @@ def load_linked_resources(template_dirs: list[str], resources_list, module_name:
206
207
  Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
207
208
  """)
208
209
 
210
+ blob = find_large_base64_blob(content)
211
+ if blob is not None:
212
+ raise UnsupportedBase64Content(
213
+ f"Referenced resource '{file_name}' in module '{module_name}' contains a large "
214
+ f"base64-encoded blob ({len(blob)} characters), such as an embedded image. Inline "
215
+ "base64 data is not supported. Remove the data from the resource. "
216
+ "If the data should be used by the end software, "
217
+ "save the data to a separate file and include the file path in the specification without it being a reference file."
218
+ )
219
+
209
220
  linked_resources[file_name] = content
210
221
 
211
222
  return linked_resources
plain2code.py CHANGED
@@ -1,4 +1,3 @@
1
- import importlib.resources
2
1
  import logging
3
2
  import logging.config
4
3
  import os
@@ -26,9 +25,11 @@ from plain2code_events import RenderFailed
26
25
  from plain2code_exceptions import (
27
26
  ConflictingRequirements,
28
27
  GitNotInstalledError,
28
+ ImportedModuleWithFunctionalitiesError,
29
29
  InvalidAPIKey,
30
30
  InvalidFridArgument,
31
31
  MissingAPIKey,
32
+ MissingFunctionalitiesError,
32
33
  MissingPreviousFunctionalitiesError,
33
34
  MissingResource,
34
35
  ModuleDoesNotExistError,
@@ -37,6 +38,7 @@ from plain2code_exceptions import (
37
38
  PlainSyntaxError,
38
39
  RenderCancelledError,
39
40
  RenderingCreditBalanceTooLow,
41
+ UnsupportedBase64Content,
40
42
  UnsupportedResourceType,
41
43
  )
42
44
  from plain2code_logger import (
@@ -54,7 +56,7 @@ from system_config import system_config
54
56
  from tui.plain2code_tui import Plain2CodeTUI
55
57
  from tui.plain_module_render_choice_tui import PlainModuleRenderChoiceTUI
56
58
 
57
- DEFAULT_TEMPLATE_DIRS = importlib.resources.files("standard_template_library")
59
+ DEFAULT_TEMPLATE_DIRS = "standard_template_library"
58
60
  RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7
59
61
 
60
62
  # Exceptions that represent expected, user-facing error conditions. They are
@@ -65,6 +67,8 @@ EXPECTED_EXCEPTIONS = (
65
67
  MissingResource,
66
68
  TemplateNotFoundError,
67
69
  PlainSyntaxError,
70
+ ImportedModuleWithFunctionalitiesError,
71
+ MissingFunctionalitiesError,
68
72
  MissingPreviousFunctionalitiesError,
69
73
  MissingAPIKey,
70
74
  InvalidAPIKey,
@@ -74,6 +78,7 @@ EXPECTED_EXCEPTIONS = (
74
78
  NetworkConnectionError,
75
79
  ModuleDoesNotExistError,
76
80
  UnsupportedResourceType,
81
+ UnsupportedBase64Content,
77
82
  GitNotInstalledError,
78
83
  SystemExit,
79
84
  )
@@ -139,8 +144,8 @@ def setup_logging(
139
144
  return logging.getLevelName(configured_log_level)
140
145
 
141
146
 
142
- def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
143
- """Check API connectivity and validate API key and client version."""
147
+ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]:
148
+ """Check API connectivity, validate API key and client version, and return the user email."""
144
149
  response = codeplainAPI.connection_check(system_config.client_version)
145
150
 
146
151
  if not response.get("api_key_valid", False):
@@ -157,33 +162,58 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
157
162
  "Please update using: uv tool upgrade codeplain"
158
163
  )
159
164
 
165
+ return response.get("user_email")
160
166
 
161
- def render(args, run_state: RunState, event_bus: EventBus, default_log_level: str = "INFO"): # noqa: C901
162
- template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
163
167
 
168
+ def warn_if_acceptance_tests_without_conformance_script(plain_module, args) -> None:
169
+ """Warn when any loaded module (including required modules) defines acceptance tests
170
+ but no conformance tests script is configured.
171
+
172
+ Acceptance tests are treated as conformance tests, so a conformance tests script is required
173
+ to actually run them. Without it, the acceptance tests cannot be executed.
174
+ """
175
+ if args.conformance_tests_script:
176
+ return
177
+
178
+ module_names_with_acceptance_tests = [
179
+ module.module_name
180
+ for module in plain_module.all_required_modules + [plain_module]
181
+ if plain_spec.has_acceptance_tests(module.plain_source)
182
+ ]
183
+ if not module_names_with_acceptance_tests:
184
+ return
185
+
186
+ module_names = ", ".join(module_names_with_acceptance_tests)
187
+ console.warning(
188
+ f"Acceptance tests were found ({module_names}) but no conformance tests script is configured. "
189
+ "Acceptance tests are treated as conformance tests and require a conformance tests script "
190
+ "(--conformance-tests-script or 'conformance_tests_script' in config) to be executed."
191
+ )
192
+
193
+
194
+ def render( # noqa: C901
195
+ plain_module: plain_modules.PlainModule,
196
+ args,
197
+ run_state: RunState,
198
+ event_bus: EventBus,
199
+ default_log_level: str = "INFO",
200
+ ):
164
201
  # Compute render range from either --render-range or --render-from
165
202
  render_range = None
166
203
  if args.render_range or args.render_from:
167
- # Parse the plain file to get the plain_source for FRID extraction
168
- _, plain_source, _ = plain_file.plain_file_parser(args.filename, template_dirs)
169
- render_range = plain_spec.compute_render_range(args, plain_source)
204
+ render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
170
205
 
171
206
  codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
172
207
  assert args.api is not None and args.api != "", "API URL is required"
173
208
  codeplainAPI.api_url = args.api
174
209
 
175
- _check_connection(codeplainAPI)
210
+ run_state.user_email = _check_connection(codeplainAPI)
176
211
 
177
212
  stop_event = threading.Event()
178
213
  enter_pause_event = threading.Event()
179
214
  signal.signal(signal.SIGTERM, lambda _signum, _frame: stop_event.set())
180
215
 
181
- plain_module = plain_modules.PlainModule(
182
- args.filename,
183
- args.build_folder,
184
- args.conformance_tests_folder,
185
- template_dirs,
186
- )
216
+ warn_if_acceptance_tests_without_conformance_script(plain_module, args)
187
217
 
188
218
  render_choice = None
189
219
  if render_range is None:
@@ -297,7 +327,7 @@ def main(): # noqa: C901
297
327
  console.error(
298
328
  "Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
299
329
  )
300
- return
330
+ sys.exit(1)
301
331
 
302
332
  if not args.api:
303
333
  args.api = "https://api.codeplain.ai"
@@ -306,30 +336,41 @@ def main(): # noqa: C901
306
336
  print_status(args.api_key, args.api, system_config.client_version)
307
337
  except Exception as e:
308
338
  console.error(f"Error fetching status: {str(e)}")
339
+ sys.exit(1)
309
340
  return
310
341
 
311
- # Handle early-exit flags before heavy initialization
312
- if args.dry_run or args.full_plain:
313
- template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
342
+ template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
314
343
 
344
+ # Handle full plain early-exit (raw text dump; does not require a parsed module).
345
+ if args.full_plain:
315
346
  try:
316
- if args.full_plain:
317
- module_name = Path(args.filename).stem
318
- plain_source = plain_file.read_module_plain_source(module_name, template_dirs)
319
- [full_plain_source, _] = file_utils.get_loaded_templates(template_dirs, plain_source)
320
- console.info("Full plain text:\n")
321
- console.info(full_plain_source)
322
- return
323
-
324
- if args.dry_run:
325
- console.info("Printing dry run output...\n")
326
- _, plain_source_tree, _ = plain_file.plain_file_parser(args.filename, template_dirs)
327
- render_range = plain_spec.compute_render_range(args, plain_source_tree)
328
- print_dry_run_output(plain_source_tree, render_range)
329
- return
347
+ module_name = Path(args.filename).stem
348
+ plain_source = plain_file.read_module_plain_source(module_name, template_dirs)
349
+ [full_plain_source, _] = file_utils.get_loaded_templates(template_dirs, plain_source)
350
+ console.info("Full plain text:\n")
351
+ console.info(full_plain_source)
330
352
  except Exception as e:
331
353
  console.error(f"Error: {str(e)}")
332
- return
354
+ return
355
+
356
+ # Parse the plain file (and its required modules) once; reused by dry-run and rendering.
357
+ try:
358
+ plain_module = plain_modules.PlainModule(
359
+ args.filename,
360
+ args.build_folder,
361
+ args.conformance_tests_folder,
362
+ template_dirs,
363
+ )
364
+ except Exception as e:
365
+ console.error(f"Error: {str(e)}")
366
+ return
367
+
368
+ if args.dry_run:
369
+ console.info("Printing dry run output...\n")
370
+ render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
371
+ print_dry_run_output(plain_module.plain_source, render_range)
372
+ warn_if_acceptance_tests_without_conformance_script(plain_module, args)
373
+ return
333
374
 
334
375
  event_bus = EventBus()
335
376
 
@@ -357,7 +398,7 @@ def main(): # noqa: C901
357
398
  raise MissingAPIKey(
358
399
  "Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
359
400
  )
360
- render(args, run_state, event_bus, default_log_level)
401
+ render(plain_module, args, run_state, event_bus, default_log_level)
361
402
  except BaseException as e:
362
403
  if isinstance(e, KeyboardInterrupt):
363
404
  error_message = "Keyboard interrupt"
plain2code_exceptions.py CHANGED
@@ -29,6 +29,11 @@ class PlainSyntaxError(Exception):
29
29
  pass
30
30
 
31
31
 
32
+ class UnsupportedBase64Content(Exception):
33
+
34
+ pass
35
+
36
+
32
37
  class InternalClientError(Exception):
33
38
  pass
34
39
 
@@ -73,6 +78,23 @@ class MissingPreviousFunctionalitiesError(Exception):
73
78
  pass
74
79
 
75
80
 
81
+ class MissingFunctionalitiesError(Exception):
82
+ """Raised when a module to be rendered has no functionalities specified at all."""
83
+
84
+ pass
85
+
86
+
87
+ class ImportedModuleWithFunctionalitiesError(Exception):
88
+ """Raised when a module brought in via ``import`` contains functional specs.
89
+
90
+ This is a usage error, not a syntax error: the module is syntactically valid
91
+ and would be fine as a render target, but functionalities are not allowed in
92
+ the import role.
93
+ """
94
+
95
+ pass
96
+
97
+
76
98
  class NetworkConnectionError(Exception):
77
99
  """Raised when there is a network connectivity issue with the API server."""
78
100
 
plain2code_logger.py CHANGED
@@ -9,6 +9,13 @@ from plain2code_state import RunState
9
9
 
10
10
  LOGGER_NAME = "codeplain"
11
11
 
12
+ # Attach a NullHandler so that log records emitted before setup_logging() configures
13
+ # the real handlers (e.g. during --dry-run, --status, --full-plain, or early parse
14
+ # errors) are not printed to stderr by logging.lastResort. Without this, console.error()
15
+ # / console.warning() — which both log the message and print it via rich — would show the
16
+ # same message twice: once in plain text (from lastResort) and once styled (from rich).
17
+ logging.getLogger(LOGGER_NAME).addHandler(logging.NullHandler())
18
+
12
19
 
13
20
  class IndentedFormatter(logging.Formatter):
14
21
  def format(self, record):
plain2code_state.py CHANGED
@@ -27,6 +27,7 @@ class RunState:
27
27
  self.current_module: Optional[str] = None
28
28
  self.current_frid: Optional[str] = None
29
29
  self.current_render_state: Optional[str] = None
30
+ self.user_email: Optional[str] = None
30
31
 
31
32
  def increment_call_count(self):
32
33
  self.call_count += 1
plain2code_telemetry.py CHANGED
@@ -98,6 +98,8 @@ def capture_crash(exc_info, run_state: Optional[RunState], args) -> bool:
98
98
  scope.set_tag("render_state", run_state.current_render_state)
99
99
  scope.set_tag("current_module", run_state.current_module)
100
100
  scope.set_tag("current_frid", run_state.current_frid)
101
+ if run_state.user_email:
102
+ scope.set_user({"email": run_state.user_email})
101
103
  scope.set_tag("headless", bool(getattr(args, "headless", False)))
102
104
  scope.set_tag("unittests_script_provided", bool(getattr(args, "unittests_script", None)))
103
105
  scope.set_tag("conformance_tests_script_provided", bool(getattr(args, "conformance_tests_script", None)))
plain2code_utils.py CHANGED
@@ -1,3 +1,21 @@
1
+ import re
2
+ from typing import Optional
3
+
4
+ #
5
+ MAX_BASE64_BLOB_LENGTH = 8192
6
+
7
+ # Matches a long contiguous base64 / base64url run, optionally preceded by a data: URI header.
8
+ _BASE64_BLOB_PATTERN = re.compile(
9
+ r"(?:data:[\w.+-]+/[\w.+-]+;base64,)?[A-Za-z0-9+/_-]{%d,}={0,2}" % MAX_BASE64_BLOB_LENGTH
10
+ )
11
+
12
+
13
+ def find_large_base64_blob(text: str) -> Optional[str]:
14
+ """Return the first contiguous base64 blob at or above the threshold, or None."""
15
+ match = _BASE64_BLOB_PATTERN.search(text)
16
+ return match.group(0) if match else None
17
+
18
+
1
19
  def format_duration_hms(total_seconds: int) -> str:
2
20
  """Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
3
21
  if total_seconds < 0: