codeplain 0.3.7__py3-none-any.whl → 0.3.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. cli_output/render_summary.py +2 -2
  2. cli_output/status.py +2 -2
  3. {codeplain-0.3.7.dist-info → codeplain-0.3.9.dist-info}/METADATA +4 -4
  4. {codeplain-0.3.7.dist-info → codeplain-0.3.9.dist-info}/RECORD +50 -42
  5. codeplain_REST_api.py +6 -2
  6. git_utils.py +0 -10
  7. memory_management.py +2 -3
  8. metadata_utils.py +46 -0
  9. module_renderer.py +3 -6
  10. plain2code.py +7 -1
  11. plain2code_arguments.py +6 -15
  12. plain2code_console.py +32 -21
  13. plain2code_events.py +1 -0
  14. plain2code_logger.py +3 -7
  15. plain2code_state.py +17 -1
  16. plain2code_telemetry.py +17 -11
  17. plain2code_utils.py +17 -13
  18. plain_modules.py +73 -41
  19. render_machine/actions/create_dist.py +2 -2
  20. render_machine/actions/exit_with_error.py +2 -1
  21. render_machine/actions/fix_conformance_test.py +4 -2
  22. render_machine/actions/prepare_repositories.py +3 -10
  23. render_machine/actions/render_functional_requirement.py +4 -3
  24. render_machine/conformance_tests.py +8 -6
  25. render_machine/render_context.py +6 -6
  26. render_machine/render_utils.py +15 -10
  27. system_config.py +80 -1
  28. tests/e2e/test_hello_world_python.py +1 -1
  29. tests/e2e/test_hello_world_python_windows.py +1 -1
  30. tests/test_arg_provenance.py +3 -3
  31. tests/test_cli_output.py +2 -2
  32. tests/test_console_log_styles.py +112 -0
  33. tests/test_folder_path_resolution.py +12 -13
  34. tests/test_plain2code.py +1 -2
  35. tests/test_plain2code_state.py +36 -0
  36. tests/test_plain2code_utils.py +36 -0
  37. tests/test_plain_modules.py +200 -15
  38. tests/test_prepare_repositories_layout.py +119 -0
  39. tests/test_system_config.py +58 -0
  40. tests/test_telemetry.py +31 -10
  41. tests/test_tui_usage.py +209 -0
  42. tests/test_usage_summary.py +28 -0
  43. tui/components.py +29 -20
  44. tui/plain2code_tui.py +71 -2
  45. tui/styles.css +4 -0
  46. tui/widget_helpers.py +20 -1
  47. usage_summary.py +30 -0
  48. _version.py +0 -1
  49. {codeplain-0.3.7.dist-info → codeplain-0.3.9.dist-info}/WHEEL +0 -0
  50. {codeplain-0.3.7.dist-info → codeplain-0.3.9.dist-info}/entry_points.txt +0 -0
  51. {codeplain-0.3.7.dist-info → codeplain-0.3.9.dist-info}/licenses/LICENSE +0 -0
@@ -4,7 +4,7 @@ from typing import Optional
4
4
 
5
5
  from plain2code_console import console
6
6
  from plain2code_state import RunState
7
- from plain2code_utils import format_duration_hms
7
+ from usage_summary import format_usage_summary
8
8
 
9
9
 
10
10
  def print_exit_summary(
@@ -24,7 +24,7 @@ def print_exit_summary(
24
24
  msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n"
25
25
  msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n"
26
26
  msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n"
27
- msg += f"[#8E8F91]functionalities [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]used credits [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]render time [#FFFFFF]{format_duration_hms(run_state.render_time_accumulated)}\n"
27
+ msg += format_usage_summary(run_state.rendered_functionalities, run_state.render_time_accumulated) + "\n"
28
28
  console.print(msg)
29
29
 
30
30
  if not run_state.render_succeeded and error_message:
cli_output/status.py CHANGED
@@ -32,7 +32,7 @@ def _display_credit_line(plan_credits: dict) -> None:
32
32
  # If naive datetime, assume UTC
33
33
  if dt.tzinfo is None:
34
34
  dt = dt.replace(tzinfo=timezone.utc)
35
- formatted_date = dt.strftime("%b %-d, %Y")
35
+ formatted_date = f"{dt.strftime('%b')} {dt.day}, {dt.year}"
36
36
 
37
37
  # Check if expired
38
38
  now = datetime.now(timezone.utc)
@@ -62,7 +62,7 @@ def _display_bucket_credit_line(bucket: dict, label: str) -> None:
62
62
  # If naive datetime, assume UTC
63
63
  if dt.tzinfo is None:
64
64
  dt = dt.replace(tzinfo=timezone.utc)
65
- formatted_date = dt.strftime("%b %-d, %Y")
65
+ formatted_date = f"{dt.strftime('%b')} {dt.day}, {dt.year}"
66
66
 
67
67
  # Check if expired
68
68
  now = datetime.now(timezone.utc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.3.7
3
+ Version: 0.3.9
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -8,7 +8,7 @@ Classifier: Intended Audience :: Developers
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Topic :: Software Development :: Code Generators
10
10
  Requires-Python: ==3.11.*
11
- Requires-Dist: gitpython==3.1.50
11
+ Requires-Dist: gitpython==3.1.52
12
12
  Requires-Dist: mistletoe==1.5.1
13
13
  Requires-Dist: networkx==3.6.1
14
14
  Requires-Dist: python-frontmatter==1.3.0
@@ -16,7 +16,7 @@ Requires-Dist: python-liquid2==0.4.0
16
16
  Requires-Dist: pyyaml==6.0.3
17
17
  Requires-Dist: requests==2.34.2
18
18
  Requires-Dist: rich==15.0.0
19
- Requires-Dist: sentry-sdk==2.63.0
19
+ Requires-Dist: sentry-sdk==2.66.0
20
20
  Requires-Dist: textual>=7.5.0
21
21
  Requires-Dist: tiktoken==0.13.0
22
22
  Requires-Dist: transitions==0.9.3
@@ -24,7 +24,7 @@ Provides-Extra: dev
24
24
  Requires-Dist: black==26.5.1; extra == 'dev'
25
25
  Requires-Dist: flake8==7.3.0; extra == 'dev'
26
26
  Requires-Dist: isort==8.0.1; extra == 'dev'
27
- Requires-Dist: mypy==2.1.0; extra == 'dev'
27
+ Requires-Dist: mypy==2.3.0; extra == 'dev'
28
28
  Requires-Dist: pytest==9.1.1; extra == 'dev'
29
29
  Description-Content-Type: text/markdown
30
30
 
@@ -1,45 +1,46 @@
1
- _version.py,sha256=J0I0c7-a50EOnWXMryTu_E6xhXSYFBPjVpeYP_a3vRI,22
2
1
  change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
3
- codeplain_REST_api.py,sha256=h_XoNws8fvf7OTtoVXhHh6OfLGg77UNFuggMBwaqBwE,21533
2
+ codeplain_REST_api.py,sha256=x-sCbbBSswID4eKXDMvv84nzWynNSbBPoyAtq__6tas,21644
4
3
  concept_utils.py,sha256=D4qVHFBRvfJZwiIq1AD_SV0Ev-RnOMg2yGWo6Cal7PM,7913
5
4
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
6
5
  event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
7
6
  file_utils.py,sha256=MNZ5li1ofIMOYB6FCrMa_UkX6nkGCgSZuaqzhqVc1Ok,11682
8
- git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
9
- memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
10
- module_renderer.py,sha256=DZCIo_QzvhFw8EqQTKeF3HUbvfumrV2O1sz-gUhWpIA,6752
7
+ git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
8
+ memory_management.py,sha256=SPoK0BXB9_yDyrrOKjFtOOlKkJIq0I5EXV7dE0OsE8g,5972
9
+ metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
10
+ module_renderer.py,sha256=AUbjGiE6USMe-uTfbOs9gEQSEMzO9qLwGTxy3_tC_M4,6615
11
11
  partial_rendering.py,sha256=0ChOaGytogBUEGISpbWkbjT9Hqy8pnOkJdJXcitJDgk,12219
12
12
  path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
13
- plain2code.py,sha256=HGv3Or296pZRuqD5jcmuA3QnGl34aJD5f8619M7l5G4,15294
14
- plain2code_arguments.py,sha256=3tAA9Haw-j97zztjXdhi6LxFtURP0Tfg9J_rvJDoXmw,17685
15
- plain2code_console.py,sha256=6_PdrVBw9oDccqSGW_gVRYQjVgC_5W3ush4QT2_jGnw,4931
16
- plain2code_events.py,sha256=AM8KJS68uiVXRMLt861KaGhs2H4yzujlMB_xiYPfHQc,1478
13
+ plain2code.py,sha256=Hai5_Ho1nY0Hau7Y6QEGkYeg8NKVx5jDMtQV0UVHsLU,15706
14
+ plain2code_arguments.py,sha256=knY7gqfpj2IKnPSl-dEdxBUpzrbohEQXamNYkFVC00Y,17426
15
+ plain2code_console.py,sha256=Ifs8tmfNX31UujQ5_0sk9AJcajMG_6qhF-6QZcfbcEE,5773
16
+ plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
17
17
  plain2code_exceptions.py,sha256=wkkEXw2k4LCd09PIH3_Hu0HyZvtXYjFaA653Sfey4FA,2287
18
- plain2code_logger.py,sha256=RJNhesE9y59lPILvGhiJpAB4snTwfsHpG1Ir6EaPhiQ,4989
18
+ plain2code_logger.py,sha256=NRPLVIMnCHNVl6dII9gFoIzMdr8xa7icnL0T_Fx2XLw,4839
19
19
  plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
20
20
  plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
21
- plain2code_state.py,sha256=1VwyxgKdA13fAJNKSiPBfBb3A20TKfGCNnOm06YdcS4,2233
22
- plain2code_telemetry.py,sha256=7yuTH7mQE_wGVW2H1oiVZ3UJg98ArB5HcUUm9isnv9g,4354
23
- plain2code_utils.py,sha256=id04t9Iez5pYygrE3fKEyrcfWDcniKFT2iQoF2CzTJs,1409
21
+ plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
22
+ plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
23
+ plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
24
24
  plain_file.py,sha256=k2rcq53q0kOf9_8egrZILpkrVkZdfJt7HU0gHBFskU4,35116
25
- plain_modules.py,sha256=uvRPfxBJwstaWi5lgYKgtQdhhf0Naiy2mS41tLw_TjY,16186
25
+ plain_modules.py,sha256=VgOwEWVhp7dyRs5NM-jVaC5GKn8KzT1sWuSNkM7YLIA,17375
26
26
  plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
27
- system_config.py,sha256=trgytfXKXoCn0Pgy9u00x2D0ZRHlh_tADlbkwZnF8QE,1038
27
+ system_config.py,sha256=nyeDS1WkyA-5m3TflB_XMDyhBvC-tVRAG3VgJaZYG_M,3694
28
+ usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
28
29
  cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
29
30
  cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
30
- cli_output/render_summary.py,sha256=QrW3Iq15xUoWXuVXpqClAKncAvHdQR0lWG1_OMUBsWk,1307
31
- cli_output/status.py,sha256=PFhHqFaC0nt2zsPR0VbD-JBPB3sEVPfhopJmOylFxbU,6160
31
+ cli_output/render_summary.py,sha256=-yRoI7ZZTS9IHyp1vd-sFleGXTVbxlrCkvuM5TX0Mp4,1168
32
+ cli_output/status.py,sha256=MvdocFXL1aA73NMp92gQb0Nd-f7-iw66XVUiHK33sEM,6194
32
33
  config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
33
34
  config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
34
35
  docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
35
36
  examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
36
37
  render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
38
  render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
38
- render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
39
+ render_machine/conformance_tests.py,sha256=jvdWRx-ZURSq4JMase1svKzYJ98Lc4svPvfHXglHeFE,7163
39
40
  render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
40
- render_machine/render_context.py,sha256=fjU_S5W5HoAX3-WlEM8hp4DKMX_NDgVlfwQUAp0yaf8,25633
41
+ render_machine/render_context.py,sha256=pdosCY7QMzfPy-cJxEsiM9m4I5K2CoNJ7N-5Fzg0NdU,25584
41
42
  render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
42
- render_machine/render_utils.py,sha256=ao0e64VSOGYfb8hwJXIZlbqer5HjNMYJWHlLDoHnJyc,9123
43
+ render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
43
44
  render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
44
45
  render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
45
46
  render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
@@ -47,16 +48,16 @@ render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySx
47
48
  render_machine/actions/base_action.py,sha256=vVzMp-LanrZyq-jamHyWaQxpRl-pLqtTdtiEzUBhfyE,534
48
49
  render_machine/actions/commit_conformance_tests_changes.py,sha256=08yxCPCpf5TlrzLPsEh5SJXVVvnLfwdNKSr7BlzlYxo,2589
49
50
  render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgWcmFbynes-BK2IGnZwA7wygrTJGoL8dY,826
50
- render_machine/actions/create_dist.py,sha256=xMPPyws7swVkaUSQg3dwMGScCi1BuII9y2C_XSP0N80,1067
51
- render_machine/actions/exit_with_error.py,sha256=-LkQsFhmV-Nr741APTx38dqu473b6OSiIvKayFFsXxI,1270
51
+ render_machine/actions/create_dist.py,sha256=ykER7i2OvWAXjrlAZnkQHpiNl00vSMF8WDxNhlWKfsA,1088
52
+ render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9HWaPkXyqg7ck,1279
52
53
  render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
53
- render_machine/actions/fix_conformance_test.py,sha256=jtVeL11J8K-a5YvoR3uW6TVytQZkiBgXlDyFR3wXYEs,10022
54
+ render_machine/actions/fix_conformance_test.py,sha256=EZLVjF4A8zSTmyqdgcLoz-2pLpeieq4M_Rb71pY3IKA,10095
54
55
  render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
55
- render_machine/actions/prepare_repositories.py,sha256=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
56
+ render_machine/actions/prepare_repositories.py,sha256=dnh4cYeTZS0snWEeD8gAxtW2ot3KBq4nStRG052qc9U,3124
56
57
  render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
57
58
  render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
58
59
  render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
59
- render_machine/actions/render_functional_requirement.py,sha256=uiEwrMdArRdPX7eWysYMTI5y2pXacUySTpIgL2O-lOI,4602
60
+ render_machine/actions/render_functional_requirement.py,sha256=53KzMVlcefoag1x73roCEQvQ7FbmH8FzhXMQau-s8tg,4655
60
61
  render_machine/actions/run_conformance_tests.py,sha256=4sJTORl6YLFXQIdLMBQHDeJd2hztGyt3tsVvrCviBko,4008
61
62
  render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
62
63
  render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
@@ -66,40 +67,47 @@ standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOz
66
67
  standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
67
68
  tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
68
69
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
69
- tests/test_arg_provenance.py,sha256=Vp-ZtNbTlSNOE8iiHOin3YI9sobbN0tRPUIfrYjxttQ,3459
70
+ tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
70
71
  tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
71
- tests/test_cli_output.py,sha256=owDP9pLHdErtPAr1paNKp1o044kuH-HxInHLIBLZQFI,18420
72
+ tests/test_cli_output.py,sha256=iKS1Li4yhoIL0ezJ58o4oEf9V8TiKcQkvQLVtJpdM4c,18420
73
+ tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
72
74
  tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
73
- tests/test_folder_path_resolution.py,sha256=_ZvVFu5m2-AH8iQNn0fCYkA0flVxVwdh1oQiWcPNIxw,6611
75
+ tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
74
76
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
75
77
  tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
76
78
  tests/test_log_file_name_resolution.py,sha256=99g9NgPF-BZ98CvNyqDcltBAx4sLBE8gpQPAAieKiPc,3619
77
79
  tests/test_logging_config_path_resolution.py,sha256=avz_ehRd18INwIXvAy1AwVzNNUz0vS8eu1nRe0bra8g,2650
78
80
  tests/test_partial_rendering.py,sha256=r0RUUd1pI4-2ZkRrM1j8_kUqv-70hrOGjuKBhZoRmH0,22758
79
81
  tests/test_path_resolution.py,sha256=yQpd-Unq1lwNv0wkRPwVs5pDT8JmyyDgbpiTQsyt--Y,2615
80
- tests/test_plain2code.py,sha256=DQYFc0JVSkMI8LZCNaQ__2Nph-DcZZW17rprJA54eyw,3851
81
- tests/test_plain_modules.py,sha256=S7sd4ZYNotXOgCyZzmBd714QFLih69nlZ3tV8WNtVPo,13766
82
+ tests/test_plain2code.py,sha256=lyb7iYK8XHu1dmTFh9Q31YgX0faNlCHiyI5M35VQwOg,3780
83
+ tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlBI,1217
84
+ tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
85
+ tests/test_plain_modules.py,sha256=jDjPv1JyKCgIMaoy3GYpjxTSQkfkI2ppmJgqzHdQ6E4,21592
82
86
  tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
83
87
  tests/test_plainfileparser.py,sha256=xd9P0KqZJlJPn3RKT510IQbfBotk6nQ9O8p9iLwJiQk,22815
84
88
  tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
89
+ tests/test_prepare_repositories_layout.py,sha256=9T9lf92HhZC_ziUZxiyogbbeqJfmg131FZS81BuUNfs,4751
85
90
  tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
86
91
  tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
87
92
  tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
88
- tests/test_telemetry.py,sha256=hjm81aRQQZcZ0RB6aVbS3WO46_nIAii_csmbbtnUh74,7110
93
+ tests/test_system_config.py,sha256=poH6eviXqAxAzTcvdP0TeS0wBp6CW1IW5irqto7FNyI,1784
94
+ tests/test_telemetry.py,sha256=LINvx1_Oz1OJ3ho2RcIcAA29IsNfUbkVVCbFaQcrBaA,8133
95
+ tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
96
+ tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
89
97
  tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
90
- tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
91
- tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
98
+ tests/e2e/test_hello_world_python.py,sha256=2Cgs3-79dtQh5gVab257tFDHXTznefZeYqbTjlM4Nng,1632
99
+ tests/e2e/test_hello_world_python_windows.py,sha256=ggynH_Wm4011M500zoPNNgvD-s4vZ3hvlorf1yARgQQ,2019
92
100
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
93
- tui/components.py,sha256=DoMDhwNVVpv--OY6o8YQYPzOQPG9Dyh9kgMOqUYtNiE,25412
101
+ tui/components.py,sha256=XkC4Aq6VyTnFnBQs8PEHwVdAOczhm0ebKN92kiYMbq0,25791
94
102
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
95
- tui/plain2code_tui.py,sha256=t-0DonjrK6_x6jPUMYW0w6tvjZZlk89E-DEor7GzjG4,13785
103
+ tui/plain2code_tui.py,sha256=8F7ZYRpmTiWRdTeQ63LPf2LGrreYjIYXvf_9XNSPyHs,17104
96
104
  tui/plain_module_render_choice_tui.py,sha256=DRl_xhUbUJsyOaCYt_uNDSLUAA3Ty9NYI9zFrVDwp9Y,11270
97
105
  tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
98
106
  tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
99
- tui/styles.css,sha256=c13WY1nOgmtkT_FH9Ad2PjXrfi8LYwr6zzD6rBOh9tU,6685
100
- tui/widget_helpers.py,sha256=mWDPrALsxLOzJg8KD9MXXafJKRiqG-EIp797jbOT77o,5972
101
- codeplain-0.3.7.dist-info/METADATA,sha256=Gs_i0uMjyodZ11UAyrWIHyAp9vY4Zw_-todAZsYLRKg,6351
102
- codeplain-0.3.7.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
103
- codeplain-0.3.7.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
104
- codeplain-0.3.7.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
105
- codeplain-0.3.7.dist-info/RECORD,,
107
+ tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
108
+ tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
109
+ codeplain-0.3.9.dist-info/METADATA,sha256=v3UbwneHulzIUZMJI9ZHf032CyyhAef-BBmn-RRsq14,6351
110
+ codeplain-0.3.9.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
111
+ codeplain-0.3.9.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
112
+ codeplain-0.3.9.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
113
+ codeplain-0.3.9.dist-info/RECORD,,
codeplain_REST_api.py CHANGED
@@ -5,6 +5,7 @@ import requests
5
5
  from requests.exceptions import ConnectionError, RequestException, Timeout
6
6
 
7
7
  import plain2code_exceptions
8
+ from plain2code_console import RETRY_COLOR
8
9
  from plain2code_state import RunState
9
10
 
10
11
  MAX_RETRIES = 4
@@ -67,8 +68,11 @@ class CodeplainAPI:
67
68
  connection_error_type = "Network error" if is_connection_error else "Error"
68
69
  if attempt < num_retries:
69
70
  if not silent:
70
- self.console.debug(f"{connection_error_type} on attempt {attempt + 1}/{num_retries + 1}: {error}")
71
- self.console.debug(f"Retrying in {retry_delay} seconds...")
71
+ self.console.debug(
72
+ f"↻ {connection_error_type} on attempt {attempt + 1}/{num_retries + 1}: {error}. "
73
+ f"Retrying in {retry_delay} seconds...",
74
+ color=RETRY_COLOR,
75
+ )
72
76
  time.sleep(retry_delay)
73
77
  # Exponential backoff
74
78
  return retry_delay * 2
git_utils.py CHANGED
@@ -64,7 +64,6 @@ def init_git_repo(
64
64
  path_to_repo: Union[str, os.PathLike],
65
65
  module_name: Optional[str] = None,
66
66
  render_id: Optional[str] = None,
67
- initial_files: Optional[dict[str, str]] = None,
68
67
  ) -> Repo:
69
68
  """
70
69
  Initializes a new git repository in the given path.
@@ -79,10 +78,6 @@ def init_git_repo(
79
78
  repo = Repo.init(path_to_repo)
80
79
  _ensure_git_config(repo)
81
80
 
82
- if initial_files:
83
- file_utils.store_response_files(path_to_repo, initial_files, [])
84
- repo.git.add(".")
85
-
86
81
  repo.git.commit(
87
82
  "--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
88
83
  )
@@ -95,14 +90,9 @@ def clone_repo(
95
90
  new_repo_path: str,
96
91
  module_name: Optional[str] = None,
97
92
  render_id: Optional[str] = None,
98
- initial_files: Optional[dict[str, str]] = None,
99
93
  ) -> Repo:
100
94
  repo = Repo.clone_from(source_repo_path, new_repo_path)
101
95
 
102
- if initial_files:
103
- file_utils.store_response_files(new_repo_path, initial_files, [])
104
- repo.git.add(".")
105
-
106
96
  repo.git.commit(
107
97
  "--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
108
98
  )
memory_management.py CHANGED
@@ -3,7 +3,6 @@ import os
3
3
 
4
4
  import file_utils
5
5
  from plain2code_console import console
6
- from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER
7
6
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
8
7
  from render_machine.render_context import RenderContext
9
8
 
@@ -24,9 +23,9 @@ class MemoryManager:
24
23
  console.debug(f"Loaded {len(memory_files_content)} memory files.")
25
24
  return memory_files, memory_files_content
26
25
 
27
- def __init__(self, codeplain_api, module_name: str, conformance_tests_folder: str):
26
+ def __init__(self, codeplain_api, memory_folder: str):
28
27
  self.codeplain_api = codeplain_api
29
- self.memory_folder = os.path.join(conformance_tests_folder, module_name, CODEPLAIN_MEMORY_SUBFOLDER)
28
+ self.memory_folder = memory_folder
30
29
 
31
30
  def create_conformance_tests_memory(
32
31
  self, render_context: RenderContext, exit_code: int, conformance_tests_issue: str
metadata_utils.py ADDED
@@ -0,0 +1,46 @@
1
+ """Read/write helpers for the per-module metadata file (``module_metadata.json``).
2
+
3
+ These helpers operate purely on a metadata path or a plain dict. Module-specific
4
+ logic — computing the hashes, assembling the payload from the spec and required
5
+ modules — stays on :class:`plain_modules.PlainModule`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+
13
+ MODULE_METADATA_FILENAME = "module_metadata.json"
14
+ MODULE_FUNCTIONALITIES = "functionalities"
15
+ REQUIRED_MODULES_FUNCTIONALITIES = "required_modules_functionalities"
16
+
17
+
18
+ def load_metadata(metadata_path: str) -> dict | None:
19
+ """Return the parsed metadata dict, or None if the file does not exist."""
20
+ if not os.path.exists(metadata_path):
21
+ return None
22
+
23
+ with open(metadata_path, "r", encoding="utf-8") as f:
24
+ return json.load(f)
25
+
26
+
27
+ def write_metadata(metadata_path: str, metadata: dict) -> None:
28
+ """Write metadata as indented JSON, creating the parent folder if needed."""
29
+ os.makedirs(os.path.dirname(metadata_path), exist_ok=True)
30
+
31
+ with open(metadata_path, "w", encoding="utf-8") as f:
32
+ json.dump(metadata, f, indent=4)
33
+
34
+
35
+ def truncate_functionalities(metadata: dict, keep_count: int) -> bool:
36
+ """Trim the functionalities list in ``metadata`` in place to ``keep_count`` entries.
37
+
38
+ Returns True if the list was shortened, False if it was already short enough
39
+ (in which case ``metadata`` is left untouched).
40
+ """
41
+ functionalities = metadata.get(MODULE_FUNCTIONALITIES, [])
42
+ if len(functionalities) <= keep_count:
43
+ return False
44
+
45
+ metadata[MODULE_FUNCTIONALITIES] = functionalities[:keep_count]
46
+ return True
module_renderer.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import argparse
2
- import os
3
2
  import threading
4
3
 
5
4
  from event_bus import EventBus
@@ -48,9 +47,8 @@ class ModuleRenderer:
48
47
  self.codeplainAPI,
49
48
  memory_manager,
50
49
  plain_module,
51
- build_folder=os.path.join(self.args.build_folder, plain_module.module_name),
50
+ build_folder=plain_module.module_build_folder,
52
51
  build_dest=self.args.build_dest,
53
- conformance_tests_folder=self.args.conformance_tests_folder,
54
52
  conformance_tests_dest=self.args.conformance_tests_dest,
55
53
  unittests_script=self.args.unittests_script,
56
54
  conformance_tests_script=self.args.conformance_tests_script,
@@ -115,8 +113,7 @@ class ModuleRenderer:
115
113
 
116
114
  memory_manager = MemoryManager(
117
115
  self.codeplainAPI,
118
- plain_module.module_name,
119
- self.args.conformance_tests_folder,
116
+ plain_module.module_memory_folder,
120
117
  )
121
118
  render_context = self._build_render_context_for_module(
122
119
  plain_module,
@@ -165,7 +162,7 @@ class ModuleRenderer:
165
162
  if self.args.copy_build:
166
163
  rendered_code_path = f"{self.args.build_dest}/"
167
164
  else:
168
- rendered_code_path = self.args.build_folder
165
+ rendered_code_path = self.plain_module.module_build_folder
169
166
 
170
167
  self.run_state.set_render_generated_code_path(rendered_code_path)
171
168
  self.event_bus.publish(RenderCompleted(rendered_code_path=rendered_code_path))
plain2code.py CHANGED
@@ -212,6 +212,12 @@ def render( # noqa: C901
212
212
 
213
213
  warn_if_acceptance_tests_without_conformance_script(plain_module, args)
214
214
 
215
+ # The module_metadata file lives outside the code git repo. That means that a crash mid-render can leave it
216
+ # claiming a functionality was implemented even thought it wasn't yet committed (because of the crash).
217
+ # Out of precaution, this reconciles every module_metadata against the code repo.
218
+ for module in plain_module.all_required_modules + [plain_module]:
219
+ module.reconcile_metadata_with_git()
220
+
215
221
  render_choice = None
216
222
  if render_range is None:
217
223
  plain_module_render_state = get_plain_module_render_state(plain_module)
@@ -286,6 +292,7 @@ def render( # noqa: C901
286
292
  render_thread = threading.Thread(target=run_render, daemon=True)
287
293
  app = Plain2CodeTUI(
288
294
  event_bus=event_bus,
295
+ run_state=run_state,
289
296
  on_ready=render_thread.start,
290
297
  render_id=run_state.render_id,
291
298
  unittests_script=args.unittests_script,
@@ -355,7 +362,6 @@ def main(): # noqa: C901
355
362
  plain_module = plain_modules.PlainModule(
356
363
  os.path.basename(args.filename),
357
364
  args.build_folder,
358
- args.conformance_tests_folder,
359
365
  template_dirs,
360
366
  )
361
367
  except Exception as e:
plain2code_arguments.py CHANGED
@@ -16,7 +16,6 @@ CODEPLAIN_API_KEY = os.getenv("CODEPLAIN_API_KEY")
16
16
 
17
17
 
18
18
  DEFAULT_BUILD_FOLDER = "plain_modules"
19
- DEFAULT_CONFORMANCE_TESTS_FOLDER = "conformance_tests"
20
19
  DEFAULT_BUILD_DEST = "dist"
21
20
  DEFAULT_CONFORMANCE_TESTS_DEST = "dist_conformance_tests"
22
21
 
@@ -299,23 +298,15 @@ def create_parser():
299
298
  parser,
300
299
  "--unittests-script",
301
300
  type=str,
302
- help="Shell script to run unit tests on generated code. Receives the build folder path as its first argument (default: 'plain_modules').",
303
- )
304
- _add_arg(
305
- parser,
306
- "--conformance-tests-folder",
307
- type=non_empty_string,
308
- default=DEFAULT_CONFORMANCE_TESTS_FOLDER,
309
- help="Folder for conformance test files",
310
- path=True,
301
+ help="Shell script to run unit tests on generated code. Receives the module's code folder path as its first argument (e.g. `plain_modules/module_name/code`).",
311
302
  )
312
303
  _add_arg(
313
304
  parser,
314
305
  "--conformance-tests-script",
315
306
  type=str,
316
307
  help="Path to conformance tests shell script. Every conformance test script should accept two arguments: "
317
- "1) Path to a folder (e.g. `plain_modules/module_name`) containing generated source code, "
318
- "2) Path to a subfolder of the conformance tests folder (e.g. `conformance_tests/subfoldername`) containing test files.",
308
+ "1) Path to a folder (e.g. `plain_modules/module_name/code`) containing generated source code, "
309
+ "2) Path to a subfolder of the module's tests folder (e.g. `plain_modules/module_name/tests/subfoldername`) containing test files.",
319
310
  )
320
311
 
321
312
  _add_arg(
@@ -400,7 +391,7 @@ def create_parser():
400
391
  "--copy-conformance-tests",
401
392
  action="store_true",
402
393
  default=False,
403
- help="If set, copy the conformance tests of code in `--conformance-tests-folder` folder to `--conformance-tests-dest` folder successful rendering. Requires --conformance-tests-script.",
394
+ help="If set, copy the module's conformance tests (from `<build-folder>/<module>/tests`) to `--conformance-tests-dest` folder after successful rendering. Requires --conformance-tests-script.",
404
395
  )
405
396
  _add_arg(
406
397
  parser,
@@ -490,8 +481,8 @@ def parse_arguments(command_line: Optional[Sequence[str]] = None):
490
481
 
491
482
  if args.build_folder == args.build_dest:
492
483
  parser.error("--build-folder and --build-dest cannot be the same")
493
- if args.conformance_tests_folder == args.conformance_tests_dest:
494
- parser.error("--conformance-tests-folder and --conformance-tests-dest cannot be the same")
484
+ if args.conformance_tests_dest == args.build_folder:
485
+ parser.error("--conformance-tests-dest and --build-folder cannot be the same")
495
486
 
496
487
  args.render_conformance_tests = args.conformance_tests_script is not None
497
488
 
plain2code_console.py CHANGED
@@ -11,6 +11,12 @@ CHARACTERS_TO_TOKENS_RULE_OF_THUMB_RATIO = 4
11
11
 
12
12
  logger = logging.getLogger(plain2code_logger.LOGGER_NAME)
13
13
 
14
+ # Colors for log messages, applied by the terminal and the TUI via the "color"
15
+ # parameter of the console methods. The file log always receives plain text.
16
+ RETRY_COLOR = "#FFB454" # Amber
17
+ SUCCESS_COLOR = "#79FC96" # Green
18
+ MUTED_COLOR = "#888888" # Grey
19
+
14
20
 
15
21
  class Plain2CodeConsole(Console):
16
22
  INFO_STYLE = Style()
@@ -33,30 +39,37 @@ class Plain2CodeConsole(Console):
33
39
  logger.debug(f"Exception: {e}")
34
40
  self.llm_encoding = None
35
41
 
36
- def info(self, *args, **kwargs):
37
- logger.info(" ".join(map(str, args)))
38
- super().print(*args, **kwargs, style=self.INFO_STYLE)
42
+ def info(self, *args, color=None, **kwargs):
43
+ self._log_and_print(logging.INFO, self.INFO_STYLE, args, color, kwargs)
39
44
 
40
- def warning(self, *args, **kwargs):
41
- logger.warning(" ".join(map(str, args)))
42
- super().print(*args, **kwargs, style=self.WARNING_STYLE)
45
+ def warning(self, *args, color=None, **kwargs):
46
+ self._log_and_print(logging.WARNING, self.WARNING_STYLE, args, color, kwargs)
43
47
 
44
- def error(self, *args, **kwargs):
45
- logger.error(" ".join(map(str, args)))
46
- super().print(*args, **kwargs, style=self.ERROR_STYLE)
48
+ def error(self, *args, color=None, **kwargs):
49
+ self._log_and_print(logging.ERROR, self.ERROR_STYLE, args, color, kwargs)
47
50
 
48
- def input(self, *args, **kwargs):
51
+ def input(self, *args, color=None, **kwargs):
49
52
  # We also log input as info so it shows in the toggled view
50
- logger.info(" ".join(map(str, args)))
51
- super().print(*args, **kwargs, style=self.INPUT_STYLE)
53
+ self._log_and_print(logging.INFO, self.INPUT_STYLE, args, color, kwargs)
54
+
55
+ def output(self, *args, color=None, **kwargs):
56
+ self._log_and_print(logging.INFO, self.OUTPUT_STYLE, args, color, kwargs)
52
57
 
53
- def output(self, *args, **kwargs):
54
- logger.info(" ".join(map(str, args)))
55
- super().print(*args, **kwargs, style=self.OUTPUT_STYLE)
58
+ def debug(self, *args, color=None, **kwargs):
59
+ self._log_and_print(logging.DEBUG, self.DEBUG_STYLE, args, color, kwargs)
56
60
 
57
- def debug(self, *args, **kwargs):
58
- logger.debug(" ".join(map(str, args)))
59
- super().print(*args, **kwargs, style=self.DEBUG_STYLE)
61
+ def _log_and_print(self, level, base_style, args, color, kwargs):
62
+ """Log the plain message text, then print it styled to the terminal.
63
+
64
+ The optional color is applied by the terminal (via style) and forwarded to
65
+ the TUI as the "log_color" record attribute; the file log stays plain text.
66
+ """
67
+ logger.log(level, " ".join(map(str, args)), extra={"log_color": color})
68
+ style = base_style + Style(color=color) if color else base_style
69
+ # Log messages must render exactly as logged: don't interpret square brackets
70
+ # in interpolated content (error texts, file names) as Rich markup.
71
+ kwargs.setdefault("markup", False)
72
+ super().print(*args, **kwargs, style=style)
60
73
 
61
74
  def print_list(self, items, style=None):
62
75
  for item in items:
@@ -130,9 +143,7 @@ class Plain2CodeConsole(Console):
130
143
  for resource_name in resources_list:
131
144
  if resource_name["target"] in linked_resources:
132
145
  file_tokens = self._count_tokens(linked_resources[resource_name["target"]])
133
- self.debug(
134
- f"- {resource_name['text']} [#4169E1]({resource_name['target']}, {file_tokens} tokens)[/#4169E1]"
135
- )
146
+ self.debug(f"- {resource_name['text']} ({resource_name['target']}, {file_tokens} tokens)")
136
147
 
137
148
  self.input()
138
149
 
plain2code_events.py CHANGED
@@ -44,6 +44,7 @@ class LogMessageEmitted(BaseEvent):
44
44
  level: str # e.g., "INFO", "DEBUG", "ERROR"
45
45
  message: str # The actual log message
46
46
  timestamp: str # Formatted timestamp
47
+ log_color: Optional[str] = None # Color for the message (e.g. "#FFB454"); the file log stays plain text
47
48
 
48
49
 
49
50
  @dataclass
plain2code_logger.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import time
3
2
 
4
3
  from event_bus import EventBus
5
4
  from plain2code_events import LogMessageEmitted
@@ -42,9 +41,7 @@ class ElapsedTimeFormatter(logging.Formatter):
42
41
  def format(self, record):
43
42
  # Calculate elapsed time the same way as LoggingHandler does for the TUI
44
43
  try:
45
- offset_seconds = self.run_state.render_time_accumulated + int(
46
- time.monotonic() - self.run_state.last_render_start_timestamp
47
- )
44
+ offset_seconds = self.run_state.get_live_render_time()
48
45
  except Exception:
49
46
  # If RunState is not available or there's any error, default to 00:00:00
50
47
  offset_seconds = 0
@@ -74,9 +71,7 @@ class LoggingHandler(logging.Handler):
74
71
 
75
72
  def emit(self, record):
76
73
  try:
77
- offset_seconds = self.run_state.render_time_accumulated + int(
78
- time.monotonic() - self.run_state.last_render_start_timestamp
79
- )
74
+ offset_seconds = self.run_state.get_live_render_time()
80
75
 
81
76
  hours = offset_seconds // 3600
82
77
  minutes = (offset_seconds % 3600) // 60
@@ -88,6 +83,7 @@ class LoggingHandler(logging.Handler):
88
83
  level=record.levelname,
89
84
  message=record.getMessage(),
90
85
  timestamp=timestamp,
86
+ log_color=getattr(record, "log_color", None),
91
87
  )
92
88
  self.event_bus.publish(event)
93
89
  except RuntimeError:
plain2code_state.py CHANGED
@@ -48,7 +48,23 @@ class RunState:
48
48
  self.rendered_functionalities += 1
49
49
 
50
50
  def add_to_render_time(self):
51
- self.render_time_accumulated += int(time.monotonic() - self.last_render_start_timestamp)
51
+ now = time.monotonic()
52
+ self.render_time_accumulated += int(now - self.last_render_start_timestamp)
53
+ # Reset the segment start so get_live_render_time() does not re-count the
54
+ # span that was just banked (e.g. after a render completes).
55
+ self.last_render_start_timestamp = now
56
+
57
+ def get_live_render_time(self) -> int:
58
+ """Render time so far in whole seconds, including the in-progress segment.
59
+
60
+ This is the single source of truth for elapsed render time, shared by the
61
+ log timestamps, the TUI usage line, and the post-render summary. Pause time
62
+ is excluded because ``add_to_render_time`` banks the elapsed span at the
63
+ start of a pause and ``set_last_render_start_timestamp`` restarts the
64
+ segment on resume. The value is only transiently inflated while the code
65
+ sits inside the pause loop itself, which no consumer samples.
66
+ """
67
+ return self.render_time_accumulated + int(time.monotonic() - self.last_render_start_timestamp)
52
68
 
53
69
  def set_last_render_start_timestamp(self):
54
70
  self.last_render_start_timestamp = time.monotonic()