codeplain 0.3.10.dev5__py3-none-any.whl → 0.3.11__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.
cli_output/status.py CHANGED
@@ -6,6 +6,8 @@ from typing import Optional
6
6
  import codeplain_REST_api as codeplain_api
7
7
  from plain2code_console import console
8
8
 
9
+ PLANS_URL = "https://platform.codeplain.ai/plans"
10
+
9
11
 
10
12
  def _create_progress_bar(remaining: int, total: int, width: int = 30) -> str:
11
13
  """Create a Unicode progress bar."""
@@ -42,12 +44,12 @@ def _display_credit_line(plan_credits: dict) -> None:
42
44
  bar = _create_progress_bar(remaining, total, width=30)
43
45
 
44
46
  # Format plan type label
45
- plan_label = "Free trial" if plan_type == "free" else plan_type.upper() + " plan"
47
+ plan_label = "Free credits" if plan_type == "free" else plan_type.upper() + " plan"
46
48
 
47
49
  if is_expired:
48
- console.print(f" {plan_label:10} {bar} expired {formatted_date}")
50
+ console.print(f" {plan_label:12} {bar} expired {formatted_date}")
49
51
  else:
50
- console.print(f" {plan_label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
52
+ console.print(f" {plan_label:12} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
51
53
 
52
54
 
53
55
  def _display_bucket_credit_line(bucket: dict, label: str) -> None:
@@ -71,9 +73,9 @@ def _display_bucket_credit_line(bucket: dict, label: str) -> None:
71
73
  bar = _create_progress_bar(remaining, total, width=30)
72
74
 
73
75
  if is_expired:
74
- console.print(f" {label:10} {bar} expired {formatted_date}")
76
+ console.print(f" {label:12} {bar} expired {formatted_date}")
75
77
  else:
76
- console.print(f" {label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
78
+ console.print(f" {label:12} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
77
79
 
78
80
 
79
81
  def _display_status_message(plan_credits: Optional[dict], purchased_credits: list, promo_credits: list) -> None:
@@ -107,7 +109,11 @@ def _display_status_message(plan_credits: Optional[dict], purchased_credits: lis
107
109
  break
108
110
 
109
111
  if not has_remaining:
110
- console.print("\nNo rendering credits remaining. Upgrade to continue rendering.")
112
+ console.print(
113
+ f"\nNo rendering credits remaining. Upgrade to the PRO plan at {PLANS_URL} to continue rendering."
114
+ )
115
+ else:
116
+ console.print(f"\nTo manage your plan navigate to {PLANS_URL}")
111
117
 
112
118
 
113
119
  def print_status(api_key: str, api_url: str, client_version: str) -> None:
@@ -148,7 +154,7 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
148
154
  # Display rendering credits section
149
155
  console.print("Rendering credits:")
150
156
 
151
- # Display plan credits (free trial or subscription)
157
+ # Display plan credits (free credits or subscription)
152
158
  if plan_credits:
153
159
  _display_credit_line(plan_credits)
154
160
 
@@ -162,4 +168,3 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
162
168
 
163
169
  # Display status messages and management link
164
170
  _display_status_message(plan_credits, purchased_credits, promo_credits)
165
- console.print("\nTo manage your plan navigate to https://platform.codeplain.ai/plans")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.10.dev5
3
+ Version: 0.3.11
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.55
11
+ Requires-Dist: gitpython==3.1.59
12
12
  Requires-Dist: mistletoe==1.6.0
13
13
  Requires-Dist: networkx==3.6.1
14
14
  Requires-Dist: python-frontmatter==1.3.0
@@ -16,9 +16,9 @@ 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.66.1
19
+ Requires-Dist: sentry-sdk==2.68.1
20
20
  Requires-Dist: textual>=7.5.0
21
- Requires-Dist: tiktoken==0.13.0
21
+ Requires-Dist: tiktoken==0.14.0
22
22
  Requires-Dist: transitions==0.9.3
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: black==26.5.1; extra == 'dev'
@@ -1,19 +1,20 @@
1
1
  change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
2
- codeplain_REST_api.py,sha256=Iu3htCE05k-8BOA6I2acz4Ezr74cHQIXmhBIsgnX4Ng,21232
2
+ codeplain_REST_api.py,sha256=ptb2u0mC5FJqMNeUbcc-bNXNhy9NRJZa5vI80uSFSwQ,22715
3
3
  concept_utils.py,sha256=TpyiQ4DfMtO231j-lPj65i0suRTPr8t9j3QNf9KiM4A,8080
4
4
  conformance_fix_journal.py,sha256=7wL3UOc3BWaUo5MCmiFbb3kIwOQ6iEpdQSyEmedo8Vk,9488
5
5
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
6
6
  event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
7
- file_utils.py,sha256=MNZ5li1ofIMOYB6FCrMa_UkX6nkGCgSZuaqzhqVc1Ok,11682
7
+ file_utils.py,sha256=IshKvld9LJLwXSB3lxZ1HQyVHa93I626BsTScBu95ss,12118
8
+ git_preflight.py,sha256=vB2A8GyYz_iWr8jOE4POFLBHr3qihmedWuidspGdsrw,1509
8
9
  git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
9
10
  memory_management.py,sha256=CwIOQqF7Xo1sNfvP69zQRK_CpjinQD9dbolhRPl6lTk,2446
10
11
  metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
11
12
  module_renderer.py,sha256=IJBle8mbBARn_xrekjSpsVID7719yy2rNPkM15-ONoI,6982
12
13
  partial_rendering.py,sha256=uzZc2j4d3pAXzfiHw_y1TK4yLRsLOb1Mm21570AGxJ8,13837
13
14
  path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
14
- plain2code.py,sha256=v8xBTv3mQjntpBTBJUvl5naGnSPFAzhEQTqpeRr40ow,18072
15
+ plain2code.py,sha256=FV13ItuD9FQN4FhS4H7RXHh7MoQaAlGSh1ctkjeMSoo,20368
15
16
  plain2code_arguments.py,sha256=xMK-R13Vu005bHz3MUDY8F0Cf3ED9hCPqNsC6jGZmkk,17620
16
- plain2code_console.py,sha256=5N4_GIV8qE-2zvFh6qoOHhhsuwCF_3r7jG19S-YWtfk,5906
17
+ plain2code_console.py,sha256=YGBONkQtwZUkrjxTKhqcBoRzW0EmSfYHPH8P4_FM60I,6040
17
18
  plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
18
19
  plain2code_exceptions.py,sha256=rtFEQbs1ruzSlGXIGJZoZry9sjYzmhPKHiUQIECcbL0,2567
19
20
  plain2code_logger.py,sha256=YQ-ENYBHKXROVv6Q52xh7eXrFkj_3Lex__N9zfxlw9Y,5729
@@ -22,15 +23,15 @@ plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,223
22
23
  plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
23
24
  plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
24
25
  plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
25
- plain_file.py,sha256=Kn8m_w6c04_RR-FBjAU05UcLOTDk4zcctpJXghYdZ8I,35757
26
- plain_modules.py,sha256=kUGR2mX908FEKOO8b2CJuk2rn9iRj8qyWGFMWTdfzoY,25977
26
+ plain_file.py,sha256=yzdHYBZye1UjI6K-vcod5u9hPqQ_zBQS050yypdmB3M,37614
27
+ plain_modules.py,sha256=nXlmw0J2rKkuU1Y3A68LVtZ1Ffdc-D5iMjxaXSaSHTQ,25963
27
28
  plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
28
29
  system_config.py,sha256=sA8wiPmBMtRsLwSQ40U5pVdLazAafbV-1Z19Yp0HiSQ,5314
29
30
  usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
30
31
  cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
31
32
  cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
32
33
  cli_output/render_summary.py,sha256=-yRoI7ZZTS9IHyp1vd-sFleGXTVbxlrCkvuM5TX0Mp4,1168
33
- cli_output/status.py,sha256=MvdocFXL1aA73NMp92gQb0Nd-f7-iw66XVUiHK33sEM,6194
34
+ cli_output/status.py,sha256=TrRo-BYUJmzjVzPlMUDpE76FUSpSsN_JdsKigKMrdmg,6294
34
35
  config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
35
36
  config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
36
37
  docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
@@ -40,7 +41,7 @@ render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt
40
41
  render_machine/conformance_tests.py,sha256=PNTCnJNd_xNIhpTPPS8vlx8qws3cTgQSTLdADRab2YM,9617
41
42
  render_machine/implementation_code_helpers.py,sha256=Fxm-IInoZjAIZn41_-WXWj7VIYczpW4yH44Gt8CYMAQ,2378
42
43
  render_machine/render_context.py,sha256=iaJWI_EMSn51LNF_b7KRvlir46ZgUkjbaEiVgSvMTCI,26314
43
- render_machine/render_types.py,sha256=IY16mGnt5uoZlLKy16Wmpq0_M5PAitVDvt2khnDhKU0,8479
44
+ render_machine/render_types.py,sha256=A5BHAkjmDHQ0pd5rR6THlRY5z6rzrdJdcgZFhN7yw9w,8860
44
45
  render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
45
46
  render_machine/state_machine_config.py,sha256=0pL4PyrZ5F2UumvHLMRfhyh64M4pDvf-9zEs-5OAEU8,27866
46
47
  render_machine/states.py,sha256=Lu-7upbs9_bjJsy0P738qx5IiN0DzVcFdhF2-m6YjnQ,2067
@@ -53,8 +54,8 @@ render_machine/actions/create_dist.py,sha256=ykER7i2OvWAXjrlAZnkQHpiNl00vSMF8WDx
53
54
  render_machine/actions/distill_conformance_test_memory.py,sha256=ENjfhTWqRLePHeRF4t8zNRhWKDV2rU4pLYuU_CZhhmo,2639
54
55
  render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9HWaPkXyqg7ck,1279
55
56
  render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
56
- render_machine/actions/fix_conformance_test.py,sha256=mHk-UJNEZ7Y65YLLUBljJ3Z5zBVA4jIYRSJ0VCd_LOw,11012
57
- render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
57
+ render_machine/actions/fix_conformance_test.py,sha256=Yy6niLefPIyR3vSxyyX4NUi3NJ4dVc0aEAH9JBCBYX4,11572
58
+ render_machine/actions/fix_unit_tests.py,sha256=pqZzp7snSAe967o_kbh1yAhp9pZIMFY9uoJK1HGcCuc,3532
58
59
  render_machine/actions/prepare_repositories.py,sha256=HyZ1R6E3H5V-G4WDSGfhxl_arskmW9FcaxNdQr6JjVs,3867
59
60
  render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
60
61
  render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
@@ -71,13 +72,15 @@ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
71
72
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
72
73
  tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
73
74
  tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
74
- tests/test_cli_output.py,sha256=ZV2F1NNW4EiZvMNkhF9XUZH6bFJbddfkZof5qAFxe3Y,18885
75
+ tests/test_cli_output.py,sha256=9yTk0pD2w6vkiSdSkYwneUmEtNqX4x64F6PzYShUEcw,19331
75
76
  tests/test_conformance_fix_journal.py,sha256=olTeAuxXgFwQ8LSQyt0FqtBy9_XY_gK-Ei_pUuJ0C3M,10389
76
- tests/test_console_log_styles.py,sha256=63DQWiwWNhe_rMP70bW9DAB6-EPaqU3Wpwyh8eR09pc,4080
77
+ tests/test_console_log_styles.py,sha256=sxlhynMAP-kmqhBn1-pKvZXdCnlKSyCGJqwYKrh6sQ0,6468
77
78
  tests/test_distill_conformance_test_memory.py,sha256=Tftw9l-gLVwIU6QttY03h2VedvuFxpIsCmZIzvWfxMA,5481
78
79
  tests/test_dry_run_link_validation.py,sha256=ivvYXQPqUkQ8glbFyUrdCninCxF2mg-rAsDcfjcB7MM,2140
79
80
  tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
81
+ tests/test_fix_unit_tests_conformance_context.py,sha256=IEsXK0xMLRuHmoQGBwrY0ENWVng2n-db_YzSO2qBfqM,9136
80
82
  tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
83
+ tests/test_git_preflight.py,sha256=6aQqInDzpk03pS-6UfkGQWjErrIXTEdHs5oeq3ZYzTw,3186
81
84
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
82
85
  tests/test_headless_logging.py,sha256=soNESeotKQaU-SQyxtjpq35l-dLVsauZ_lJ-wk9i2mI,6419
83
86
  tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
@@ -90,30 +93,32 @@ tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlB
90
93
  tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
91
94
  tests/test_plain_modules.py,sha256=Fn3FeH-M4exnyFJDZzc9YWaLEfBmiqPH5TpiBT9jgAs,33033
92
95
  tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
93
- tests/test_plainfileparser.py,sha256=l8GfXkVaSzgH74fX45Wp5jX0q3QlO6t_TgDG72dc28o,23867
96
+ tests/test_plainfileparser.py,sha256=OY7dCilaUEeM4vAOShQYsZXKQzzUOi7nGYBaROu8Te8,26606
94
97
  tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
95
98
  tests/test_prepare_repositories_layout.py,sha256=PhJQLnHfjfjo6CTWoicyEKgL0_GtjwB2OKYAWSBVHtk,14638
99
+ tests/test_render_finished_report.py,sha256=QtViWItlo8YB8bLOv3YwvfLprVZgZ9r8Q1Td7_vGi18,7445
96
100
  tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
97
101
  tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
98
102
  tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
99
103
  tests/test_system_config.py,sha256=gH5qs9q4lh5CljAdz28HUrJzuKqCvkZherFuv7enMak,2787
100
104
  tests/test_telemetry.py,sha256=LINvx1_Oz1OJ3ho2RcIcAA29IsNfUbkVVCbFaQcrBaA,8133
105
+ tests/test_tui_components.py,sha256=O_PB6z_Xt7YWebsD21b99PVgFtqq5c03lVtYytIT5Ps,6200
101
106
  tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
102
107
  tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
103
108
  tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
104
109
  tests/e2e/test_hello_world_python.py,sha256=2Cgs3-79dtQh5gVab257tFDHXTznefZeYqbTjlM4Nng,1632
105
110
  tests/e2e/test_hello_world_python_windows.py,sha256=ggynH_Wm4011M500zoPNNgvD-s4vZ3hvlorf1yARgQQ,2019
106
111
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
107
- tui/components.py,sha256=XkC4Aq6VyTnFnBQs8PEHwVdAOczhm0ebKN92kiYMbq0,25791
112
+ tui/components.py,sha256=tAgP7paI8bR6XAWOWyiUsKexRurEx1CNz_4z81Ty0G4,27195
108
113
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
109
- tui/plain2code_tui.py,sha256=8F7ZYRpmTiWRdTeQ63LPf2LGrreYjIYXvf_9XNSPyHs,17104
114
+ tui/plain2code_tui.py,sha256=b6FOEXt_dcVhpfOrAHvg-Rvn0bSH18moy5zmUaz3NS4,17552
110
115
  tui/plain_module_render_choice_tui.py,sha256=XwBMMzyDat9FYqMkKdIV7kmIevddfwGA5Qk9kgy5sYY,12112
111
116
  tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
112
117
  tui/state_handlers.py,sha256=zbUS_D9eU8qIwVpaxPEp9zL9bpg8gXsWNoUaqg9TLrQ,16664
113
118
  tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
114
- tui/widget_helpers.py,sha256=ToO9MV2bF2Rp-XMNcjIYclnB_qmgO5sLw4UtQ2CyTMg,6652
115
- codeplain-0.3.10.dev5.dist-info/METADATA,sha256=nG1mz4SKQy9tZ2ybsG-Va9_plwrluGfGVc9AxONWh2s,7256
116
- codeplain-0.3.10.dev5.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
117
- codeplain-0.3.10.dev5.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
118
- codeplain-0.3.10.dev5.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
119
- codeplain-0.3.10.dev5.dist-info/RECORD,,
119
+ tui/widget_helpers.py,sha256=jitm2WHiKj-NVFL-hULxWzckPreRbUTl9OIZLN31-ek,6997
120
+ codeplain-0.3.11.dist-info/METADATA,sha256=WtLGmKTaR0d2o383f3bvaZ6JtDdF86bBNjcRxncpLSQ,7251
121
+ codeplain-0.3.11.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
122
+ codeplain-0.3.11.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
123
+ codeplain-0.3.11.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
124
+ codeplain-0.3.11.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.4
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
codeplain_REST_api.py CHANGED
@@ -11,6 +11,10 @@ from plain2code_state import RunState
11
11
  MAX_RETRIES = 4
12
12
  RETRY_DELAY = 3
13
13
 
14
+ # A report the render does not consume must not be able to hold the process open
15
+ # at exit, and requests waits indefinitely by default.
16
+ REPORT_TIMEOUT_SECONDS = 5
17
+
14
18
  RETRY_ERROR_CODES = [
15
19
  "LLMInternalError",
16
20
  ]
@@ -125,6 +129,7 @@ class CodeplainAPI:
125
129
  run_state: Optional[RunState],
126
130
  num_retries: int = MAX_RETRIES,
127
131
  silent: bool = False,
132
+ timeout: Optional[int] = None,
128
133
  ):
129
134
  if run_state is not None:
130
135
  self._extend_payload_with_run_state(payload, run_state)
@@ -134,7 +139,7 @@ class CodeplainAPI:
134
139
 
135
140
  for attempt in range(num_retries + 1):
136
141
  try:
137
- response = requests.post(endpoint_url, headers=headers, json=payload)
142
+ response = requests.post(endpoint_url, headers=headers, json=payload, timeout=timeout)
138
143
 
139
144
  try:
140
145
  response_json = response.json()
@@ -173,6 +178,27 @@ class CodeplainAPI:
173
178
  }
174
179
  return self.post_request(endpoint_url, headers, payload, None, num_retries=0, silent=True)
175
180
 
181
+ def render_finished(self, outcome: str, run_state: RunState):
182
+ """Report the terminal state a render reached.
183
+
184
+ Called once per render, whatever the outcome. The render state machine
185
+ lives here, so the API cannot tell a finished render from one still
186
+ going; it has to be told.
187
+
188
+ No retries and no console output: a report has no result the render
189
+ needs, and must not add minutes to a render that has already ended nor
190
+ print retry noise over the exit summary.
191
+ """
192
+ endpoint_url = f"{self.api_url}/render_finished"
193
+ headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
194
+ payload = {
195
+ "outcome": outcome,
196
+ }
197
+
198
+ return self.post_request(
199
+ endpoint_url, headers, payload, run_state, num_retries=0, silent=True, timeout=REPORT_TIMEOUT_SECONDS
200
+ )
201
+
176
202
  def render_functional_requirement(
177
203
  self,
178
204
  frid: str,
@@ -237,6 +263,7 @@ class CodeplainAPI:
237
263
  required_modules,
238
264
  unittests_issue,
239
265
  run_state: RunState,
266
+ conformance_tests_fixes: list[dict] | None = None,
240
267
  ):
241
268
  endpoint_url = f"{self.api_url}/fix_unittests_issue"
242
269
  headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
@@ -251,6 +278,10 @@ class CodeplainAPI:
251
278
  "unittests_issue": unittests_issue,
252
279
  "unittest_batch_id": run_state.unittest_batch_id,
253
280
  }
281
+ # Implementation code changes made by the conformance tests fixer right before this unit tests run;
282
+ # only sent when unit tests are processed inside the conformance tests phase.
283
+ if conformance_tests_fixes is not None:
284
+ payload["conformance_tests_fixes"] = conformance_tests_fixes
254
285
 
255
286
  return self.post_request(endpoint_url, headers, payload, run_state)
256
287
 
file_utils.py CHANGED
@@ -180,6 +180,20 @@ def open_from(dirs, file_name):
180
180
  return None
181
181
 
182
182
 
183
+ def resolve_linked_resource(dirs, file_name):
184
+ """Return the first existing path for file_name across dirs, or None if not found.
185
+
186
+ Mirrors the search order used by open_from so that validating a linked resource and
187
+ loading it can never disagree.
188
+ """
189
+ for dir in dirs:
190
+ full_file_name = os.path.join(dir, file_name)
191
+ if os.path.exists(full_file_name):
192
+ return full_file_name
193
+
194
+ return None
195
+
196
+
183
197
  def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
184
198
  linked_resources = {}
185
199
 
git_preflight.py ADDED
@@ -0,0 +1,41 @@
1
+ """Verify a working git is available before GitPython is imported.
2
+
3
+ GitPython probes for the git executable on import and raises from module scope
4
+ if it is missing or broken, so the CLI would crash with a traceback before
5
+ main() could explain it.
6
+ """
7
+
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+
12
+ from plain2code_console import console
13
+
14
+ GIT_MISSING_MESSAGE = "git is not installed. Please install git and try again."
15
+ GIT_BROKEN_MESSAGE = "git is installed but not working. Please repair your git installation and try again."
16
+ MACOS_BROKEN_GIT_HINT = (
17
+ "This usually means the Command Line Tools are missing; install them with 'xcode-select --install'."
18
+ )
19
+
20
+
21
+ def git_runs(git_path):
22
+ """Whether `git version` succeeds. A git on PATH can still be unusable: on
23
+ macOS without the Command Line Tools /usr/bin/git is a stub that exits with
24
+ an xcrun error, and GIT_PYTHON_REFRESH=quiet does not suppress that."""
25
+ try:
26
+ return subprocess.run([git_path, "version"], capture_output=True, timeout=10).returncode == 0
27
+ except (OSError, subprocess.SubprocessError):
28
+ return False
29
+
30
+
31
+ def require_git():
32
+ """Exit with a clean message unless a working git is available."""
33
+ git_path = shutil.which("git")
34
+ if git_path is None:
35
+ console.error(f"{GIT_MISSING_MESSAGE}\n")
36
+ sys.exit(1)
37
+
38
+ if not git_runs(git_path):
39
+ hint = f"\n{MACOS_BROKEN_GIT_HINT}" if sys.platform == "darwin" else ""
40
+ console.error(f"{GIT_BROKEN_MESSAGE}{hint}\n")
41
+ sys.exit(1)
plain2code.py CHANGED
@@ -61,6 +61,11 @@ from tui.plain_module_render_choice_tui import PlainModuleRenderChoiceTUI
61
61
  DEFAULT_TEMPLATE_DIRS = "standard_template_library"
62
62
  RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7
63
63
 
64
+ # The terminal states a render can reach, as the API names them.
65
+ RENDER_OUTCOME_COMPLETED = "completed"
66
+ RENDER_OUTCOME_FAILED = "failed"
67
+ RENDER_OUTCOME_CANCELLED = "cancelled"
68
+
64
69
  # Exceptions that represent expected, user-facing error conditions. They are
65
70
  # reported to the user directly and must never be sent to Sentry as crashes.
66
71
  EXPECTED_EXCEPTIONS = (
@@ -177,6 +182,52 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]
177
182
  return response.get("user_email")
178
183
 
179
184
 
185
+ def _render_outcome(run_state: RunState, interrupted: bool = False) -> str:
186
+ """Name the terminal state a render reached.
187
+
188
+ Ctrl-C is the user stopping the render, so it is a cancellation. It does not
189
+ set render_cancelled: the exit summary reads that flag, and its wording for
190
+ an interrupt stays as it is.
191
+
192
+ Cancellation is tested first. A cancelled render leaves render_succeeded
193
+ False, so asking about success first would report it as a failure.
194
+ """
195
+ if interrupted or run_state.render_cancelled:
196
+ return RENDER_OUTCOME_CANCELLED
197
+ if run_state.render_succeeded:
198
+ return RENDER_OUTCOME_COMPLETED
199
+ return RENDER_OUTCOME_FAILED
200
+
201
+
202
+ def report_render_finished(
203
+ codeplainAPI: codeplain_api.CodeplainAPI, run_state: RunState, interrupted: bool = False
204
+ ) -> None:
205
+ """Tell the API which terminal state this render reached.
206
+
207
+ The API cannot work it out for itself - the state machine lives here - and
208
+ main()'s finally is the one place that sees every ending: success, failure,
209
+ cancellation, an unexpected crash and a keyboard interrupt alike. The state
210
+ machine's own completed and failed hooks see only two of the five.
211
+
212
+ Reported only once the connection check has set a user email, which is
213
+ exactly the condition "the API knows who we are". A missing key, an invalid
214
+ key and an outdated client all leave it unset, and none of them got as far
215
+ as a render.
216
+
217
+ Never raises. A report must not change the exit code, and must not mask the
218
+ error that ended the render.
219
+ """
220
+ if not run_state.user_email:
221
+ return
222
+
223
+ try:
224
+ codeplainAPI.render_finished(_render_outcome(run_state, interrupted), run_state)
225
+ except Exception:
226
+ # Deliberately silent: logging has stopped by now, and a failed report
227
+ # is not the user's problem.
228
+ pass
229
+
230
+
180
231
  def warn_if_acceptance_tests_without_conformance_script(plain_module, args) -> None:
181
232
  """Warn when any loaded module (including required modules) defines acceptance tests
182
233
  but no conformance tests script is configured.
@@ -214,6 +265,7 @@ def validate_linked_resources(plain_module) -> None:
214
265
 
215
266
  def render( # noqa: C901
216
267
  plain_module: plain_modules.PlainModule,
268
+ codeplainAPI: codeplain_api.CodeplainAPI,
217
269
  args,
218
270
  run_state: RunState,
219
271
  event_bus: EventBus,
@@ -224,10 +276,6 @@ def render( # noqa: C901
224
276
  if args.render_range or args.render_from:
225
277
  render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
226
278
 
227
- codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
228
- assert args.api is not None and args.api != "", "API URL is required"
229
- codeplainAPI.api_url = args.api
230
-
231
279
  run_state.user_email = _check_connection(codeplainAPI)
232
280
 
233
281
  stop_event = threading.Event()
@@ -419,6 +467,10 @@ def main(): # noqa: C901
419
467
  if not args.api:
420
468
  args.api = system_config.default_api_url
421
469
 
470
+ assert args.api is not None and args.api != "", "API URL is required"
471
+ codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
472
+ codeplainAPI.api_url = args.api
473
+
422
474
  run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
423
475
 
424
476
  if args.headless:
@@ -431,6 +483,7 @@ def main(): # noqa: C901
431
483
 
432
484
  exc_info = None
433
485
  error_message = None
486
+ interrupted = False
434
487
 
435
488
  try:
436
489
  # Validate API key is present
@@ -438,10 +491,11 @@ def main(): # noqa: C901
438
491
  raise MissingAPIKey(
439
492
  "Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
440
493
  )
441
- render(plain_module, args, run_state, event_bus, default_log_level)
494
+ render(plain_module, codeplainAPI, args, run_state, event_bus, default_log_level)
442
495
  except BaseException as e:
443
496
  if isinstance(e, KeyboardInterrupt):
444
497
  error_message = "Keyboard interrupt"
498
+ interrupted = True
445
499
  else:
446
500
  error_message = str(e) if str(e) else repr(e)
447
501
 
@@ -460,6 +514,8 @@ def main(): # noqa: C901
460
514
  # Remove any scratch extractions created for archive-only ("<module>.module") modules.
461
515
  for module in plain_module.all_required_modules + [plain_module]:
462
516
  module.cleanup_scratch()
517
+ # Last, so nothing the user is waiting for sits behind a network call.
518
+ report_render_finished(codeplainAPI, run_state, interrupted)
463
519
 
464
520
  if args.headless and (exc_info is not None or not run_state.render_succeeded):
465
521
  sys.exit(1)
plain2code_console.py CHANGED
@@ -27,7 +27,7 @@ class Plain2CodeConsole(Console):
27
27
  DEBUG_STYLE = Style(color="purple")
28
28
 
29
29
  def __init__(self):
30
- super().__init__()
30
+ super().__init__(emoji=False)
31
31
  try:
32
32
  import tiktoken
33
33
 
@@ -66,9 +66,10 @@ class Plain2CodeConsole(Console):
66
66
  """
67
67
  logger.log(level, " ".join(map(str, args)), extra={"log_color": color})
68
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, and don't
71
- # let the repr highlighter restyle brackets and numbers inside them.
69
+ # Messages must render exactly as logged and not pick its own styling:
70
+ # -- no Rich markup (via square brackets) in interpolated content (error texts, file names)
71
+ # -- no repr highlighter restyling brackets and numbers inside them.
72
+ # -- no emoji substitution, but it's set False in __init__, otherwise it won't reach renderables like Trees
72
73
  kwargs.setdefault("markup", False)
73
74
  kwargs.setdefault("highlight", False)
74
75
  super().print(*args, **kwargs, style=style)
plain_file.py CHANGED
@@ -90,7 +90,7 @@ def remove_quotes(token):
90
90
  token.children = tuple(new_children)
91
91
 
92
92
 
93
- def check_section_for_linked_resources(section):
93
+ def check_section_for_linked_resources(section, template_dirs):
94
94
  linked_resources = []
95
95
  for link in traverse(section, klass=Link):
96
96
  parsed_url = urlparse(link.node.target)
@@ -99,11 +99,19 @@ def check_section_for_linked_resources(section):
99
99
  f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
100
100
  )
101
101
 
102
- if not os.path.exists(link.node.target):
103
- raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} does not exist.")
102
+ resolved_target = file_utils.resolve_linked_resource(template_dirs, link.node.target)
103
+ if resolved_target is None:
104
+ searched_dirs = "\n".join(f" {position}. {dir}" for position, dir in enumerate(template_dirs, start=1))
105
+ raise PlainSyntaxError(
106
+ f"Plain syntax error: Link {link.node.target} does not exist. "
107
+ f"Linked resources are looked up relative to the following directories "
108
+ f"(highest to lowest precedence):\n{searched_dirs}"
109
+ )
104
110
 
105
- if not os.path.isfile(link.node.target):
106
- raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} must be a file.")
111
+ if not os.path.isfile(resolved_target):
112
+ raise PlainSyntaxError(
113
+ f"Plain syntax error: Link {link.node.target} must be a file (resolved to {resolved_target})."
114
+ )
107
115
 
108
116
  if len(link.node.children) != 1:
109
117
  raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).")
@@ -122,15 +130,15 @@ def check_section_for_linked_resources(section):
122
130
  section.linked_resources = linked_resources
123
131
 
124
132
 
125
- def check_for_linked_resources(plain_source):
133
+ def check_for_linked_resources(plain_source, template_dirs):
126
134
  for specification_heading in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
127
135
  if specification_heading in plain_source and hasattr(plain_source[specification_heading], "children"):
128
136
  for requirement in plain_source[specification_heading].children:
129
- check_section_for_linked_resources(requirement)
137
+ check_section_for_linked_resources(requirement, template_dirs)
130
138
 
131
139
  if hasattr(requirement, plain_spec.ACCEPTANCE_TESTS):
132
140
  for acceptance_test in requirement.acceptance_tests:
133
- check_section_for_linked_resources(acceptance_test)
141
+ check_section_for_linked_resources(acceptance_test, template_dirs)
134
142
 
135
143
 
136
144
  def process_section_code_variables(section, code_variables):
@@ -615,15 +623,45 @@ def parse_plain_source( # noqa: C901
615
623
  f"Plain syntax error: Syntax error at line {nested_acceptance_test_heading.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
616
624
  )
617
625
 
626
+ defined_concepts = set[str]()
618
627
  if plain_source[plain_spec.DEFINITIONS] is not None:
619
628
  with PlainRenderer() as renderer:
620
629
  for token in plain_source[plain_spec.DEFINITIONS].children:
621
630
  rendered_token = renderer.render(token)
622
631
  new_concepts, _ = concept_utils.extract_concepts_from_definition(rendered_token)
632
+ defined_concepts.update(new_concepts)
623
633
  for concept in new_concepts:
624
634
  if concept in required_concepts:
625
635
  required_concepts.remove(concept)
626
636
 
637
+ if EXPORTED_CONCEPTS_DIRECTIVE in plain_source_obj.metadata:
638
+ module_name = modules_trace[-1] if modules_trace else None
639
+ subject = f"Module '{module_name}'" if module_name else "Module"
640
+
641
+ for entry in plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]:
642
+ exported_concepts, errors = concept_utils.extract_concepts_from_definition(entry)
643
+ if errors:
644
+ raise PlainSyntaxError(
645
+ f"Plain syntax error: {subject} exports an invalid concept (`{entry}`). "
646
+ f"Expected a concept token, e.g. :ConceptName:."
647
+ )
648
+
649
+ for exported_concept in exported_concepts:
650
+ if exported_concept in concept_utils.DEFAULT_CONCEPTS:
651
+ raise PlainSyntaxError(
652
+ f"Plain syntax error: {subject} cannot export default concept {exported_concept}. "
653
+ f"Only user-defined concepts can be exported."
654
+ )
655
+
656
+ if exported_concept not in defined_concepts:
657
+ message = f"Plain syntax error: {subject} exports undefined concept {exported_concept}."
658
+ if REQUIRES_DIRECTIVE in plain_source_obj.metadata:
659
+ message += (
660
+ " An exported concept must be defined in this module's definitions or in an "
661
+ "imported module; a concept inherited through requires cannot be re-exported."
662
+ )
663
+ raise PlainSyntaxError(message)
664
+
627
665
  required_modules = []
628
666
  if REQUIRES_DIRECTIVE in plain_source_obj.metadata:
629
667
  required_modules = plain_source_obj.metadata[REQUIRES_DIRECTIVE]
@@ -710,15 +748,7 @@ def process_required_modules(
710
748
  if EXPORTED_CONCEPTS_DIRECTIVE in plain_file_parse_result.plain_source_obj.metadata:
711
749
  exported_concepts = list[str]()
712
750
  for concept in plain_file_parse_result.plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]:
713
- if concept in concept_utils.DEFAULT_CONCEPTS:
714
- raise PlainSyntaxError(
715
- f"Plain syntax error: Default concept cannot be exported: {concept}. Only user-defined concepts can be exported."
716
- )
717
-
718
- if isinstance(concept, str):
719
- exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0])
720
- else:
721
- raise PlainSyntaxError(f"Plain syntax error: Invalid exported concept: {concept}.")
751
+ exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0])
722
752
 
723
753
  with PlainRenderer() as renderer:
724
754
  for exported_concept in exported_concepts:
@@ -737,6 +767,9 @@ def process_exported_definitions(plain_source: dict, exported_definitions: list[
737
767
  if len(exported_definitions) == 0:
738
768
  return
739
769
 
770
+ if plain_source[plain_spec.DEFINITIONS] is None:
771
+ plain_source[plain_spec.DEFINITIONS] = mistletoe.Document("")
772
+
740
773
  with PlainRenderer() as renderer:
741
774
  for exported_definition in exported_definitions:
742
775
  add_defintion = True
@@ -820,7 +853,7 @@ def plain_file_parser( # noqa: C901
820
853
 
821
854
  process_acceptance_tests(plain_file_parse_result.plain_source)
822
855
 
823
- check_for_linked_resources(plain_file_parse_result.plain_source)
856
+ check_for_linked_resources(plain_file_parse_result.plain_source, template_dirs)
824
857
 
825
858
  marshalled_plain_source = marshall_plain_source(plain_file_parse_result.plain_source)
826
859