codeplain 0.3.7__py3-none-any.whl → 0.3.8__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.
@@ -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.8
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,6 +1,5 @@
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
@@ -10,25 +9,26 @@ memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
10
9
  module_renderer.py,sha256=DZCIo_QzvhFw8EqQTKeF3HUbvfumrV2O1sz-gUhWpIA,6752
11
10
  partial_rendering.py,sha256=0ChOaGytogBUEGISpbWkbjT9Hqy8pnOkJdJXcitJDgk,12219
12
11
  path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
13
- plain2code.py,sha256=HGv3Or296pZRuqD5jcmuA3QnGl34aJD5f8619M7l5G4,15294
12
+ plain2code.py,sha256=YLwwzAvE0EWz51V9p8b_kbTltkPvhA2Q8J4JrYyMrAM,15327
14
13
  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
14
+ plain2code_console.py,sha256=Ifs8tmfNX31UujQ5_0sk9AJcajMG_6qhF-6QZcfbcEE,5773
15
+ plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
17
16
  plain2code_exceptions.py,sha256=wkkEXw2k4LCd09PIH3_Hu0HyZvtXYjFaA653Sfey4FA,2287
18
- plain2code_logger.py,sha256=RJNhesE9y59lPILvGhiJpAB4snTwfsHpG1Ir6EaPhiQ,4989
17
+ plain2code_logger.py,sha256=NRPLVIMnCHNVl6dII9gFoIzMdr8xa7icnL0T_Fx2XLw,4839
19
18
  plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
20
19
  plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
21
- plain2code_state.py,sha256=1VwyxgKdA13fAJNKSiPBfBb3A20TKfGCNnOm06YdcS4,2233
20
+ plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
22
21
  plain2code_telemetry.py,sha256=7yuTH7mQE_wGVW2H1oiVZ3UJg98ArB5HcUUm9isnv9g,4354
23
- plain2code_utils.py,sha256=id04t9Iez5pYygrE3fKEyrcfWDcniKFT2iQoF2CzTJs,1409
22
+ plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
24
23
  plain_file.py,sha256=k2rcq53q0kOf9_8egrZILpkrVkZdfJt7HU0gHBFskU4,35116
25
24
  plain_modules.py,sha256=uvRPfxBJwstaWi5lgYKgtQdhhf0Naiy2mS41tLw_TjY,16186
26
25
  plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
27
- system_config.py,sha256=trgytfXKXoCn0Pgy9u00x2D0ZRHlh_tADlbkwZnF8QE,1038
26
+ system_config.py,sha256=VyE6MhXfi6agZvE02p_G4K8yCHkLh3eu890cBzPyDvs,2196
27
+ usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
28
28
  cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
29
29
  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
30
+ cli_output/render_summary.py,sha256=-yRoI7ZZTS9IHyp1vd-sFleGXTVbxlrCkvuM5TX0Mp4,1168
31
+ cli_output/status.py,sha256=MvdocFXL1aA73NMp92gQb0Nd-f7-iw66XVUiHK33sEM,6194
32
32
  config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
33
33
  config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
34
34
  docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
@@ -37,9 +37,9 @@ render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
37
  render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt1M,3959
38
38
  render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
39
39
  render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
40
- render_machine/render_context.py,sha256=fjU_S5W5HoAX3-WlEM8hp4DKMX_NDgVlfwQUAp0yaf8,25633
40
+ render_machine/render_context.py,sha256=eoaO807LntIRYjLSpc3x7bvwBnFfzLlHmKkyKlIRrxs,25697
41
41
  render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
42
- render_machine/render_utils.py,sha256=ao0e64VSOGYfb8hwJXIZlbqer5HjNMYJWHlLDoHnJyc,9123
42
+ render_machine/render_utils.py,sha256=Jb9NJ73yV1qCfGy9S5A9zcnz0FGVt3MHG8Ofk-9A-aI,9355
43
43
  render_machine/state_machine_config.py,sha256=NnX5sBGkc6cNG8OOQ4PrJeJBCADRGZKJYQXV7ndR4bs,26789
44
44
  render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
45
45
  render_machine/triggers.py,sha256=VekNhHGR-u5JXIboqc_Eyqf73LNH7wezLK7r3WWtodk,1269
@@ -47,16 +47,16 @@ render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySx
47
47
  render_machine/actions/base_action.py,sha256=vVzMp-LanrZyq-jamHyWaQxpRl-pLqtTdtiEzUBhfyE,534
48
48
  render_machine/actions/commit_conformance_tests_changes.py,sha256=08yxCPCpf5TlrzLPsEh5SJXVVvnLfwdNKSr7BlzlYxo,2589
49
49
  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
50
+ render_machine/actions/create_dist.py,sha256=ykER7i2OvWAXjrlAZnkQHpiNl00vSMF8WDxNhlWKfsA,1088
51
+ render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9HWaPkXyqg7ck,1279
52
52
  render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
53
- render_machine/actions/fix_conformance_test.py,sha256=jtVeL11J8K-a5YvoR3uW6TVytQZkiBgXlDyFR3wXYEs,10022
53
+ render_machine/actions/fix_conformance_test.py,sha256=EZLVjF4A8zSTmyqdgcLoz-2pLpeieq4M_Rb71pY3IKA,10095
54
54
  render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
55
55
  render_machine/actions/prepare_repositories.py,sha256=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
56
56
  render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
57
57
  render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
58
58
  render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
59
- render_machine/actions/render_functional_requirement.py,sha256=uiEwrMdArRdPX7eWysYMTI5y2pXacUySTpIgL2O-lOI,4602
59
+ render_machine/actions/render_functional_requirement.py,sha256=53KzMVlcefoag1x73roCEQvQ7FbmH8FzhXMQau-s8tg,4655
60
60
  render_machine/actions/run_conformance_tests.py,sha256=4sJTORl6YLFXQIdLMBQHDeJd2hztGyt3tsVvrCviBko,4008
61
61
  render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
62
62
  render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
@@ -68,7 +68,8 @@ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
68
68
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
69
69
  tests/test_arg_provenance.py,sha256=Vp-ZtNbTlSNOE8iiHOin3YI9sobbN0tRPUIfrYjxttQ,3459
70
70
  tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
71
- tests/test_cli_output.py,sha256=owDP9pLHdErtPAr1paNKp1o044kuH-HxInHLIBLZQFI,18420
71
+ tests/test_cli_output.py,sha256=iKS1Li4yhoIL0ezJ58o4oEf9V8TiKcQkvQLVtJpdM4c,18420
72
+ tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
72
73
  tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
73
74
  tests/test_folder_path_resolution.py,sha256=_ZvVFu5m2-AH8iQNn0fCYkA0flVxVwdh1oQiWcPNIxw,6611
74
75
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
@@ -78,6 +79,8 @@ tests/test_logging_config_path_resolution.py,sha256=avz_ehRd18INwIXvAy1AwVzNNUz0
78
79
  tests/test_partial_rendering.py,sha256=r0RUUd1pI4-2ZkRrM1j8_kUqv-70hrOGjuKBhZoRmH0,22758
79
80
  tests/test_path_resolution.py,sha256=yQpd-Unq1lwNv0wkRPwVs5pDT8JmyyDgbpiTQsyt--Y,2615
80
81
  tests/test_plain2code.py,sha256=DQYFc0JVSkMI8LZCNaQ__2Nph-DcZZW17rprJA54eyw,3851
82
+ tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlBI,1217
83
+ tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
81
84
  tests/test_plain_modules.py,sha256=S7sd4ZYNotXOgCyZzmBd714QFLih69nlZ3tV8WNtVPo,13766
82
85
  tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
83
86
  tests/test_plainfileparser.py,sha256=xd9P0KqZJlJPn3RKT510IQbfBotk6nQ9O8p9iLwJiQk,22815
@@ -86,20 +89,22 @@ tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
86
89
  tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
87
90
  tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
88
91
  tests/test_telemetry.py,sha256=hjm81aRQQZcZ0RB6aVbS3WO46_nIAii_csmbbtnUh74,7110
92
+ tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
93
+ tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
89
94
  tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
90
95
  tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
91
96
  tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
92
97
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
93
- tui/components.py,sha256=DoMDhwNVVpv--OY6o8YQYPzOQPG9Dyh9kgMOqUYtNiE,25412
98
+ tui/components.py,sha256=XkC4Aq6VyTnFnBQs8PEHwVdAOczhm0ebKN92kiYMbq0,25791
94
99
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
95
- tui/plain2code_tui.py,sha256=t-0DonjrK6_x6jPUMYW0w6tvjZZlk89E-DEor7GzjG4,13785
100
+ tui/plain2code_tui.py,sha256=8F7ZYRpmTiWRdTeQ63LPf2LGrreYjIYXvf_9XNSPyHs,17104
96
101
  tui/plain_module_render_choice_tui.py,sha256=DRl_xhUbUJsyOaCYt_uNDSLUAA3Ty9NYI9zFrVDwp9Y,11270
97
102
  tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
98
103
  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,,
104
+ tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
105
+ tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
106
+ codeplain-0.3.8.dist-info/METADATA,sha256=nfj-9PV0EN0MS1W9ILf1GyOnSfcB-YSyEXW04UY9vHQ,6351
107
+ codeplain-0.3.8.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
108
+ codeplain-0.3.8.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
109
+ codeplain-0.3.8.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
110
+ codeplain-0.3.8.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
plain2code.py CHANGED
@@ -286,6 +286,7 @@ def render( # noqa: C901
286
286
  render_thread = threading.Thread(target=run_render, daemon=True)
287
287
  app = Plain2CodeTUI(
288
288
  event_bus=event_bus,
289
+ run_state=run_state,
289
290
  on_ready=render_thread.start,
290
291
  render_id=run_state.render_id,
291
292
  unittests_script=args.unittests_script,
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()
plain2code_utils.py CHANGED
@@ -16,19 +16,23 @@ def find_large_base64_blob(text: str) -> Optional[str]:
16
16
  return match.group(0) if match else None
17
17
 
18
18
 
19
- def format_duration_hms(total_seconds: int) -> str:
20
- """Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
21
- if total_seconds < 0:
22
- total_seconds = 0
23
- h = int(total_seconds // 3600)
24
- m = int((total_seconds % 3600) // 60)
25
- s = total_seconds % 60
26
- if h:
27
- return f"{h}h {m}m {s}s"
28
- if m:
29
- return f"{m}m {s}s"
30
- text = f"{s}".rstrip("0").rstrip(".")
31
- return f"{text}s" if text else "0s"
19
+ def format_duration_hms(total_seconds: float) -> str:
20
+ """Format a whole-second duration compactly (e.g. ``10s``, ``5m 49s``, ``1h 2m``).
21
+
22
+ Fractional seconds are truncated. Durations under a minute render as ``{s}s``,
23
+ under an hour as ``{m}m {s}s``, and beyond as ``{h}h {m}m``.
24
+ """
25
+ elapsed = int(total_seconds)
26
+ if elapsed < 0:
27
+ elapsed = 0
28
+ if elapsed < 60:
29
+ return f"{elapsed}s"
30
+ minutes = elapsed // 60
31
+ seconds = elapsed % 60
32
+ if minutes < 60:
33
+ return f"{minutes}m {seconds}s"
34
+ hours = minutes // 60
35
+ return f"{hours}h {minutes % 60}m"
32
36
 
33
37
 
34
38
  AMBIGUITY_CAUSES = {
@@ -1,7 +1,7 @@
1
1
  from typing import Any
2
2
 
3
3
  import file_utils
4
- from plain2code_console import console
4
+ from plain2code_console import SUCCESS_COLOR, console
5
5
  from render_machine.actions.base_action import BaseAction
6
6
  from render_machine.render_context import RenderContext
7
7
 
@@ -21,6 +21,6 @@ class CreateDist(BaseAction):
21
21
  render_context.conformance_tests.get_module_conformance_tests_folder(render_context.module_name),
22
22
  render_context.conformance_tests_dest,
23
23
  )
24
- console.info(f"[#79FC96]Render of module {render_context.module_name} completed successfully.[/#79FC96]")
24
+ console.info(f"✓ Render of module {render_context.module_name} completed successfully.", color=SUCCESS_COLOR)
25
25
 
26
26
  return self.SUCCESSFUL_OUTCOME, None
@@ -20,7 +20,8 @@ class ExitWithError(BaseAction):
20
20
 
21
21
  if render_context.frid_context is not None:
22
22
  console.info(
23
- f"To continue rendering from the last successfully rendered functionality, provide the [red]--render-from {render_context.frid_context.frid}[/red] flag."
23
+ f"To continue rendering from the last successfully rendered functionality, "
24
+ f"provide the --render-from {render_context.frid_context.frid} flag."
24
25
  )
25
26
 
26
27
  if render_context.run_state.render_id is not None:
@@ -4,7 +4,7 @@ import diff_utils
4
4
  import file_utils
5
5
  import plain_spec
6
6
  from memory_management import MemoryManager
7
- from plain2code_console import console
7
+ from plain2code_console import RETRY_COLOR, console
8
8
  from plain2code_exceptions import InternalClientError
9
9
  from render_machine.actions.base_action import BaseAction
10
10
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
@@ -144,7 +144,9 @@ class FixConformanceTest(BaseAction):
144
144
  render_context.conformance_tests_running_context.conflicting_module_name = current_testing_module_name
145
145
  render_context.conformance_tests_running_context.conflicting_frid = current_testing_frid
146
146
  console.info(
147
- f"Potential conflicting functionalities detected while fixing conformance tests for functionality {current_testing_frid} in module {current_testing_module_name}."
147
+ f"↻ Potential conflicting functionalities detected while fixing conformance tests "
148
+ f"for functionality {current_testing_frid} in module {current_testing_module_name}.",
149
+ color=RETRY_COLOR,
148
150
  )
149
151
 
150
152
  if issue_reason_code == self.ISSUE_REASON_CODE_CONFORMANCE_TESTS:
@@ -3,7 +3,7 @@ from typing import Any
3
3
  import file_utils
4
4
  import render_machine.render_utils as render_utils
5
5
  from memory_management import MemoryManager
6
- from plain2code_console import console
6
+ from plain2code_console import RETRY_COLOR, console
7
7
  from plain2code_exceptions import FunctionalRequirementTooComplex
8
8
  from render_machine.actions.base_action import BaseAction
9
9
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
@@ -28,8 +28,9 @@ class RenderFunctionalRequirement(BaseAction):
28
28
 
29
29
  if render_context.frid_context.functional_requirement_render_attempts > 1:
30
30
  console.info(
31
- f"Unittests could not be fixed after rendering the functionality. "
32
- f"Restarting rendering functionality {render_context.frid_context.frid} from scratch."
31
+ f"↻ Unittests could not be fixed after rendering the functionality. "
32
+ f"Restarting rendering functionality {render_context.frid_context.frid} from scratch.",
33
+ color=RETRY_COLOR,
33
34
  )
34
35
 
35
36
  render_utils.revert_changes_for_frid(render_context)
@@ -7,7 +7,7 @@ import git_utils
7
7
  import plain_spec
8
8
  from codeplain_REST_api import CodeplainAPI
9
9
  from event_bus import EventBus
10
- from plain2code_console import console
10
+ from plain2code_console import RETRY_COLOR, console
11
11
  from plain2code_events import RenderContextSnapshot
12
12
  from plain2code_state import RunState
13
13
  from plain_modules import PlainModule
@@ -283,9 +283,11 @@ class RenderContext:
283
283
  self.dispatch_error(error_msg)
284
284
  else:
285
285
  console.info(
286
- f"Failed to adjust the unit tests after implementation code was updated while fixing the conformance tests for functionality {self.frid_context.frid}."
286
+ f"↻ Failed to adjust the unit tests after implementation code was updated while fixing the "
287
+ f"conformance tests for functionality {self.frid_context.frid}. "
288
+ f"Restarting rendering the functionality {self.frid_context.frid} from scratch.",
289
+ color=RETRY_COLOR,
287
290
  )
288
- console.info(f"Restarting rendering the functionality {self.frid_context.frid} from scratch.")
289
291
  self.machine.dispatch(triggers.RESTART_FRID_PROCESSING)
290
292
 
291
293
  def _on_unit_test_limit_exceeded_in_refactoring(self):
@@ -14,7 +14,7 @@ if sys.platform == "linux":
14
14
  import file_utils
15
15
  import git_utils
16
16
  import plain_spec
17
- from plain2code_console import console
17
+ from plain2code_console import MUTED_COLOR, RETRY_COLOR, SUCCESS_COLOR, console
18
18
  from plain2code_exceptions import RenderCancelledError
19
19
 
20
20
  SCRIPT_EXECUTION_TIMEOUT = 120
@@ -178,24 +178,30 @@ def execute_script( # noqa: C901
178
178
  temp_file.write(f"{script_type} script {script} successfully passed.\n")
179
179
  temp_file.write(f"{script_type} script execution time: {elapsed_time:.2f} seconds.\n")
180
180
 
181
- console.debug(f"[#888888]{script_type} script output stored in: {temp_file_path.strip()}[/#888888]")
181
+ console.debug(f"{script_type} script output stored in: {temp_file_path.strip()}", color=MUTED_COLOR)
182
182
 
183
183
  if proc.returncode != 0:
184
184
  if frid is not None:
185
- console.debug(
186
- f"The {script_type} script for functionality ID {frid} of module {module} has failed. Initiating the patching mode to automatically correct the discrepancies."
185
+ console.info(
186
+ f"↻ The {script_type} script for functionality ID {frid} of module {module} has failed. "
187
+ f"Initiating the patching mode to automatically correct the discrepancies.",
188
+ color=RETRY_COLOR,
187
189
  )
188
190
  else:
189
- console.debug(
190
- f"The {script_type} script has failed. Initiating the patching mode to automatically correct the discrepancies."
191
+ console.info(
192
+ f"↻ The {script_type} script has failed. "
193
+ f"Initiating the patching mode to automatically correct the discrepancies.",
194
+ color=RETRY_COLOR,
191
195
  )
192
196
  else:
193
197
  if frid is not None:
194
198
  console.info(
195
- f"[#79FC96]The {script_type} script for functionality ID {frid} of module {module} has passed successfully.[/#79FC96]"
199
+ f"✓ The {script_type} script for functionality ID {frid} of module {module} "
200
+ f"has passed successfully.",
201
+ color=SUCCESS_COLOR,
196
202
  )
197
203
  else:
198
- console.info(f"[#79FC96]All {script_type} scripts have passed successfully.[/#79FC96]")
204
+ console.info(f"✓ All {script_type} scripts have passed successfully.", color=SUCCESS_COLOR)
199
205
 
200
206
  return proc.returncode, sanitized_script_output, temp_file_path
201
207
 
system_config.py CHANGED
@@ -1,12 +1,46 @@
1
1
  import importlib.resources
2
+ import os
2
3
  import sys
3
4
 
4
5
  import yaml
5
6
 
6
- from _version import __version__
7
7
  from plain2code_console import console
8
8
 
9
9
 
10
+ def _resolve_version() -> str:
11
+ """Resolve the client version.
12
+
13
+ For an installed package the version is read from its metadata (hatch-vcs
14
+ bakes it in from the git tag at build time). When running from a source
15
+ checkout that hasn't been installed, fall back to the nearest git tag.
16
+ """
17
+ from importlib.metadata import PackageNotFoundError, version
18
+
19
+ try:
20
+ return version("codeplain")
21
+ except PackageNotFoundError:
22
+ pass
23
+
24
+ try:
25
+ import git
26
+
27
+ # Anchor to this source file's own location so we inspect the
28
+ # codeplain checkout's repo, not the caller's working directory
29
+ # (codeplain may be run from anywhere).
30
+ source_dir = os.path.dirname(os.path.abspath(__file__))
31
+ repo = git.Repo(source_dir, search_parent_directories=True)
32
+
33
+ # Highest version tag, regardless of branch ancestry (a dev run may sit
34
+ # on a feature branch that doesn't descend from the latest release tag).
35
+ latest_tag = repo.git.tag("--list", "--sort=-v:refname").splitlines()[0]
36
+ return latest_tag.lstrip("v")
37
+ except Exception:
38
+ return "0.0.0.dev0"
39
+
40
+
41
+ __version__ = _resolve_version()
42
+
43
+
10
44
  class SystemConfig:
11
45
  """Manages system-level configuration including requirements and error messages."""
12
46
 
tests/test_cli_output.py CHANGED
@@ -79,14 +79,14 @@ class TestDisplayCreditLine:
79
79
  "type": "pro",
80
80
  "total": 1000,
81
81
  "remaining": 200,
82
- "period_end": "2026-07-15T00:00:00+00:00",
82
+ "period_end": "2028-07-15T00:00:00+00:00",
83
83
  }
84
84
  _display_credit_line(plan_credits)
85
85
 
86
86
  call_args = mock_console.print.call_args[0][0]
87
87
  assert "PRO plan" in call_args
88
88
  assert "200 of 1000 remaining" in call_args
89
- assert "expires Jul 15, 2026" in call_args
89
+ assert "expires Jul 15, 2028" in call_args
90
90
 
91
91
  @patch("cli_output.status.console")
92
92
  def test_display_expired_credits(self, mock_console):