codeplain 0.2.3__py3-none-any.whl → 0.2.5__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 (43) hide show
  1. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/METADATA +3 -3
  2. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/RECORD +43 -43
  3. codeplain_REST_api.py +57 -43
  4. config/system_config.yaml +1 -16
  5. file_utils.py +5 -4
  6. git_utils.py +43 -13
  7. memory_management.py +1 -1
  8. module_renderer.py +114 -0
  9. plain2code.py +91 -30
  10. plain2code_console.py +3 -5
  11. plain2code_exceptions.py +26 -6
  12. plain2code_logger.py +11 -5
  13. plain2code_utils.py +7 -5
  14. plain_file.py +15 -37
  15. plain_modules.py +1 -4
  16. plain_spec.py +24 -6
  17. render_machine/actions/create_dist.py +1 -1
  18. render_machine/actions/exit_with_error.py +1 -1
  19. render_machine/actions/prepare_testing_environment.py +1 -1
  20. render_machine/actions/render_conformance_tests.py +2 -4
  21. render_machine/actions/render_functional_requirement.py +6 -6
  22. render_machine/actions/run_conformance_tests.py +3 -2
  23. render_machine/actions/run_unit_tests.py +1 -1
  24. render_machine/render_context.py +3 -3
  25. render_machine/render_utils.py +14 -6
  26. standard_template_library/golang-console-app-template.plain +2 -2
  27. standard_template_library/python-console-app-template.plain +2 -2
  28. standard_template_library/typescript-react-app-template.plain +2 -2
  29. system_config.py +3 -11
  30. tests/test_imports.py +2 -2
  31. tests/test_plainfile.py +2 -2
  32. tests/test_plainfileparser.py +10 -10
  33. tests/test_plainspec.py +2 -2
  34. tests/test_requires.py +2 -1
  35. tui/components.py +311 -103
  36. tui/plain2code_tui.py +101 -52
  37. tui/state_handlers.py +94 -47
  38. tui/styles.css +240 -52
  39. tui/widget_helpers.py +43 -47
  40. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/WHEEL +0 -0
  41. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/entry_points.txt +0 -0
  42. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/licenses/LICENSE +0 -0
  43. /spinner.py → /tui/spinner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -14,13 +14,13 @@ Requires-Dist: networkx==3.6.1
14
14
  Requires-Dist: python-frontmatter==1.1.0
15
15
  Requires-Dist: python-liquid2==0.3.0
16
16
  Requires-Dist: pyyaml==6.0.2
17
- Requires-Dist: requests==2.32.3
17
+ Requires-Dist: requests==2.32.4
18
18
  Requires-Dist: rich==14.2.0
19
19
  Requires-Dist: textual==1.0.0
20
20
  Requires-Dist: tiktoken==0.12.0
21
21
  Requires-Dist: transitions==0.9.3
22
22
  Provides-Extra: dev
23
- Requires-Dist: black==24.2.0; extra == 'dev'
23
+ Requires-Dist: black==24.3.0; extra == 'dev'
24
24
  Requires-Dist: flake8==7.0.0; extra == 'dev'
25
25
  Requires-Dist: isort==5.13.2; extra == 'dev'
26
26
  Requires-Dist: mypy==1.11.2; extra == 'dev'
@@ -1,29 +1,28 @@
1
- codeplain_REST_api.py,sha256=qfzUw9v0-NKII4ibw3vqSBrCeU2WP4RcrrEti43F3zs,18400
1
+ codeplain_REST_api.py,sha256=54OGXuuh4b6mHLgs9n902My4CaRiBk0OJmK3ZJU5wbI,18163
2
2
  concept_utils.py,sha256=vwSY4-FmxyqaDBxhntYzqG8t9pXvAWwiULP0DYQI32o,7905
3
3
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
4
4
  event_bus.py,sha256=sduIR3bgIbxAbLhwKd8Gx9YN9gzaeqy9-mNupS04aKw,1759
5
- file_utils.py,sha256=4BIxzsteZQOaK-efkvQcoaIfYydsQNFR6elpsxJgXLQ,11591
6
- git_utils.py,sha256=gTRps6RIzJJkyy9amaDxP38FPoxYulZViBWr9V0IPQg,12414
5
+ file_utils.py,sha256=AYs5JM-DidAMwG0cTsSeRGK4tsKl0mqv3OIPi_5cfZ0,11614
6
+ git_utils.py,sha256=JjUTE5TPYq4-b2I84X7L2zH0r3KCq1Xcg_hZo_PnDtI,13660
7
7
  hash_key.py,sha256=lMKgKpOhPeC4UpFf9e7eCNK20FEaDsXTCJ8j3eCWidE,837
8
- memory_management.py,sha256=ABrM6PhOsalP_PBq2hCazQvQX9C5rpPHhgT35l68RM4,4357
9
- module_renderer.py,sha256=LLjLb34phSJlF2YblwGDy5HKWVdi9mo57rr7s-Hj3RA,6261
10
- plain2code.py,sha256=cIxPkxfxZkPlen5RG5H_rvo22sSGYQ9PnqSlw3ECVkY,9841
8
+ memory_management.py,sha256=rcl0C_vLKaCNzv1_LuMmHPzkJZpOSinHGEg13j2ceZc,4358
9
+ module_renderer.py,sha256=7Imv8RXOTVbEDnlVELwFYzfMx44vEb4HKsIRDjf3jFI,11678
10
+ plain2code.py,sha256=bhtzCHED6xYcue-_WuX46FWzjAoDDw0c_Eximrp1dFM,12306
11
11
  plain2code_arguments.py,sha256=PdQjd3orklrtlEyz7-Qdz_m2SvjZtZp6dmbkqXgglSU,11826
12
- plain2code_console.py,sha256=-QorOFimS1AWYK7dmFqI3uhjc51cPtEWsaK4DhFibIw,4139
12
+ plain2code_console.py,sha256=HNL7XVwRE9kCjsiVB1BVgckgqv5YevbHc4Pl3hDJAX4,4056
13
13
  plain2code_events.py,sha256=5fPhpRzgJ02xWh0MXqrMv9A4curr2UOE68Y1uKlAcqQ,1347
14
- plain2code_exceptions.py,sha256=Y_2Jmv9rLE5xMqlzp_mDcAIIYsIfS0yR1U3scXUzUng,760
15
- plain2code_logger.py,sha256=xSDGN8QAWpA4J6YKyj-kfNWSO-jBl0sfUCFRs7Tflf0,4067
14
+ plain2code_exceptions.py,sha256=Vuv6amo9lgc4hy2pNxhOBUuxhP7BkjGAaovj0xVu7s8,1135
15
+ plain2code_logger.py,sha256=I9AchbS34QY_8lvqUHe_TSJLfRZqfdi9rtV_OpJ0dss,4416
16
16
  plain2code_nodes.py,sha256=Om624mfb5MB1Z09c0Zqtb9G1gGkUY9x-v2hzAf3A2gw,3818
17
17
  plain2code_read_config.py,sha256=QnpbW_Bi9LzDIOubM5x7fVXH1HgiFL3-5Oa_O1EUoE4,2449
18
18
  plain2code_state.py,sha256=InLXdytMXnm9YC4o8MSTbtB1TmqYYUs6rASGBlyA_YY,1274
19
- plain2code_utils.py,sha256=3xPXWci5yVdpdK_WrNcbinSV94XSeMQDW5UzDuBMaUM,1767
20
- plain_file.py,sha256=OXbWvErVTVhaeRFUu9f0bzi16P01DShW_jLhNI-tmeA,28931
21
- plain_modules.py,sha256=iDqqamtix5KahMC_v-vfQ7yndugmqtBW1z6XxTB4x6w,4876
22
- plain_spec.py,sha256=zC-VOb_UJOs8OxtEiwQJuonw7Lkmbi7YHyFvvCvUZNo,13529
23
- spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
24
- system_config.py,sha256=mgHLn-CRHLO9Y9vKyI_eFBreY_YhFad-ctZgBYp-rIg,1777
19
+ plain2code_utils.py,sha256=CubIc4S_cyz92OZJkiUSTfLejesNO3fqVrflOLPI4qM,1772
20
+ plain_file.py,sha256=UxO8_DC_eqpn_joxdbJfF7wS5T1Kdd1JFYyI908vhsM,28434
21
+ plain_modules.py,sha256=6gteJeiK-WvUZDrIt_kJK0MFiptq63rEVpYvV5yYFXc,4881
22
+ plain_spec.py,sha256=WY2cStFdwRLL8itlOwEOfX5ixFsFsbSKi_iqb9kgDfc,14078
23
+ system_config.py,sha256=_m75WJEo8_kG6Q8keiLp0dGW0fmE_X8_4aoVhg58j6k,1460
25
24
  config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
26
- config/system_config.yaml,sha256=bB5Th5jxgXFyaIvceUPID1ReebMMXsyMibV4gtu9sWQ,1148
25
+ config/system_config.yaml,sha256=LLxWyAP7oOzajc6mMue-UaRO7qjur0TpzAv2rLOix7I,534
27
26
  docs/generate_cli.py,sha256=xJmiihtdgqEDBYt_wBsRbniZvIfq7Q6tjAsi3SjoMJg,531
28
27
  examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
29
28
  render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -31,9 +30,9 @@ render_machine/code_renderer.py,sha256=jOoYivkZzCuGG947YlNPlvD1Dpd2xTWVK8pwfF_Oq
31
30
  render_machine/conformance_test_helpers.py,sha256=6Ru7Dh24SLNIKWfz_sP5hE_EmWpvk6nfgN1T1yaCZus,2887
32
31
  render_machine/conformance_tests.py,sha256=M0dMCawkUljMLwnf-9BLzjyKKJjfxmxJ4wUFyJX0hHU,7155
33
32
  render_machine/implementation_code_helpers.py,sha256=KctilT0x07niph0IWgKwP7ACXw8KJyErhJsl1iC3LQg,960
34
- render_machine/render_context.py,sha256=9rvjpp_sm7D5CWvSInWVNJ44k4GDZLegfWFI55vNMcg,21519
33
+ render_machine/render_context.py,sha256=LMgIN3pVQs1aSccyU6T3fXi-ANAT-K51r3t52UmO2iM,21498
35
34
  render_machine/render_types.py,sha256=Y0MTzlEEPyqEF3NH-SGe5tL5TeCjYOCWhQ1aqBZdy-Q,7187
36
- render_machine/render_utils.py,sha256=nUO6mT2dqMmP4t7vefvZRAvxrRJPyrtQBA5qDd_rVI8,4485
35
+ render_machine/render_utils.py,sha256=j6UAEJnBP_khpub-ix98Xiq69i0X6PLqaDVihfdgjaQ,4999
37
36
  render_machine/state_machine_config.py,sha256=sd-P9kdvBYlj0R6aT1RZLFquIwAcbEBNL3aZG-Wo4hw,27988
38
37
  render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
39
38
  render_machine/triggers.py,sha256=GarwIYPY6ZZzVdRF7yXLB54tofr08NpFZfp5DH6AOuI,1331
@@ -41,41 +40,42 @@ render_machine/actions/analyze_specification_ambiguity.py,sha256=okEmM03WVGnDR4c
41
40
  render_machine/actions/base_action.py,sha256=vVzMp-LanrZyq-jamHyWaQxpRl-pLqtTdtiEzUBhfyE,534
42
41
  render_machine/actions/commit_conformance_tests_changes.py,sha256=08yxCPCpf5TlrzLPsEh5SJXVVvnLfwdNKSr7BlzlYxo,2589
43
42
  render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgWcmFbynes-BK2IGnZwA7wygrTJGoL8dY,826
44
- render_machine/actions/create_dist.py,sha256=ZyhCgl_UHEPjGLLEdqV_2geG7yh_y6G7pYBrlpH99_Q,1046
45
- render_machine/actions/exit_with_error.py,sha256=IgTQQbhpluKzTcc1SHYMq8XjNgkx3sEF8Rf8877uK8Q,1044
43
+ render_machine/actions/create_dist.py,sha256=du1Ur5rmES6OEg_ZXjJEDptEuN_FYFhRCFoTrNDeALM,1065
44
+ render_machine/actions/exit_with_error.py,sha256=RUslIaZC6y_1Gx65QKd7CxwnOMo7xUkH1mKeuQE1X2M,1037
46
45
  render_machine/actions/finish_functional_requirement.py,sha256=g279oDxfz7BoRxoJT6Gsv5reawed1lfLZw_7fn0mZ0g,687
47
46
  render_machine/actions/fix_conformance_test.py,sha256=YsKPDlheGtBmzh0ac9ugsI8JZQYAZW5CHevZSsatlKI,7326
48
47
  render_machine/actions/fix_unit_tests.py,sha256=wrj5c8flhln9wAZUQ-hELI3dRbLNl1HZqImNDcLE-OE,2558
49
48
  render_machine/actions/prepare_repositories.py,sha256=1jLGwbJS7jJkhjm-1p2oNv_9n1pD105S1gCCnMNCyrg,3160
50
- render_machine/actions/prepare_testing_environment.py,sha256=u5Qdk8EOMewtVNQ4WbscrrnEBDGpDdEK3WG_TlpfI8s,1851
49
+ render_machine/actions/prepare_testing_environment.py,sha256=VN4CSFTM6zolO7p6pM4afgGY0Rocka6JWSTQLevxLlI,1844
51
50
  render_machine/actions/refactor_code.py,sha256=2cZViWb4Dj6t7dCN2fo9qniIIrn_VE22dsSORjmr_oA,2112
52
- render_machine/actions/render_conformance_tests.py,sha256=UwxFJHGjv_xnM97n2haiqXz2h7EEcNqZ71ZLr_0j4v4,8873
53
- render_machine/actions/render_functional_requirement.py,sha256=O3FOYwK6LUbV2HAzgy3xSh9wkTVurdBVxcfpIIBFT1w,4153
54
- render_machine/actions/run_conformance_tests.py,sha256=ICOzo2lgJm57cnT8AWGMCuay2s5d7O4Olex0OciX--E,3414
55
- render_machine/actions/run_unit_tests.py,sha256=kJOmKhNCLY1latm60DBHvxCKl5-GXdlYUmloZcB1pRY,2031
51
+ render_machine/actions/render_conformance_tests.py,sha256=gQaYImSAD6fK8FhfDMQcKH1Uy9_Qq6jsj5ZEKHLmy9c,8798
52
+ render_machine/actions/render_functional_requirement.py,sha256=lk9SquVSH_EwvtXPqyEfoirGwisQf5eHdBdV-NG2eoY,4120
53
+ render_machine/actions/run_conformance_tests.py,sha256=ttc1-1jQUBhCGw6mxoMLkpAbVd3w2tlERMnWQcHpPbA,3488
54
+ render_machine/actions/run_unit_tests.py,sha256=68huFbL_RExMGw5XaTDKwu18uQyEgo_fxREfO5aYyBI,2024
56
55
  render_machine/actions/summarize_conformance_tests.py,sha256=BbBMJX3HkrZF_BlyNFw6prPgF9NLySapoLeGQ6_e3xU,1867
57
56
  standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
58
- standard_template_library/golang-console-app-template.plain,sha256=M3gE1wZ6o_F-HGtPOgUkVt3yKCGXVWsNZEFcVyYBfbg,946
59
- standard_template_library/python-console-app-template.plain,sha256=ofIJIkNXScGFTx2w8mY3lP4KmRml2GQv2EiEoXlbqqs,1005
57
+ standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
58
+ standard_template_library/python-console-app-template.plain,sha256=I5Zd28GInw3vLa9Ub9qnQjjiCuslsycguNcsBEa19Wc,1008
60
59
  standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOzWnDHdVpFj-XBTq6s4_2dy7LKV3lVkCzv8i4,114
61
- standard_template_library/typescript-react-app-template.plain,sha256=SG1cBMVt1ncMRK4cOGOyVVj0MmsQTmF4MI67IBn1lFg,380
60
+ standard_template_library/typescript-react-app-template.plain,sha256=Rh8THR6ADSBZvw4ZF2dlMuaKU7xYTNaQ688R8T61Y44,383
62
61
  tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
63
62
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
64
63
  tests/test_git_utils.py,sha256=Knc4R2ZkHYVj9iPFKhb0_ewaad_mxPrakOn6gpziHMc,13138
65
- tests/test_imports.py,sha256=cdw7u93xNFD064sjwwEWy4oLHhJNlt1-DRDYhHjymFw,2078
66
- tests/test_plainfile.py,sha256=N3f59hpxOO5zULdBtmXZM6KO2PKADoOL5d3WxNTUYXg,11106
67
- tests/test_plainfileparser.py,sha256=9B9r2jEeUKDMhqb4TJEPK7aatYl1op1z92GDAD4fJ20,18685
68
- tests/test_plainspec.py,sha256=ouS899oejStu8u1D3399y5NAxcKlNbLcinNoxZUp59M,2437
69
- tests/test_requires.py,sha256=u8wY-UI7Ryat84htaI0yFXpl-PjX7G90EnMKM35S6No,1182
64
+ tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
65
+ tests/test_plainfile.py,sha256=H0asjkBjdqAgwHHZJTaGYm0BG97ah6yirLk-yaHBzwM,11090
66
+ tests/test_plainfileparser.py,sha256=Pq52n5NzJP_h56BRk7OofRwry0aAW1Al86U3VEkte9U,18724
67
+ tests/test_plainspec.py,sha256=kfTyMlZ8eiIoIGqvEN6iwqvFLckx14S_zZRUuU1T2ko,2440
68
+ tests/test_requires.py,sha256=BwlZeLXIs4m-FGnjLjO4vHkxfEX_QgjYoomb3oIu7Vs,1236
70
69
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
71
- tui/components.py,sha256=l7f-jLk0sj9WM8z2IpaHksmSyytXe_bl2GBdHASoIg4,13901
70
+ tui/components.py,sha256=A-Z4NrNl5B8ZmeoeKm7jMTFU18B8vP7Roa-Q6r84CcE,21797
72
71
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
73
- tui/plain2code_tui.py,sha256=7agVU0NnaCuF1LuOSEmjx-XfEOQ6HHr1HlWe_b1Tses,11695
74
- tui/state_handlers.py,sha256=HbjgaV-9xGhp3E-3X114zOqPkeNcCjT-R1PbVRxVdso,12674
75
- tui/styles.css,sha256=Umm2TLePmywizZGV4Nd8UezZRiK5pFyibYRbpRvGqbs,3056
76
- tui/widget_helpers.py,sha256=VJorEM2PjRBzN-jIDmKJPolFgo2d8-2NmTumgC5xeNo,5229
77
- codeplain-0.2.3.dist-info/METADATA,sha256=8GLBTQ7B-NEivsGSRPlOqsTvnFnvkAsF6qyjAAQ0yRc,4278
78
- codeplain-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
79
- codeplain-0.2.3.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
80
- codeplain-0.2.3.dist-info/licenses/LICENSE,sha256=wsFi5dpbJurnRNfBj8q2RCcF3ryrmdRIfxc3lPcmc4c,1069
81
- codeplain-0.2.3.dist-info/RECORD,,
72
+ tui/plain2code_tui.py,sha256=mj6fzksBYCqqltXL6z2VXh-u7Y6NMOmp9lsnNhHEbGc,14318
73
+ tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
74
+ tui/state_handlers.py,sha256=rjufJdDCS8IYy_0PmA_eNBdHe4iHAjUunaAM_mnJJY8,15155
75
+ tui/styles.css,sha256=BN5A1dm3AZi9Tjyry6_B2rctcTeP5YTP9ltqUuFUSWM,6083
76
+ tui/widget_helpers.py,sha256=E-ZkBx6z6x_d6SMCcWc7I63jI_1x90YwI3v7uOfVsY8,5248
77
+ codeplain-0.2.5.dist-info/METADATA,sha256=Sh1o1WSwyRAQ9RyJpVKL9MYJqrPdMyxPCfxXiW9RClQ,4278
78
+ codeplain-0.2.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
79
+ codeplain-0.2.5.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
80
+ codeplain-0.2.5.dist-info/licenses/LICENSE,sha256=wsFi5dpbJurnRNfBj8q2RCcF3ryrmdRIfxc3lPcmc4c,1069
81
+ codeplain-0.2.5.dist-info/RECORD,,
codeplain_REST_api.py CHANGED
@@ -2,7 +2,6 @@ import time
2
2
  from typing import Optional
3
3
 
4
4
  import requests
5
- from requests.exceptions import ConnectionError, RequestException, Timeout
6
5
 
7
6
  import plain2code_exceptions
8
7
  from plain2code_state import RunState
@@ -10,6 +9,22 @@ from plain2code_state import RunState
10
9
  MAX_RETRIES = 4
11
10
  RETRY_DELAY = 3
12
11
 
12
+ # TODO: Handle connection errors
13
+ RETRY_ERROR_CODES = [
14
+ "LLMInternalError",
15
+ ]
16
+
17
+ # Mapping from API error codes to exception classes
18
+ ERROR_CODE_EXCEPTIONS = {
19
+ "FunctionalRequirementTooComplex": plain2code_exceptions.FunctionalRequirementTooComplex,
20
+ "ConflictingRequirements": plain2code_exceptions.ConflictingRequirements,
21
+ "CreditBalanceTooLow": plain2code_exceptions.CreditBalanceTooLow,
22
+ "LLMInternalError": plain2code_exceptions.LLMInternalError,
23
+ "MissingResource": plain2code_exceptions.MissingResource,
24
+ "PlainSyntaxError": plain2code_exceptions.PlainSyntaxError,
25
+ "InternalServerError": plain2code_exceptions.InternalServerError,
26
+ }
27
+
13
28
 
14
29
  class CodeplainAPI:
15
30
 
@@ -29,12 +44,31 @@ class CodeplainAPI:
29
44
  run_state.increment_call_count()
30
45
  payload["render_state"] = run_state.to_dict()
31
46
 
32
- def post_request(self, endpoint_url, headers, payload, run_state: Optional[RunState]): # noqa: C901
47
+ def _raise_for_error_code(self, response_json):
48
+ """Raise appropriate exception based on error code in response."""
49
+ error_code = response_json.get("error_code")
50
+ if error_code not in ERROR_CODE_EXCEPTIONS:
51
+ return
52
+
53
+ exception_class = ERROR_CODE_EXCEPTIONS[error_code]
54
+ message = response_json.get("message", "")
55
+
56
+ # FunctionalRequirementTooComplex has an extra parameter
57
+ if error_code == "FunctionalRequirementTooComplex":
58
+ raise exception_class(message, response_json.get("proposed_breakdown"))
59
+
60
+ raise exception_class(message)
61
+
62
+ def post_request(
63
+ self, endpoint_url, headers, payload, run_state: Optional[RunState], num_retries: int = MAX_RETRIES
64
+ ):
33
65
  if run_state is not None:
34
66
  self._extend_payload_with_run_state(payload, run_state)
35
67
 
36
68
  retry_delay = RETRY_DELAY
37
- for attempt in range(MAX_RETRIES + 1):
69
+ response_json = None
70
+
71
+ for attempt in range(num_retries + 1):
38
72
  try:
39
73
  response = requests.post(endpoint_url, headers=headers, json=payload)
40
74
 
@@ -45,53 +79,33 @@ class CodeplainAPI:
45
79
  raise
46
80
 
47
81
  if response.status_code == requests.codes.bad_request and "error_code" in response_json:
48
- if response_json["error_code"] == "FunctionalRequirementTooComplex":
49
- raise plain2code_exceptions.FunctionalRequirementTooComplex(
50
- response_json["message"], response_json.get("proposed_breakdown")
51
- )
52
-
53
- if response_json["error_code"] == "ConflictingRequirements":
54
- raise plain2code_exceptions.ConflictingRequirements(response_json["message"])
55
-
56
- if response_json["error_code"] == "CreditBalanceTooLow":
57
- raise plain2code_exceptions.CreditBalanceTooLow(response_json["message"])
58
-
59
- if response_json["error_code"] == "LLMInternalError":
60
- raise plain2code_exceptions.LLMInternalError(response_json["message"])
61
-
62
- if response_json["error_code"] == "MissingResource":
63
- raise plain2code_exceptions.MissingResource(response_json["message"])
64
-
65
- if response_json["error_code"] == "PlainSyntaxError":
66
- raise plain2code_exceptions.PlainSyntaxError(response_json["message"])
67
-
68
- if response_json["error_code"] == "OnlyRelativeLinksAllowed":
69
- raise plain2code_exceptions.OnlyRelativeLinksAllowed(response_json["message"])
70
-
71
- if response_json["error_code"] == "LinkMustHaveTextSpecified":
72
- raise plain2code_exceptions.LinkMustHaveTextSpecified(response_json["message"])
73
-
74
- if response_json["error_code"] == "NoRenderFound":
75
- raise plain2code_exceptions.NoRenderFound(response_json["message"])
76
-
77
- if response_json["error_code"] == "MultipleRendersFound":
78
- raise plain2code_exceptions.MultipleRendersFound(response_json["message"])
82
+ self._raise_for_error_code(response_json)
79
83
 
80
84
  response.raise_for_status()
81
85
  return response_json
82
86
 
83
- except (ConnectionError, Timeout, RequestException) as e:
84
- if attempt < MAX_RETRIES:
85
- self.console.info(f"Connection error on attempt {attempt + 1}/{MAX_RETRIES + 1}: {e}")
87
+ except Exception as e:
88
+ if response_json is not None and "error_code" in response_json:
89
+ if response_json["error_code"] not in RETRY_ERROR_CODES:
90
+ raise e
91
+
92
+ if attempt < num_retries:
93
+ self.console.info(f"Error on attempt {attempt + 1}/{num_retries + 1}: {e}")
86
94
  self.console.info(f"Retrying in {retry_delay} seconds...")
87
95
  time.sleep(retry_delay)
88
- # Exponential backoff
89
- retry_delay *= 2
96
+ retry_delay *= 2 # Exponential backoff
90
97
  else:
91
- self.console.error(f"Max retries ({MAX_RETRIES}) exceeded. Last error: {e}")
92
- raise RequestException(
93
- f"Connection error: Unable to reach the Codeplain API at {self.api_url}. Please try again or contact support."
94
- )
98
+ self.console.error(f"Max retries ({num_retries}) exceeded. Last error: {e}")
99
+ raise e
100
+
101
+ def connection_check(self, client_version):
102
+ endpoint_url = f"{self.api_url}/connection_check"
103
+ headers = {"Content-Type": "application/json"}
104
+ payload = {
105
+ "api_key": self.api_key,
106
+ "client_version": client_version,
107
+ }
108
+ return self.post_request(endpoint_url, headers, payload, None, num_retries=0)
95
109
 
96
110
  def render_functional_requirement(
97
111
  self,
config/system_config.yaml CHANGED
@@ -1,19 +1,4 @@
1
- system_requirements:
2
- timeout:
3
- command: timeout
4
- error_message: |
5
- Error: Required system command 'timeout' is not available.
6
- This command is needed to enforce time limits on test execution.
7
-
8
- To install the timeout command:
9
- - On Linux: Install coreutils package
10
- - Debian/Ubuntu: sudo apt-get install coreutils
11
- - CentOS/RHEL: sudo yum install coreutils
12
- - Fedora: sudo dnf install coreutils
13
- - On macOS:
14
- - Using Homebrew: brew install coreutils
15
- - Using MacPorts: port install coreutils
16
- - On Windows: Use Windows Subsystem for Linux (WSL), Cygwin, or Git Bash
1
+ client_version: "0.17.0"
17
2
 
18
3
  error_messages:
19
4
  template_not_found:
file_utils.py CHANGED
@@ -43,6 +43,8 @@ FILE_EXTENSION_MAPPING = {
43
43
  ".bat": "Batch File",
44
44
  }
45
45
 
46
+ SYSTEM_FOLDERS = [".git", CODEPLAIN_METADATA_FOLDER, CODEPLAIN_MEMORY_SUBFOLDER]
47
+
46
48
 
47
49
  def get_file_type(file_name):
48
50
 
@@ -55,10 +57,9 @@ def get_file_type(file_name):
55
57
 
56
58
  def list_all_text_files(directory):
57
59
  all_files = []
58
- skip_dirs = [".git", CODEPLAIN_METADATA_FOLDER, CODEPLAIN_MEMORY_SUBFOLDER]
59
60
  for root, dirs, files in os.walk(directory, topdown=True):
60
61
  # Skip directories that should not be traversed
61
- for skip_dir in skip_dirs:
62
+ for skip_dir in SYSTEM_FOLDERS:
62
63
  if skip_dir in dirs:
63
64
  dirs.remove(skip_dir)
64
65
 
@@ -322,5 +323,5 @@ def copy_folder_to_output(source_folder, output_folder):
322
323
  if os.path.exists(output_folder):
323
324
  delete_files_and_subfolders(output_folder)
324
325
 
325
- # Copy source folder contents directly to output folder (excluding .git)
326
- copy_folder_content(source_folder, output_folder, ignore_folders=[".git"])
326
+ # Copy source folder contents directly to output folder (excluding SYSTEM_FOLDERS)
327
+ copy_folder_content(source_folder, output_folder, ignore_folders=SYSTEM_FOLDERS)
git_utils.py CHANGED
@@ -4,6 +4,7 @@ from typing import Optional, Union
4
4
  from git import Repo
5
5
 
6
6
  import file_utils
7
+ from plain2code_exceptions import InvalidGitRepositoryError
7
8
 
8
9
  FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE = (
9
10
  "[Codeplain] Implemented code and unit tests for functional requirement {}"
@@ -25,12 +26,6 @@ MODULE_NAME_MESSAGE = "Module name: {}"
25
26
  RENDER_ID_MESSAGE = "Render ID: {}"
26
27
 
27
28
 
28
- class InvalidGitRepositoryError(Exception):
29
- """Raised when the git repository is in an invalid state."""
30
-
31
- pass
32
-
33
-
34
29
  def _get_full_commit_message(message, module_name, frid, render_id) -> str:
35
30
  full_message = message
36
31
 
@@ -252,7 +247,10 @@ def diff(repo_path: Union[str, os.PathLike], previous_frid: str = None) -> dict:
252
247
 
253
248
  def _get_commit(repo: Repo, frid: Optional[str]) -> str:
254
249
  if frid:
255
- return _get_commit_with_frid(repo, frid)
250
+ commit_with_frid = _get_commit_with_frid(repo, frid)
251
+ if not commit_with_frid:
252
+ raise InvalidGitRepositoryError(f"No commit with frid {frid} found.")
253
+ return commit_with_frid
256
254
 
257
255
  base_folder_commit = _get_base_folder_commit(repo)
258
256
  initial_commit = _get_initial_commit(repo)
@@ -261,12 +259,44 @@ def _get_commit(repo: Repo, frid: Optional[str]) -> str:
261
259
  return initial_commit
262
260
 
263
261
 
264
- def _get_commit_with_frid(repo: Repo, frid: str) -> str:
265
- """Finds commit with given frid mentioned in the commit message."""
266
- commit = _get_commit_with_message(repo, FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(frid))
267
- if not commit:
268
- raise InvalidGitRepositoryError(f"No commit with frid {frid} found.")
269
- return commit
262
+ def _get_commit_with_frid(repo: Repo, frid: str, module_name: Optional[str] = None) -> str:
263
+ """
264
+ Finds commit with given frid mentioned in the commit message.
265
+
266
+ Args:
267
+ repo (Repo): Git repository object
268
+ frid (str): Functional requirement ID
269
+ module_name (Optional[str]): Module name to filter by. If provided, only returns
270
+ commits that have both the FRID and module name.
271
+
272
+ Returns:
273
+ str: Commit SHA if found, empty string otherwise
274
+ """
275
+ commit_message_pattern = FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(frid)
276
+
277
+ # If no module name filtering is needed, use the original logic
278
+ if not module_name:
279
+ return _get_commit_with_message(repo, commit_message_pattern)
280
+
281
+ # Use multiple grep patterns with --all-match for AND condition
282
+ escaped_frid_message = commit_message_pattern.replace("[", "\\[").replace("]", "\\]")
283
+ module_name_pattern = MODULE_NAME_MESSAGE.format(module_name)
284
+ escaped_module_message = module_name_pattern.replace("[", "\\[").replace("]", "\\]")
285
+
286
+ return repo.git.rev_list(
287
+ repo.active_branch.name,
288
+ "--grep",
289
+ escaped_frid_message,
290
+ "--grep",
291
+ escaped_module_message,
292
+ "--all-match",
293
+ "-n",
294
+ "1",
295
+ )
296
+
297
+
298
+ def has_commit_for_frid(repo_path: Union[str, os.PathLike], frid: str, module_name: Optional[str] = None) -> bool:
299
+ return bool(_get_commit_with_frid(Repo(repo_path), frid, module_name))
270
300
 
271
301
 
272
302
  def _get_base_folder_commit(repo: Repo) -> str:
memory_management.py CHANGED
@@ -51,7 +51,7 @@ class MemoryManager:
51
51
  code_diff_files = render_context.conformance_tests_running_context.code_diff_files
52
52
 
53
53
  if not should_create_memory or code_diff_files is None:
54
- console.info(
54
+ console.debug(
55
55
  "Skipping creation of conformance test memory because the conditions for creating memories are not met."
56
56
  )
57
57
  return
module_renderer.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import argparse
2
2
  import os
3
3
 
4
+ import git_utils
4
5
  import plain_file
5
6
  import plain_modules
6
7
  import plain_spec
@@ -8,6 +9,7 @@ from event_bus import EventBus
8
9
  from memory_management import MemoryManager
9
10
  from plain2code_console import console
10
11
  from plain2code_events import RenderCompleted, RenderFailed
12
+ from plain2code_exceptions import MissingPreviousFunctionalitiesError
11
13
  from plain2code_state import RunState
12
14
  from plain_modules import PlainModule
13
15
  from render_machine.code_renderer import CodeRenderer
@@ -35,6 +37,114 @@ class ModuleRenderer:
35
37
  self.run_state = run_state
36
38
  self.event_bus = event_bus
37
39
 
40
+ def _ensure_module_folders_exist(self, module_name: str, first_render_frid: str) -> tuple[str, str]:
41
+ """
42
+ Ensure that build and conformance test folders exist for the module.
43
+
44
+ Args:
45
+ module_name: Name of the module being rendered
46
+ first_render_frid: The first FRID in the render range
47
+
48
+ Returns:
49
+ tuple[str, str]: (build_folder_path, conformance_tests_path)
50
+
51
+ Raises:
52
+ MissingPreviousFridCommitsError: If any required folders are missing
53
+ """
54
+ build_folder_path = os.path.join(self.args.build_folder, module_name)
55
+ conformance_tests_path = os.path.join(self.args.conformance_tests_folder, module_name)
56
+
57
+ if not os.path.exists(build_folder_path):
58
+ raise MissingPreviousFunctionalitiesError(
59
+ f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the source code folder does not exist.\n\n"
60
+ f"To fix this, please render the module from the beginning by running:\n"
61
+ f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}"
62
+ )
63
+
64
+ if not os.path.exists(conformance_tests_path):
65
+ raise MissingPreviousFunctionalitiesError(
66
+ f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests folder does not exist.\n\n"
67
+ f"To fix this, please render the module from the beginning by running:\n"
68
+ f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}"
69
+ )
70
+
71
+ return build_folder_path, conformance_tests_path
72
+
73
+ def _ensure_frid_commit_exists(
74
+ self,
75
+ frid: str,
76
+ module_name: str,
77
+ build_folder_path: str,
78
+ conformance_tests_path: str,
79
+ first_render_frid: str,
80
+ ) -> None:
81
+ """
82
+ Ensure commit exists for a single FRID in both repositories.
83
+
84
+ Args:
85
+ frid: The FRID to check
86
+ module_name: Name of the module
87
+ build_folder_path: Path to the build folder
88
+ conformance_tests_path: Path to the conformance tests folder
89
+ first_render_frid: The first FRID in the render range (for error messages)
90
+
91
+ Raises:
92
+ MissingPreviousFridCommitsError: If the commit is missing
93
+ """
94
+ # Check in build folder
95
+ if not git_utils.has_commit_for_frid(build_folder_path, frid, module_name):
96
+ raise MissingPreviousFunctionalitiesError(
97
+ f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the implementation of the previous functionality ({frid}) hasn't been completed yet.\n\n"
98
+ f"To fix this, please render the missing functionality ({frid}) first by running:\n"
99
+ f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}"
100
+ )
101
+
102
+ # Check in conformance tests folder (only if conformance tests are enabled)
103
+ if self.args.render_conformance_tests:
104
+ if not git_utils.has_commit_for_frid(conformance_tests_path, frid, module_name):
105
+ raise MissingPreviousFunctionalitiesError(
106
+ f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests for the previous functionality ({frid}) haven't been completed yet.\n\n"
107
+ f"To fix this, please render the missing functionality ({frid}) first by running:\n"
108
+ f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}"
109
+ )
110
+
111
+ def _ensure_previous_frid_commits_exist(
112
+ self, module_name: str, plain_source: dict, render_range: list[str]
113
+ ) -> None:
114
+ """
115
+ Ensure that all FRID commits before the render_range exist.
116
+
117
+ This is a precondition check that must pass before rendering can proceed.
118
+ Raises an exception if any previous FRID commits are missing.
119
+
120
+ Args:
121
+ module_name: Name of the module being rendered
122
+ plain_source: The plain source tree
123
+ render_range: List of FRIDs to render
124
+
125
+ Raises:
126
+ MissingPreviousFridCommitsError: If any previous FRID commits are missing
127
+ """
128
+ first_render_frid = render_range[0]
129
+
130
+ # Get all FRIDs that should have been rendered before this one
131
+ previous_frids = plain_spec.get_frids_before(plain_source, first_render_frid)
132
+ if not previous_frids:
133
+ return
134
+
135
+ # Ensure the module folders exist
136
+ build_folder_path, conformance_tests_path = self._ensure_module_folders_exist(module_name, first_render_frid)
137
+
138
+ # Verify commits exist for all previous FRIDs
139
+ for prev_frid in previous_frids:
140
+ self._ensure_frid_commit_exists(
141
+ prev_frid,
142
+ module_name,
143
+ build_folder_path,
144
+ conformance_tests_path,
145
+ first_render_frid,
146
+ )
147
+
38
148
  def _build_render_context_for_module(
39
149
  self,
40
150
  module_name: str,
@@ -81,6 +191,10 @@ class ModuleRenderer:
81
191
  resources_list = []
82
192
  plain_spec.collect_linked_resources(plain_source, resources_list, None, True)
83
193
 
194
+ # Ensure that all previous FRID commits exist before proceeding with render_range
195
+ if render_range is not None:
196
+ self._ensure_previous_frid_commits_exist(module_name, plain_source, render_range)
197
+
84
198
  required_modules = []
85
199
  has_any_required_module_changed = False
86
200
  if not self.args.render_machine_graph and required_modules_list: