Add initial web editor app, CLI scripts, and test scaffolding.
This introduces the FastAPI editor implementation and related project setup so the app can be run and validated locally. Made-with: Cursor
This commit is contained in:
1
.connectionmachine_startup_script
Normal file
1
.connectionmachine_startup_script
Normal file
@@ -0,0 +1 @@
|
|||||||
|
code/demo_prompt_args.py
|
||||||
26
Pipfile
Normal file
26
Pipfile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
pytest = "*"
|
||||||
|
pytest-cov = "*"
|
||||||
|
httpx = "*"
|
||||||
|
pylint = "*"
|
||||||
|
jedi = "*"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
fastapi = "*"
|
||||||
|
uvicorn = "*"
|
||||||
|
websockets = "*"
|
||||||
|
pillow = "*"
|
||||||
|
startspimage = "*"
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.11"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
dev = "uvicorn app:app --app-dir src --reload --port 8080"
|
||||||
|
test = "bash -lc 'cd src && PYTHONPATH=. pytest ../tests'"
|
||||||
|
test-cov = "bash -lc 'cd src && PYTHONPATH=. pytest ../tests --cov=editor_app --cov=app --cov-report=term-missing --cov-fail-under=95'"
|
||||||
708
Pipfile.lock
generated
Normal file
708
Pipfile.lock
generated
Normal file
@@ -0,0 +1,708 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "2eb83d592adec12e8260d182f8391fa19a3983b5bb75be542182cd5036ee5a86"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.11"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"annotated-doc": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320",
|
||||||
|
"sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==0.0.4"
|
||||||
|
},
|
||||||
|
"annotated-types": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53",
|
||||||
|
"sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
|
"anyio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708",
|
||||||
|
"sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==4.13.0"
|
||||||
|
},
|
||||||
|
"click": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5",
|
||||||
|
"sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==8.3.2"
|
||||||
|
},
|
||||||
|
"fastapi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9b0f590c813acd13d0ab43dd8494138eb58e484bfac405db1f3187cfc5810d98",
|
||||||
|
"sha256:bd6d7caf1a2bdd8d676843cdcd2287729572a1ef524fc4d65c17ae002a1be654"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==0.135.3"
|
||||||
|
},
|
||||||
|
"h11": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
|
||||||
|
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==0.16.0"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
||||||
|
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.11"
|
||||||
|
},
|
||||||
|
"pillow": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9",
|
||||||
|
"sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5",
|
||||||
|
"sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987",
|
||||||
|
"sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9",
|
||||||
|
"sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b",
|
||||||
|
"sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f",
|
||||||
|
"sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd",
|
||||||
|
"sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e",
|
||||||
|
"sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e",
|
||||||
|
"sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe",
|
||||||
|
"sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795",
|
||||||
|
"sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601",
|
||||||
|
"sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1",
|
||||||
|
"sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed",
|
||||||
|
"sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea",
|
||||||
|
"sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5",
|
||||||
|
"sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97",
|
||||||
|
"sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453",
|
||||||
|
"sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98",
|
||||||
|
"sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa",
|
||||||
|
"sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b",
|
||||||
|
"sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d",
|
||||||
|
"sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705",
|
||||||
|
"sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8",
|
||||||
|
"sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024",
|
||||||
|
"sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0",
|
||||||
|
"sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286",
|
||||||
|
"sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150",
|
||||||
|
"sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2",
|
||||||
|
"sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3",
|
||||||
|
"sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b",
|
||||||
|
"sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f",
|
||||||
|
"sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463",
|
||||||
|
"sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940",
|
||||||
|
"sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166",
|
||||||
|
"sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed",
|
||||||
|
"sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f",
|
||||||
|
"sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795",
|
||||||
|
"sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780",
|
||||||
|
"sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7",
|
||||||
|
"sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1",
|
||||||
|
"sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5",
|
||||||
|
"sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295",
|
||||||
|
"sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b",
|
||||||
|
"sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354",
|
||||||
|
"sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60",
|
||||||
|
"sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65",
|
||||||
|
"sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005",
|
||||||
|
"sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c",
|
||||||
|
"sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be",
|
||||||
|
"sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5",
|
||||||
|
"sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06",
|
||||||
|
"sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae",
|
||||||
|
"sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c",
|
||||||
|
"sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c",
|
||||||
|
"sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612",
|
||||||
|
"sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e",
|
||||||
|
"sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab",
|
||||||
|
"sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808",
|
||||||
|
"sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f",
|
||||||
|
"sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e",
|
||||||
|
"sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909",
|
||||||
|
"sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec",
|
||||||
|
"sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe",
|
||||||
|
"sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50",
|
||||||
|
"sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4",
|
||||||
|
"sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f",
|
||||||
|
"sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff",
|
||||||
|
"sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5",
|
||||||
|
"sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb",
|
||||||
|
"sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414",
|
||||||
|
"sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1",
|
||||||
|
"sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032",
|
||||||
|
"sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76",
|
||||||
|
"sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136",
|
||||||
|
"sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e",
|
||||||
|
"sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c",
|
||||||
|
"sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3",
|
||||||
|
"sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea",
|
||||||
|
"sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f",
|
||||||
|
"sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104",
|
||||||
|
"sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176",
|
||||||
|
"sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24",
|
||||||
|
"sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3",
|
||||||
|
"sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4",
|
||||||
|
"sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed",
|
||||||
|
"sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43",
|
||||||
|
"sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421",
|
||||||
|
"sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7",
|
||||||
|
"sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06",
|
||||||
|
"sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==12.2.0"
|
||||||
|
},
|
||||||
|
"pydantic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49",
|
||||||
|
"sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.12.5"
|
||||||
|
},
|
||||||
|
"pydantic-core": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90",
|
||||||
|
"sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740",
|
||||||
|
"sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504",
|
||||||
|
"sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84",
|
||||||
|
"sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33",
|
||||||
|
"sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c",
|
||||||
|
"sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0",
|
||||||
|
"sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e",
|
||||||
|
"sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0",
|
||||||
|
"sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a",
|
||||||
|
"sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34",
|
||||||
|
"sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2",
|
||||||
|
"sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3",
|
||||||
|
"sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815",
|
||||||
|
"sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14",
|
||||||
|
"sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba",
|
||||||
|
"sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375",
|
||||||
|
"sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf",
|
||||||
|
"sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963",
|
||||||
|
"sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1",
|
||||||
|
"sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808",
|
||||||
|
"sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553",
|
||||||
|
"sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1",
|
||||||
|
"sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2",
|
||||||
|
"sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5",
|
||||||
|
"sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470",
|
||||||
|
"sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2",
|
||||||
|
"sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b",
|
||||||
|
"sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660",
|
||||||
|
"sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c",
|
||||||
|
"sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093",
|
||||||
|
"sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5",
|
||||||
|
"sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594",
|
||||||
|
"sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008",
|
||||||
|
"sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a",
|
||||||
|
"sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a",
|
||||||
|
"sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd",
|
||||||
|
"sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284",
|
||||||
|
"sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586",
|
||||||
|
"sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869",
|
||||||
|
"sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294",
|
||||||
|
"sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f",
|
||||||
|
"sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66",
|
||||||
|
"sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51",
|
||||||
|
"sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc",
|
||||||
|
"sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97",
|
||||||
|
"sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a",
|
||||||
|
"sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d",
|
||||||
|
"sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9",
|
||||||
|
"sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c",
|
||||||
|
"sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07",
|
||||||
|
"sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36",
|
||||||
|
"sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e",
|
||||||
|
"sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05",
|
||||||
|
"sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e",
|
||||||
|
"sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941",
|
||||||
|
"sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3",
|
||||||
|
"sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612",
|
||||||
|
"sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3",
|
||||||
|
"sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b",
|
||||||
|
"sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe",
|
||||||
|
"sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146",
|
||||||
|
"sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11",
|
||||||
|
"sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60",
|
||||||
|
"sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd",
|
||||||
|
"sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b",
|
||||||
|
"sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c",
|
||||||
|
"sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a",
|
||||||
|
"sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460",
|
||||||
|
"sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1",
|
||||||
|
"sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf",
|
||||||
|
"sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf",
|
||||||
|
"sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858",
|
||||||
|
"sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2",
|
||||||
|
"sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9",
|
||||||
|
"sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2",
|
||||||
|
"sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3",
|
||||||
|
"sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6",
|
||||||
|
"sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770",
|
||||||
|
"sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d",
|
||||||
|
"sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc",
|
||||||
|
"sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23",
|
||||||
|
"sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26",
|
||||||
|
"sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa",
|
||||||
|
"sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8",
|
||||||
|
"sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d",
|
||||||
|
"sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3",
|
||||||
|
"sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d",
|
||||||
|
"sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034",
|
||||||
|
"sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9",
|
||||||
|
"sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1",
|
||||||
|
"sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56",
|
||||||
|
"sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b",
|
||||||
|
"sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c",
|
||||||
|
"sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a",
|
||||||
|
"sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e",
|
||||||
|
"sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9",
|
||||||
|
"sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5",
|
||||||
|
"sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a",
|
||||||
|
"sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556",
|
||||||
|
"sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e",
|
||||||
|
"sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49",
|
||||||
|
"sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2",
|
||||||
|
"sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9",
|
||||||
|
"sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b",
|
||||||
|
"sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc",
|
||||||
|
"sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb",
|
||||||
|
"sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0",
|
||||||
|
"sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8",
|
||||||
|
"sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82",
|
||||||
|
"sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69",
|
||||||
|
"sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b",
|
||||||
|
"sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c",
|
||||||
|
"sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75",
|
||||||
|
"sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5",
|
||||||
|
"sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f",
|
||||||
|
"sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad",
|
||||||
|
"sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b",
|
||||||
|
"sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7",
|
||||||
|
"sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425",
|
||||||
|
"sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.41.5"
|
||||||
|
},
|
||||||
|
"starlette": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149",
|
||||||
|
"sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==1.0.0"
|
||||||
|
},
|
||||||
|
"startspimage": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4f726cc244baad240dcdac4ec387211d46a2d67abe6ce60e366d87b2915acd89"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.6"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466",
|
||||||
|
"sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==4.15.0"
|
||||||
|
},
|
||||||
|
"typing-inspection": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7",
|
||||||
|
"sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==0.4.2"
|
||||||
|
},
|
||||||
|
"uvicorn": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6c942071b68f07e178264b9152f1f16dfac5da85880c4ce06366a96d70d4f31e",
|
||||||
|
"sha256:ce937c99a2cc70279556967274414c087888e8cec9f9c94644dfca11bd3ced89"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==0.44.0"
|
||||||
|
},
|
||||||
|
"websockets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c",
|
||||||
|
"sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a",
|
||||||
|
"sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe",
|
||||||
|
"sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e",
|
||||||
|
"sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec",
|
||||||
|
"sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1",
|
||||||
|
"sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64",
|
||||||
|
"sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3",
|
||||||
|
"sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8",
|
||||||
|
"sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206",
|
||||||
|
"sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3",
|
||||||
|
"sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156",
|
||||||
|
"sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d",
|
||||||
|
"sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9",
|
||||||
|
"sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad",
|
||||||
|
"sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2",
|
||||||
|
"sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03",
|
||||||
|
"sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8",
|
||||||
|
"sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230",
|
||||||
|
"sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8",
|
||||||
|
"sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea",
|
||||||
|
"sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641",
|
||||||
|
"sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957",
|
||||||
|
"sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6",
|
||||||
|
"sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6",
|
||||||
|
"sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5",
|
||||||
|
"sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f",
|
||||||
|
"sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00",
|
||||||
|
"sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e",
|
||||||
|
"sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b",
|
||||||
|
"sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72",
|
||||||
|
"sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39",
|
||||||
|
"sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9",
|
||||||
|
"sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79",
|
||||||
|
"sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0",
|
||||||
|
"sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac",
|
||||||
|
"sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35",
|
||||||
|
"sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0",
|
||||||
|
"sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5",
|
||||||
|
"sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c",
|
||||||
|
"sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8",
|
||||||
|
"sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1",
|
||||||
|
"sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244",
|
||||||
|
"sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3",
|
||||||
|
"sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767",
|
||||||
|
"sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a",
|
||||||
|
"sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d",
|
||||||
|
"sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd",
|
||||||
|
"sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e",
|
||||||
|
"sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944",
|
||||||
|
"sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82",
|
||||||
|
"sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d",
|
||||||
|
"sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4",
|
||||||
|
"sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5",
|
||||||
|
"sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904",
|
||||||
|
"sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde",
|
||||||
|
"sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f",
|
||||||
|
"sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c",
|
||||||
|
"sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89",
|
||||||
|
"sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da",
|
||||||
|
"sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"anyio": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708",
|
||||||
|
"sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==4.13.0"
|
||||||
|
},
|
||||||
|
"astroid": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753",
|
||||||
|
"sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.10.0'",
|
||||||
|
"version": "==4.0.4"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa",
|
||||||
|
"sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2026.2.25"
|
||||||
|
},
|
||||||
|
"coverage": {
|
||||||
|
"extras": [
|
||||||
|
"toml"
|
||||||
|
],
|
||||||
|
"hashes": [
|
||||||
|
"sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256",
|
||||||
|
"sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b",
|
||||||
|
"sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5",
|
||||||
|
"sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d",
|
||||||
|
"sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a",
|
||||||
|
"sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969",
|
||||||
|
"sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642",
|
||||||
|
"sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87",
|
||||||
|
"sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740",
|
||||||
|
"sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215",
|
||||||
|
"sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d",
|
||||||
|
"sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422",
|
||||||
|
"sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8",
|
||||||
|
"sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911",
|
||||||
|
"sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b",
|
||||||
|
"sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587",
|
||||||
|
"sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8",
|
||||||
|
"sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606",
|
||||||
|
"sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9",
|
||||||
|
"sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf",
|
||||||
|
"sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633",
|
||||||
|
"sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6",
|
||||||
|
"sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43",
|
||||||
|
"sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2",
|
||||||
|
"sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61",
|
||||||
|
"sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930",
|
||||||
|
"sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc",
|
||||||
|
"sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247",
|
||||||
|
"sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75",
|
||||||
|
"sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e",
|
||||||
|
"sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376",
|
||||||
|
"sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01",
|
||||||
|
"sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1",
|
||||||
|
"sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3",
|
||||||
|
"sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743",
|
||||||
|
"sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9",
|
||||||
|
"sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf",
|
||||||
|
"sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e",
|
||||||
|
"sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1",
|
||||||
|
"sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd",
|
||||||
|
"sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b",
|
||||||
|
"sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab",
|
||||||
|
"sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d",
|
||||||
|
"sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a",
|
||||||
|
"sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0",
|
||||||
|
"sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510",
|
||||||
|
"sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f",
|
||||||
|
"sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0",
|
||||||
|
"sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8",
|
||||||
|
"sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf",
|
||||||
|
"sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209",
|
||||||
|
"sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9",
|
||||||
|
"sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3",
|
||||||
|
"sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3",
|
||||||
|
"sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d",
|
||||||
|
"sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd",
|
||||||
|
"sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2",
|
||||||
|
"sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882",
|
||||||
|
"sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09",
|
||||||
|
"sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea",
|
||||||
|
"sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c",
|
||||||
|
"sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562",
|
||||||
|
"sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3",
|
||||||
|
"sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806",
|
||||||
|
"sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e",
|
||||||
|
"sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878",
|
||||||
|
"sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e",
|
||||||
|
"sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9",
|
||||||
|
"sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45",
|
||||||
|
"sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29",
|
||||||
|
"sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4",
|
||||||
|
"sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c",
|
||||||
|
"sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479",
|
||||||
|
"sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400",
|
||||||
|
"sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c",
|
||||||
|
"sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a",
|
||||||
|
"sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf",
|
||||||
|
"sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686",
|
||||||
|
"sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de",
|
||||||
|
"sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028",
|
||||||
|
"sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0",
|
||||||
|
"sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179",
|
||||||
|
"sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16",
|
||||||
|
"sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85",
|
||||||
|
"sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a",
|
||||||
|
"sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0",
|
||||||
|
"sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810",
|
||||||
|
"sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161",
|
||||||
|
"sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607",
|
||||||
|
"sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26",
|
||||||
|
"sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819",
|
||||||
|
"sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40",
|
||||||
|
"sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5",
|
||||||
|
"sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15",
|
||||||
|
"sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0",
|
||||||
|
"sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90",
|
||||||
|
"sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0",
|
||||||
|
"sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6",
|
||||||
|
"sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a",
|
||||||
|
"sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58",
|
||||||
|
"sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b",
|
||||||
|
"sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17",
|
||||||
|
"sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5",
|
||||||
|
"sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664",
|
||||||
|
"sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0",
|
||||||
|
"sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==7.13.5"
|
||||||
|
},
|
||||||
|
"dill": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d",
|
||||||
|
"sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.11'",
|
||||||
|
"version": "==0.4.1"
|
||||||
|
},
|
||||||
|
"h11": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1",
|
||||||
|
"sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==0.16.0"
|
||||||
|
},
|
||||||
|
"httpcore": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55",
|
||||||
|
"sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==1.0.9"
|
||||||
|
},
|
||||||
|
"httpx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc",
|
||||||
|
"sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==0.28.1"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
||||||
|
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.11"
|
||||||
|
},
|
||||||
|
"iniconfig": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730",
|
||||||
|
"sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==2.3.0"
|
||||||
|
},
|
||||||
|
"isort": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d",
|
||||||
|
"sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75"
|
||||||
|
],
|
||||||
|
"markers": "python_full_version >= '3.10.0'",
|
||||||
|
"version": "==8.0.1"
|
||||||
|
},
|
||||||
|
"jedi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0",
|
||||||
|
"sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.19.2"
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
|
||||||
|
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4",
|
||||||
|
"sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==26.0"
|
||||||
|
},
|
||||||
|
"parso": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd",
|
||||||
|
"sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.8.6"
|
||||||
|
},
|
||||||
|
"platformdirs": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a",
|
||||||
|
"sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==4.9.6"
|
||||||
|
},
|
||||||
|
"pluggy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3",
|
||||||
|
"sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==1.6.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f",
|
||||||
|
"sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.20.0"
|
||||||
|
},
|
||||||
|
"pylint": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00f51c9b14a3b3ae08cff6b2cdd43f28165c78b165b628692e428fb1f8dc2cf2",
|
||||||
|
"sha256:8cd6a618df75deb013bd7eb98327a95f02a6fb839205a6bbf5456ef96afb317c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_full_version >= '3.10.0'",
|
||||||
|
"version": "==4.0.5"
|
||||||
|
},
|
||||||
|
"pytest": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9",
|
||||||
|
"sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==9.0.3"
|
||||||
|
},
|
||||||
|
"pytest-cov": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2",
|
||||||
|
"sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==7.1.0"
|
||||||
|
},
|
||||||
|
"tomlkit": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680",
|
||||||
|
"sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==0.14.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
README.md
14
README.md
@@ -1,2 +1,16 @@
|
|||||||
# connectionmachine
|
# connectionmachine
|
||||||
|
export NODE_OPTIONS=--max-old-space-size=800
|
||||||
|
|
||||||
|
## Web text editor
|
||||||
|
|
||||||
|
This repo includes a FastAPI-based web editor in `editor/` for editing text files.
|
||||||
|
|
||||||
|
Run it from the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipenv run start
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open [http://localhost:8000](http://localhost:8000).
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
38
code/demo_prompt_args.py
Normal file
38
code/demo_prompt_args.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
workspace_root = Path(__file__).resolve().parents[1]
|
||||||
|
prompts_root = workspace_root / "prompts"
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: demo_prompt_args.py <prompt-folder>")
|
||||||
|
print("Example: demo_prompt_args.py test")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
prompt_folder = sys.argv[1].strip().strip("/")
|
||||||
|
if not prompt_folder:
|
||||||
|
print("Error: prompt folder arg is empty.")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
target_dir = prompts_root / prompt_folder
|
||||||
|
if not target_dir.exists() or not target_dir.is_dir():
|
||||||
|
print(f"Error: prompts folder not found: {target_dir}")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
txt_files = sorted(target_dir.glob("*.txt"))
|
||||||
|
if not txt_files:
|
||||||
|
print(f"No .txt files found in prompts/{prompt_folder}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"Reading {len(txt_files)} prompt file(s) from prompts/{prompt_folder}")
|
||||||
|
for txt_file in txt_files:
|
||||||
|
print(f"\n--- {txt_file.name} ---")
|
||||||
|
print(txt_file.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
7
code/main.py
Normal file
7
code/main.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from time import sleep
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while True:
|
||||||
|
print(f"Hello {i}")
|
||||||
|
i += 1
|
||||||
|
sleep(1)
|
||||||
3
code/test.py
Normal file
3
code/test.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from printer import printer
|
||||||
|
|
||||||
|
printer("hello")
|
||||||
57
lib/helpers.py
Normal file
57
lib/helpers.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now_iso() -> str:
|
||||||
|
"""Return current UTC time in ISO-8601 format."""
|
||||||
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def log(message: str) -> None:
|
||||||
|
"""Print a timestamped log line."""
|
||||||
|
print(f"[{utc_now_iso()}] {message}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_workspace_root(current_file: str | Path) -> Path:
|
||||||
|
"""Infer workspace root from a script path inside code/."""
|
||||||
|
current = Path(current_file).resolve()
|
||||||
|
return current.parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def get_prompt_dir(workspace_root: Path, prompt_folder: str) -> Path:
|
||||||
|
"""Resolve prompts/<prompt_folder> directory safely."""
|
||||||
|
folder = prompt_folder.strip().strip("/")
|
||||||
|
if not folder:
|
||||||
|
raise ValueError("Prompt folder argument is empty")
|
||||||
|
target = (workspace_root / "prompts" / folder).resolve()
|
||||||
|
prompts_root = (workspace_root / "prompts").resolve()
|
||||||
|
if prompts_root not in target.parents and target != prompts_root:
|
||||||
|
raise ValueError("Prompt folder escapes prompts root")
|
||||||
|
if not target.exists() or not target.is_dir():
|
||||||
|
raise FileNotFoundError(f"Prompt folder not found: {target}")
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def read_prompt_texts(prompt_dir: Path) -> dict[str, str]:
|
||||||
|
"""Read all .txt files from prompt directory."""
|
||||||
|
result: dict[str, str] = {}
|
||||||
|
for txt_file in sorted(prompt_dir.glob("*.txt")):
|
||||||
|
result[txt_file.name] = txt_file.read_text(encoding="utf-8")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def read_json(path: Path, default: Any = None) -> Any:
|
||||||
|
"""Read JSON file; return default when missing."""
|
||||||
|
if not path.exists():
|
||||||
|
return default
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(path: Path, data: Any) -> None:
|
||||||
|
"""Write JSON with stable formatting."""
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(json.dumps(data, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
2
lib/printer.py
Normal file
2
lib/printer.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
def printer(text):
|
||||||
|
print(text)
|
||||||
1
prompts/test/test.txt
Normal file
1
prompts/test/test.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
hello
|
||||||
7
src/app.py
Normal file
7
src/app.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from editor_app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|
||||||
2
src/editor_app/__init__.py
Normal file
2
src/editor_app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from .main import app
|
||||||
|
|
||||||
BIN
src/editor_app/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
src/editor_app/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
src/editor_app/__pycache__/main.cpython-313.pyc
Normal file
BIN
src/editor_app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
30
src/editor_app/config.py
Normal file
30
src/editor_app/config.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
STATIC_DIR = BASE_DIR / "static"
|
||||||
|
|
||||||
|
|
||||||
|
def load_env_file(env_path: Path) -> None:
|
||||||
|
"""Load KEY=VALUE entries from a local .env file."""
|
||||||
|
if not env_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
stripped = line.strip()
|
||||||
|
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
||||||
|
continue
|
||||||
|
key, value = stripped.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip().strip('"').strip("'")
|
||||||
|
if key and key not in os.environ:
|
||||||
|
os.environ[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
load_env_file(BASE_DIR / ".env")
|
||||||
|
|
||||||
|
WORKSPACE_ROOT = Path(
|
||||||
|
os.getenv("WORKSPACE_ROOT", "/home/jimmy/projects/connectionmachine")
|
||||||
|
)
|
||||||
|
|
||||||
28
src/editor_app/main.py
Normal file
28
src/editor_app/main.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from editor_app.config import STATIC_DIR, WORKSPACE_ROOT
|
||||||
|
from editor_app.routers.files import router as files_router
|
||||||
|
from editor_app.routers.frontend import router as frontend_router
|
||||||
|
from editor_app.routers.python_exec import router as python_router
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
app = FastAPI()
|
||||||
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||||
|
app.include_router(frontend_router)
|
||||||
|
app.include_router(files_router)
|
||||||
|
app.include_router(python_router)
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def run_configured_startup_script() -> None:
|
||||||
|
from editor_app.services import python_runner
|
||||||
|
|
||||||
|
(WORKSPACE_ROOT / "lib").mkdir(parents=True, exist_ok=True)
|
||||||
|
python_runner.run_startup_script_if_configured()
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
38
src/editor_app/models.py
Normal file
38
src/editor_app/models.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
class FileContent(BaseModel):
|
||||||
|
content: str
|
||||||
|
|
||||||
|
|
||||||
|
class FileInfo(BaseModel):
|
||||||
|
name: str
|
||||||
|
is_directory: bool
|
||||||
|
size: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FolderOperation(BaseModel):
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
class RunPythonRequest(BaseModel):
|
||||||
|
file_path: str
|
||||||
|
args: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionRequest(BaseModel):
|
||||||
|
file_path: str
|
||||||
|
content: str
|
||||||
|
line: int
|
||||||
|
column: int
|
||||||
|
max_results: int = 20
|
||||||
|
|
||||||
|
|
||||||
|
class MoveFileRequest(BaseModel):
|
||||||
|
source_path: str
|
||||||
|
destination_folder: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class StartupScriptRequest(BaseModel):
|
||||||
|
file_path: str
|
||||||
1
src/editor_app/routers/__init__.py
Normal file
1
src/editor_app/routers/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
52
src/editor_app/routers/files.py
Normal file
52
src/editor_app/routers/files.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from editor_app.models import FileContent, FolderOperation, MoveFileRequest
|
||||||
|
from editor_app.services import filesystem
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/files")
|
||||||
|
async def list_files(path: str = ""):
|
||||||
|
files = filesystem.list_files(path)
|
||||||
|
return {"files": files}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/file/{file_path:path}")
|
||||||
|
async def read_file(file_path: str):
|
||||||
|
content, filename = filesystem.read_text_file(file_path)
|
||||||
|
return {"content": content, "filename": filename}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/file/{file_path:path}")
|
||||||
|
async def save_file(file_path: str, file_data: FileContent):
|
||||||
|
filename = filesystem.save_text_file(file_path, file_data.content)
|
||||||
|
return {"message": "File saved successfully", "filename": filename}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/file-move")
|
||||||
|
async def move_file(move_data: MoveFileRequest):
|
||||||
|
new_path, moved_type = filesystem.move_path(
|
||||||
|
source_path=move_data.source_path,
|
||||||
|
destination_folder=move_data.destination_folder,
|
||||||
|
)
|
||||||
|
return {"message": "Path moved successfully", "new_path": new_path, "moved_type": moved_type}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/file/{file_path:path}")
|
||||||
|
async def delete_file(file_path: str):
|
||||||
|
filesystem.delete_file(file_path)
|
||||||
|
return {"message": "File deleted successfully"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/folder/new/{folder_path:path}")
|
||||||
|
async def create_folder(folder_path: str, folder_data: FolderOperation):
|
||||||
|
folder_name = filesystem.create_folder(folder_path)
|
||||||
|
return {"message": "Folder created successfully", "folder": folder_name}
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/folder/{folder_path:path}")
|
||||||
|
async def delete_folder(folder_path: str):
|
||||||
|
filesystem.delete_folder(folder_path)
|
||||||
|
return {"message": "Folder deleted successfully"}
|
||||||
|
|
||||||
17
src/editor_app/routers/frontend.py
Normal file
17
src/editor_app/routers/frontend.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
|
||||||
|
from editor_app.config import STATIC_DIR
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
async def serve_home():
|
||||||
|
return FileResponse(STATIC_DIR / "home.html")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/editor")
|
||||||
|
async def serve_frontend():
|
||||||
|
return FileResponse(STATIC_DIR / "index.html")
|
||||||
|
|
||||||
120
src/editor_app/routers/python_exec.py
Normal file
120
src/editor_app/routers/python_exec.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from fastapi import APIRouter, HTTPException, WebSocket
|
||||||
|
|
||||||
|
from editor_app import config
|
||||||
|
from editor_app.models import CompletionRequest, RunPythonRequest, StartupScriptRequest
|
||||||
|
from editor_app.services import filesystem, python_runner
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/python")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/run")
|
||||||
|
async def run_python_file(run_request: RunPythonRequest):
|
||||||
|
target_path = filesystem.resolve_workspace_path(run_request.file_path)
|
||||||
|
if not target_path.exists() or not target_path.is_file():
|
||||||
|
raise HTTPException(status_code=404, detail="Python file not found")
|
||||||
|
if target_path.suffix.lower() != ".py":
|
||||||
|
raise HTTPException(status_code=400, detail="Only .py files can be run")
|
||||||
|
|
||||||
|
python_runner.run_python_file(target_path, run_request.file_path, run_request.args)
|
||||||
|
return {"message": "Python process started"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/output")
|
||||||
|
async def get_python_output(offset: int = 0):
|
||||||
|
return python_runner.get_output(offset)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stop")
|
||||||
|
async def stop_python_process():
|
||||||
|
message = python_runner.stop_python_process()
|
||||||
|
return {"message": message}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/completions")
|
||||||
|
async def get_python_completions(completion_request: CompletionRequest):
|
||||||
|
try:
|
||||||
|
target_path = filesystem.resolve_workspace_path(completion_request.file_path)
|
||||||
|
max_results = max(1, min(completion_request.max_results, 100))
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jedi
|
||||||
|
except ImportError as exc:
|
||||||
|
raise HTTPException(status_code=500, detail="jedi is not installed") from exc
|
||||||
|
|
||||||
|
script = jedi.Script(
|
||||||
|
code=completion_request.content,
|
||||||
|
path=str(target_path),
|
||||||
|
project=jedi.Project(path=str(config.WORKSPACE_ROOT)),
|
||||||
|
)
|
||||||
|
completions = script.complete(
|
||||||
|
line=completion_request.line,
|
||||||
|
column=completion_request.column,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"completions": [
|
||||||
|
{"name": item.name, "type": item.type, "complete": item.complete}
|
||||||
|
for item in completions[:max_results]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Completion failed: {exc}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
@router.websocket("/ws/output")
|
||||||
|
async def websocket_python_output(websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
offset = 0
|
||||||
|
last_running = None
|
||||||
|
last_return_code = None
|
||||||
|
while True:
|
||||||
|
data = python_runner.get_output(offset)
|
||||||
|
if data["lines"]:
|
||||||
|
await websocket.send_json(
|
||||||
|
{
|
||||||
|
"lines": data["lines"],
|
||||||
|
"next_offset": data["next_offset"],
|
||||||
|
"running": data["running"],
|
||||||
|
"return_code": data["return_code"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
offset = data["next_offset"]
|
||||||
|
last_running = data["running"]
|
||||||
|
last_return_code = data["return_code"]
|
||||||
|
# Yield between pushes to avoid overwhelming browser UI threads.
|
||||||
|
await asyncio.sleep(0.03)
|
||||||
|
else:
|
||||||
|
if data["running"] != last_running or data["return_code"] != last_return_code:
|
||||||
|
await websocket.send_json(
|
||||||
|
{
|
||||||
|
"lines": [],
|
||||||
|
"next_offset": offset,
|
||||||
|
"running": data["running"],
|
||||||
|
"return_code": data["return_code"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
last_running = data["running"]
|
||||||
|
last_return_code = data["return_code"]
|
||||||
|
# Keep the websocket active and low-latency.
|
||||||
|
await asyncio.sleep(0.2)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/scripts")
|
||||||
|
async def list_python_scripts():
|
||||||
|
return {"scripts": python_runner.list_python_scripts()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/startup-script")
|
||||||
|
async def get_startup_script():
|
||||||
|
return {"file_path": python_runner.get_startup_script()}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/startup-script")
|
||||||
|
async def set_startup_script(startup_request: StartupScriptRequest):
|
||||||
|
file_path = python_runner.set_startup_script(startup_request.file_path)
|
||||||
|
return {"message": "Startup script saved", "file_path": file_path}
|
||||||
|
|
||||||
1
src/editor_app/services/__init__.py
Normal file
1
src/editor_app/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
182
src/editor_app/services/filesystem.py
Normal file
182
src/editor_app/services/filesystem.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from editor_app import config
|
||||||
|
from editor_app.models import FileInfo
|
||||||
|
|
||||||
|
LIB_DIR_NAME = "lib"
|
||||||
|
WRITABLE_ROOTS = {"code", "prompts"}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_relative_path(relative_path: str) -> str:
|
||||||
|
cleaned = (relative_path or "").strip().lstrip("/")
|
||||||
|
if not cleaned:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parts = [segment for segment in cleaned.split("/") if segment]
|
||||||
|
if len(parts) >= 2 and parts[0] in {"code", "prompts"}:
|
||||||
|
while len(parts) >= 2 and parts[0] == parts[1]:
|
||||||
|
parts.pop(1)
|
||||||
|
return "/".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_workspace_path(relative_path: str) -> Path:
|
||||||
|
relative_path = normalize_relative_path(relative_path)
|
||||||
|
target_path = (config.WORKSPACE_ROOT / relative_path).resolve()
|
||||||
|
try:
|
||||||
|
target_path.relative_to(config.WORKSPACE_ROOT.resolve())
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail="Path escapes workspace") from exc
|
||||||
|
return target_path
|
||||||
|
|
||||||
|
|
||||||
|
def _is_path_in_lib(target_path: Path) -> bool:
|
||||||
|
workspace = config.WORKSPACE_ROOT.resolve()
|
||||||
|
lib_root = (workspace / LIB_DIR_NAME).resolve()
|
||||||
|
try:
|
||||||
|
target_path.resolve().relative_to(lib_root)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_not_lib_path(target_path: Path) -> None:
|
||||||
|
if _is_path_in_lib(target_path):
|
||||||
|
raise HTTPException(status_code=403, detail="lib is read-only")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_writable_path(target_path: Path) -> bool:
|
||||||
|
workspace = config.WORKSPACE_ROOT.resolve()
|
||||||
|
resolved = target_path.resolve()
|
||||||
|
try:
|
||||||
|
relative = resolved.relative_to(workspace)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if not relative.parts:
|
||||||
|
return False
|
||||||
|
return relative.parts[0] in WRITABLE_ROOTS
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_writable_path(target_path: Path) -> None:
|
||||||
|
if not _is_writable_path(target_path):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Only code and prompts are writable (lib is read-only)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_files(path: str = "") -> list[FileInfo]:
|
||||||
|
path = normalize_relative_path(path)
|
||||||
|
target_path = config.WORKSPACE_ROOT / path if path else config.WORKSPACE_ROOT
|
||||||
|
if not target_path.exists() or not target_path.is_dir():
|
||||||
|
raise HTTPException(status_code=404, detail="Directory not found")
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for item in sorted(target_path.iterdir()):
|
||||||
|
if item.name.startswith("."):
|
||||||
|
continue
|
||||||
|
files.append(
|
||||||
|
FileInfo(
|
||||||
|
name=item.name,
|
||||||
|
is_directory=item.is_dir(),
|
||||||
|
size=item.stat().st_size if item.is_file() else None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def read_text_file(file_path: str) -> tuple[str, str]:
|
||||||
|
target_path = resolve_workspace_path(file_path)
|
||||||
|
if not target_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
if target_path.is_dir():
|
||||||
|
raise HTTPException(status_code=400, detail="Path is a directory")
|
||||||
|
try:
|
||||||
|
content = target_path.read_text(encoding="utf-8")
|
||||||
|
except UnicodeDecodeError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail="File is not a text file") from exc
|
||||||
|
return content, target_path.name
|
||||||
|
|
||||||
|
|
||||||
|
def save_text_file(file_path: str, content: str) -> str:
|
||||||
|
target_path = resolve_workspace_path(file_path)
|
||||||
|
_ensure_not_lib_path(target_path)
|
||||||
|
_ensure_writable_path(target_path)
|
||||||
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target_path.write_text(content, encoding="utf-8")
|
||||||
|
return target_path.name
|
||||||
|
|
||||||
|
|
||||||
|
def delete_file(file_path: str) -> None:
|
||||||
|
target_path = resolve_workspace_path(file_path)
|
||||||
|
_ensure_not_lib_path(target_path)
|
||||||
|
_ensure_writable_path(target_path)
|
||||||
|
if not target_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
if target_path.is_dir():
|
||||||
|
raise HTTPException(status_code=400, detail="Cannot delete directories")
|
||||||
|
target_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def move_path(source_path: str, destination_folder: str) -> tuple[str, str]:
|
||||||
|
source = resolve_workspace_path(source_path)
|
||||||
|
_ensure_not_lib_path(source)
|
||||||
|
_ensure_writable_path(source)
|
||||||
|
if not source.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Source path not found")
|
||||||
|
|
||||||
|
destination_dir = (
|
||||||
|
resolve_workspace_path(destination_folder)
|
||||||
|
if destination_folder
|
||||||
|
else config.WORKSPACE_ROOT
|
||||||
|
)
|
||||||
|
_ensure_not_lib_path(destination_dir)
|
||||||
|
_ensure_writable_path(destination_dir)
|
||||||
|
if not destination_dir.exists() or not destination_dir.is_dir():
|
||||||
|
raise HTTPException(status_code=404, detail="Destination folder not found")
|
||||||
|
|
||||||
|
destination = destination_dir / source.name
|
||||||
|
source_resolved = source.resolve()
|
||||||
|
destination_resolved = destination.resolve()
|
||||||
|
if destination_resolved == source_resolved:
|
||||||
|
raise HTTPException(status_code=400, detail="Path is already in that folder")
|
||||||
|
if source.is_dir():
|
||||||
|
source_prefix = str(source_resolved) + "/"
|
||||||
|
if str(destination_dir.resolve()).startswith(source_prefix):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Cannot move a folder into itself or its child"
|
||||||
|
)
|
||||||
|
if destination.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="A path with that name already exists in destination",
|
||||||
|
)
|
||||||
|
|
||||||
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
source.rename(destination)
|
||||||
|
moved_type = "folder" if destination.is_dir() else "file"
|
||||||
|
return str(destination.relative_to(config.WORKSPACE_ROOT)), moved_type
|
||||||
|
|
||||||
|
|
||||||
|
def create_folder(folder_path: str) -> str:
|
||||||
|
target_path = resolve_workspace_path(folder_path)
|
||||||
|
_ensure_not_lib_path(target_path)
|
||||||
|
_ensure_writable_path(target_path)
|
||||||
|
if target_path.exists():
|
||||||
|
raise HTTPException(status_code=400, detail="Folder already exists")
|
||||||
|
target_path.mkdir(parents=True, exist_ok=False)
|
||||||
|
return target_path.name
|
||||||
|
|
||||||
|
|
||||||
|
def delete_folder(folder_path: str) -> None:
|
||||||
|
target_path = resolve_workspace_path(folder_path)
|
||||||
|
_ensure_not_lib_path(target_path)
|
||||||
|
_ensure_writable_path(target_path)
|
||||||
|
if not target_path.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Folder not found")
|
||||||
|
if not target_path.is_dir():
|
||||||
|
raise HTTPException(status_code=400, detail="Path is not a directory")
|
||||||
|
shutil.rmtree(target_path)
|
||||||
|
|
||||||
172
src/editor_app/services/python_runner.py
Normal file
172
src/editor_app/services/python_runner.py
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from editor_app import config
|
||||||
|
from editor_app.services import filesystem
|
||||||
|
|
||||||
|
|
||||||
|
class PythonRunnerState:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self.process: Optional[subprocess.Popen] = None
|
||||||
|
self.output_lines: list[str] = []
|
||||||
|
self.output_base_offset = 0
|
||||||
|
self.return_code: Optional[int] = None
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
|
||||||
|
python_runner = PythonRunnerState()
|
||||||
|
STARTUP_SCRIPT_FILE = config.WORKSPACE_ROOT / ".connectionmachine_startup_script"
|
||||||
|
MAX_OUTPUT_LINES = 5000
|
||||||
|
|
||||||
|
|
||||||
|
def _append_output_line(line: str) -> None:
|
||||||
|
python_runner.output_lines.append(line)
|
||||||
|
if len(python_runner.output_lines) > MAX_OUTPUT_LINES:
|
||||||
|
overflow = len(python_runner.output_lines) - MAX_OUTPUT_LINES
|
||||||
|
if overflow > 0:
|
||||||
|
del python_runner.output_lines[:overflow]
|
||||||
|
python_runner.output_base_offset += overflow
|
||||||
|
|
||||||
|
|
||||||
|
def stream_process_output(process: subprocess.Popen) -> None:
|
||||||
|
if process.stdout is None:
|
||||||
|
return
|
||||||
|
for line in process.stdout:
|
||||||
|
with python_runner.lock:
|
||||||
|
_append_output_line(line)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_process(process: subprocess.Popen) -> None:
|
||||||
|
return_code = process.wait()
|
||||||
|
with python_runner.lock:
|
||||||
|
python_runner.return_code = return_code
|
||||||
|
python_runner.running = False
|
||||||
|
python_runner.process = None
|
||||||
|
|
||||||
|
|
||||||
|
def run_python_file(target_path, requested_path: str, args: Optional[list[str]] = None) -> None:
|
||||||
|
run_args = [str(arg) for arg in (args or [])]
|
||||||
|
with python_runner.lock:
|
||||||
|
if python_runner.running:
|
||||||
|
raise HTTPException(status_code=409, detail="A Python process is already running")
|
||||||
|
|
||||||
|
try:
|
||||||
|
run_env = os.environ.copy()
|
||||||
|
run_env["PYTHONUNBUFFERED"] = "1"
|
||||||
|
lib_path = str((config.WORKSPACE_ROOT / "lib").resolve())
|
||||||
|
existing_pythonpath = run_env.get("PYTHONPATH", "")
|
||||||
|
if existing_pythonpath:
|
||||||
|
run_env["PYTHONPATH"] = f"{lib_path}:{existing_pythonpath}"
|
||||||
|
else:
|
||||||
|
run_env["PYTHONPATH"] = lib_path
|
||||||
|
process = subprocess.Popen(
|
||||||
|
[sys.executable, "-u", str(target_path), *run_args],
|
||||||
|
cwd=str(config.WORKSPACE_ROOT),
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.STDOUT,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
env=run_env,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to start process: {exc}") from exc
|
||||||
|
|
||||||
|
python_runner.process = process
|
||||||
|
command_parts = [sys.executable, requested_path, *run_args]
|
||||||
|
python_runner.output_lines = [f"$ {' '.join(command_parts)}\n"]
|
||||||
|
python_runner.output_base_offset = 0
|
||||||
|
python_runner.return_code = None
|
||||||
|
python_runner.running = True
|
||||||
|
|
||||||
|
threading.Thread(target=stream_process_output, args=(process,), daemon=True).start()
|
||||||
|
threading.Thread(target=wait_for_process, args=(process,), daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def get_output(offset: int = 0) -> dict:
|
||||||
|
with python_runner.lock:
|
||||||
|
safe_offset = max(python_runner.output_base_offset, offset)
|
||||||
|
relative_index = max(0, safe_offset - python_runner.output_base_offset)
|
||||||
|
lines = python_runner.output_lines[relative_index:]
|
||||||
|
return {
|
||||||
|
"lines": lines,
|
||||||
|
"next_offset": safe_offset + len(lines),
|
||||||
|
"running": python_runner.running,
|
||||||
|
"return_code": python_runner.return_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def stop_python_process() -> str:
|
||||||
|
with python_runner.lock:
|
||||||
|
process = python_runner.process
|
||||||
|
if not python_runner.running or process is None:
|
||||||
|
return "No running process"
|
||||||
|
|
||||||
|
process.terminate()
|
||||||
|
try:
|
||||||
|
process.wait(timeout=2)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
process.kill()
|
||||||
|
process.wait(timeout=2)
|
||||||
|
|
||||||
|
with python_runner.lock:
|
||||||
|
_append_output_line("\n[Process stopped]\n")
|
||||||
|
python_runner.running = False
|
||||||
|
python_runner.return_code = process.returncode
|
||||||
|
python_runner.process = None
|
||||||
|
|
||||||
|
return "Python process stopped"
|
||||||
|
|
||||||
|
|
||||||
|
def list_python_scripts() -> list[str]:
|
||||||
|
workspace = config.WORKSPACE_ROOT
|
||||||
|
scripts: list[str] = []
|
||||||
|
for path in workspace.rglob("*.py"):
|
||||||
|
rel = path.relative_to(workspace)
|
||||||
|
# Skip hidden paths.
|
||||||
|
if any(part.startswith(".") for part in rel.parts):
|
||||||
|
continue
|
||||||
|
scripts.append(str(rel))
|
||||||
|
scripts.sort()
|
||||||
|
return scripts
|
||||||
|
|
||||||
|
|
||||||
|
def set_startup_script(file_path: str) -> str:
|
||||||
|
target = filesystem.resolve_workspace_path(file_path)
|
||||||
|
if not target.exists() or not target.is_file():
|
||||||
|
raise HTTPException(status_code=404, detail="Python file not found")
|
||||||
|
if target.suffix.lower() != ".py":
|
||||||
|
raise HTTPException(status_code=400, detail="Only .py files can be selected")
|
||||||
|
relative_path = str(target.relative_to(config.WORKSPACE_ROOT))
|
||||||
|
STARTUP_SCRIPT_FILE.write_text(relative_path, encoding="utf-8")
|
||||||
|
return relative_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_startup_script() -> Optional[str]:
|
||||||
|
if not STARTUP_SCRIPT_FILE.exists():
|
||||||
|
return None
|
||||||
|
value = STARTUP_SCRIPT_FILE.read_text(encoding="utf-8").strip()
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def run_startup_script_if_configured() -> None:
|
||||||
|
startup_script = get_startup_script()
|
||||||
|
if not startup_script:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
target_path = filesystem.resolve_workspace_path(startup_script)
|
||||||
|
if not target_path.exists() or not target_path.is_file():
|
||||||
|
return
|
||||||
|
if target_path.suffix.lower() != ".py":
|
||||||
|
return
|
||||||
|
run_python_file(target_path, startup_script)
|
||||||
|
except HTTPException:
|
||||||
|
return
|
||||||
|
|
||||||
14
src/static/codemirror.bundle.mjs
Normal file
14
src/static/codemirror.bundle.mjs
Normal file
File diff suppressed because one or more lines are too long
345
src/static/home.html
Normal file
345
src/static/home.html
Normal file
@@ -0,0 +1,345 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Connection Machine</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #0f172a, #1e293b);
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card {
|
||||||
|
width: min(560px, 92vw);
|
||||||
|
background: rgba(15, 23, 42, 0.78);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 20px 45px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 0.75rem 0;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 1.25rem 0;
|
||||||
|
color: #cbd5e1;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.65rem 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: transparent;
|
||||||
|
border-color: #64748b;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner {
|
||||||
|
border-top: 1px solid rgba(148, 163, 184, 0.25);
|
||||||
|
padding-top: 1rem;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
color: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 220px;
|
||||||
|
background: #0f172a;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid #64748b;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runner-log {
|
||||||
|
min-height: 140px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
background: #020617;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.6rem;
|
||||||
|
font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #cbd5e1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="home-card">
|
||||||
|
<h1>Connection Machine</h1>
|
||||||
|
<p>Open the editor for the <code>code</code> workspace.</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn btn-primary" href="/editor">Open Editor</a>
|
||||||
|
</div>
|
||||||
|
<section class="runner">
|
||||||
|
<h2>Startup Script</h2>
|
||||||
|
<div class="runner-controls">
|
||||||
|
<select id="startup-script-select"></select>
|
||||||
|
<button id="save-startup-btn" class="btn btn-secondary" type="button">Set Startup</button>
|
||||||
|
</div>
|
||||||
|
<div class="runner-controls">
|
||||||
|
<select id="script-args-select"></select>
|
||||||
|
</div>
|
||||||
|
<div class="runner-controls">
|
||||||
|
<button id="start-script-btn" class="btn btn-primary" type="button">Start</button>
|
||||||
|
<button id="stop-script-btn" class="btn btn-secondary" type="button">Stop</button>
|
||||||
|
<span id="runner-status" class="status"></span>
|
||||||
|
</div>
|
||||||
|
<pre id="runner-log" class="runner-log"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
const selectEl = document.getElementById("startup-script-select");
|
||||||
|
const saveBtn = document.getElementById("save-startup-btn");
|
||||||
|
const startBtn = document.getElementById("start-script-btn");
|
||||||
|
const stopBtn = document.getElementById("stop-script-btn");
|
||||||
|
const argsSelectEl = document.getElementById("script-args-select");
|
||||||
|
const logEl = document.getElementById("runner-log");
|
||||||
|
const statusEl = document.getElementById("runner-status");
|
||||||
|
const homeStateKey = "connectionmachine.home.state.v1";
|
||||||
|
let outputOffset = 0;
|
||||||
|
let logSocket = null;
|
||||||
|
let pendingLogText = "";
|
||||||
|
let logFlushTimer = null;
|
||||||
|
|
||||||
|
function loadHomeState() {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(homeStateKey);
|
||||||
|
return raw ? JSON.parse(raw) : {};
|
||||||
|
} catch (_error) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveHomeState() {
|
||||||
|
try {
|
||||||
|
const state = {
|
||||||
|
selectedScript: selectEl.value || "",
|
||||||
|
selectedArg: argsSelectEl.value || "",
|
||||||
|
};
|
||||||
|
localStorage.setItem(homeStateKey, JSON.stringify(state));
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore storage failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(message) {
|
||||||
|
statusEl.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLog(lines) {
|
||||||
|
if (!Array.isArray(lines) || lines.length === 0) return;
|
||||||
|
pendingLogText += lines.join("");
|
||||||
|
if (logFlushTimer) return;
|
||||||
|
logFlushTimer = setTimeout(() => {
|
||||||
|
if (pendingLogText) {
|
||||||
|
logEl.textContent += pendingLogText;
|
||||||
|
pendingLogText = "";
|
||||||
|
}
|
||||||
|
const maxChars = 200000;
|
||||||
|
if (logEl.textContent.length > maxChars) {
|
||||||
|
logEl.textContent = logEl.textContent.slice(-maxChars);
|
||||||
|
}
|
||||||
|
logEl.scrollTop = logEl.scrollHeight;
|
||||||
|
logFlushTimer = null;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshScripts() {
|
||||||
|
const scriptsResp = await fetch("/api/python/scripts");
|
||||||
|
const scriptsData = await scriptsResp.json();
|
||||||
|
const startupResp = await fetch("/api/python/startup-script");
|
||||||
|
const startupData = await startupResp.json();
|
||||||
|
const scripts = (scriptsData.scripts || []).filter((path) => path.startsWith("code/"));
|
||||||
|
const selected = startupData.file_path || "";
|
||||||
|
selectEl.innerHTML = "";
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = "";
|
||||||
|
opt.textContent = "No Python scripts found in code/";
|
||||||
|
selectEl.appendChild(opt);
|
||||||
|
selectEl.disabled = true;
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
startBtn.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
scripts.forEach((scriptPath) => {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = scriptPath;
|
||||||
|
opt.textContent = scriptPath.slice("code/".length);
|
||||||
|
if (scriptPath === selected) {
|
||||||
|
opt.selected = true;
|
||||||
|
}
|
||||||
|
selectEl.appendChild(opt);
|
||||||
|
});
|
||||||
|
const state = loadHomeState();
|
||||||
|
if (state.selectedScript && scripts.includes(state.selectedScript)) {
|
||||||
|
selectEl.value = state.selectedScript;
|
||||||
|
}
|
||||||
|
selectEl.disabled = false;
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
startBtn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshArgsOptions() {
|
||||||
|
const resp = await fetch("/api/files?path=prompts");
|
||||||
|
const data = await resp.json();
|
||||||
|
const folders = (data.files || []).filter((item) => item.is_directory).map((item) => item.name);
|
||||||
|
argsSelectEl.innerHTML = "";
|
||||||
|
const none = document.createElement("option");
|
||||||
|
none.value = "";
|
||||||
|
none.textContent = "No script args";
|
||||||
|
argsSelectEl.appendChild(none);
|
||||||
|
folders.forEach((folderName) => {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = folderName;
|
||||||
|
option.textContent = `Prompt folder: ${folderName}`;
|
||||||
|
argsSelectEl.appendChild(option);
|
||||||
|
});
|
||||||
|
const state = loadHomeState();
|
||||||
|
if (typeof state.selectedArg === "string") {
|
||||||
|
argsSelectEl.value = state.selectedArg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveStartupScript() {
|
||||||
|
const filePath = selectEl.value;
|
||||||
|
const resp = await fetch("/api/python/startup-script", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ file_path: filePath }),
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
setStatus("Failed to save startup script");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("Startup script saved");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startScript() {
|
||||||
|
const filePath = selectEl.value;
|
||||||
|
const selectedArg = argsSelectEl.value.trim();
|
||||||
|
const args = selectedArg ? [selectedArg] : [];
|
||||||
|
const resp = await fetch("/api/python/run", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ file_path: filePath, args }),
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
setStatus("Failed to start script");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
outputOffset = 0;
|
||||||
|
logEl.textContent = "";
|
||||||
|
setStatus("Script running");
|
||||||
|
startLogStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopScript() {
|
||||||
|
const resp = await fetch("/api/python/stop", { method: "POST" });
|
||||||
|
if (!resp.ok) {
|
||||||
|
setStatus("Failed to stop script");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setStatus("Script stopped");
|
||||||
|
stopLogStream();
|
||||||
|
startLogStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startLogStream() {
|
||||||
|
stopLogStream();
|
||||||
|
const protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||||
|
logSocket = new WebSocket(`${protocol}://${window.location.host}/api/python/ws/output`);
|
||||||
|
logSocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
addLog(data.lines || []);
|
||||||
|
outputOffset = data.next_offset ?? outputOffset;
|
||||||
|
if (!data.running) {
|
||||||
|
if (data.return_code !== null && data.return_code !== undefined) {
|
||||||
|
addLog([`\n[Process exited with code ${data.return_code}]\n`]);
|
||||||
|
}
|
||||||
|
setStatus("Script idle");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
logSocket.onerror = () => {
|
||||||
|
setStatus("Log stream connection error");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopLogStream() {
|
||||||
|
if (logSocket) {
|
||||||
|
logSocket.close();
|
||||||
|
logSocket = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBtn.addEventListener("click", saveStartupScript);
|
||||||
|
startBtn.addEventListener("click", startScript);
|
||||||
|
stopBtn.addEventListener("click", stopScript);
|
||||||
|
selectEl.addEventListener("change", saveHomeState);
|
||||||
|
argsSelectEl.addEventListener("change", saveHomeState);
|
||||||
|
window.addEventListener("beforeunload", saveHomeState);
|
||||||
|
window.addEventListener("pagehide", saveHomeState);
|
||||||
|
|
||||||
|
Promise.all([refreshScripts(), refreshArgsOptions()]).then(() => {
|
||||||
|
setStatus("Ready");
|
||||||
|
startLogStream();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
src/static/index.html
Normal file
79
src/static/index.html
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Connection Machine Editor</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<link rel="stylesheet" href="/static/styles.css?v=5">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="sidebar">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<h3>Files</h3>
|
||||||
|
<div class="sidebar-actions">
|
||||||
|
<button id="new-file-btn" title="New File">📄</button>
|
||||||
|
<button id="new-folder-btn" title="New Folder">📁+</button>
|
||||||
|
<button id="delete-selected-btn" title="Delete Selected">🗑️</button>
|
||||||
|
<button id="refresh-btn" title="Refresh">🔄</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="file-tree" id="file-tree">
|
||||||
|
<div class="loading">Loading files...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="editor-header">
|
||||||
|
<div class="file-info">
|
||||||
|
<span id="save-status" class="save-status"></span>
|
||||||
|
</div>
|
||||||
|
<div class="mode-toggle">
|
||||||
|
<a id="home-btn" class="mode-btn active" href="/">Home</a>
|
||||||
|
</div>
|
||||||
|
<div class="editor-actions">
|
||||||
|
<button id="run-btn" disabled>Run Python</button>
|
||||||
|
<button id="stop-btn" disabled>Stop</button>
|
||||||
|
<select id="run-file-select" title="Script to run">
|
||||||
|
<option value="">Run active file</option>
|
||||||
|
</select>
|
||||||
|
<select id="run-arg-select" title="Prompt folder argument">
|
||||||
|
<option value="">No prompt arg</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tabs" class="tabs"></div>
|
||||||
|
|
||||||
|
<div class="editor-container">
|
||||||
|
<div id="editor"></div>
|
||||||
|
<div id="prompt-panel" class="prompt-panel hidden">
|
||||||
|
<label for="prompt-input">Prompt</label>
|
||||||
|
<textarea id="prompt-input" placeholder="Write your prompt here..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="completion-dropdown" class="completion-dropdown"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="console-container">
|
||||||
|
<div class="console-header">Console Output</div>
|
||||||
|
<pre id="console-output" class="console-output"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New File Modal -->
|
||||||
|
<div id="new-file-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Create New File</h3>
|
||||||
|
<input type="text" id="new-filename" placeholder="Enter filename (e.g., example.txt)" />
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="create-file-btn">Create</button>
|
||||||
|
<button id="cancel-create-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/static/script.js?v=7"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1664
src/static/script.js
Normal file
1664
src/static/script.js
Normal file
File diff suppressed because it is too large
Load Diff
500
src/static/styles.css
Normal file
500
src/static/styles.css
Normal file
@@ -0,0 +1,500 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: 300px;
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #4a5568;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-actions button:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
padding: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
background-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.selected {
|
||||||
|
background-color: #3182ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.drag-target {
|
||||||
|
background-color: #2b6cb0;
|
||||||
|
outline: 1px dashed #90cdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.dragging-file {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.directory {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item.root-drop {
|
||||||
|
border: 1px dashed #4a5568;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f7fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-file {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status.saved {
|
||||||
|
color: #38a169;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status.unsaved {
|
||||||
|
color: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
border: none;
|
||||||
|
background: #edf2f7;
|
||||||
|
color: #4a5568;
|
||||||
|
padding: 0.45rem 0.8rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn + .mode-btn {
|
||||||
|
border-left: 1px solid #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn.active {
|
||||||
|
background: #3182ce;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#run-arg-select {
|
||||||
|
min-width: 220px;
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#run-file-select {
|
||||||
|
min-width: 220px;
|
||||||
|
padding: 0.5rem 0.65rem;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: white;
|
||||||
|
color: #4a5568;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions button:hover:not(:disabled) {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
border-color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions button:not(:disabled):active {
|
||||||
|
background-color: #edf2f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-panel {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-panel label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4a5568;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-panel textarea {
|
||||||
|
flex: 1;
|
||||||
|
resize: none;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 30;
|
||||||
|
display: none;
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 420px;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-item:hover,
|
||||||
|
.completion-item.active {
|
||||||
|
background: #ebf8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-name {
|
||||||
|
color: #1a202c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.completion-type {
|
||||||
|
color: #718096;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0.4rem 0.5rem;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
border: 1px solid #cbd5e0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: white;
|
||||||
|
color: #2d3748;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
border-color: #3182ce;
|
||||||
|
background: #ebf8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
max-width: 260px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #718096;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
color: #e53e3e;
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 1rem;
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CodeMirror 6 styling */
|
||||||
|
.cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-focused {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-container {
|
||||||
|
height: 220px;
|
||||||
|
border-top: 1px solid #e2e8f0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-header {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: #cbd5e1;
|
||||||
|
border-bottom: 1px solid #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
overflow: auto;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #e2e8f0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: white;
|
||||||
|
margin: 15% auto;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 400px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h3 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3182ce;
|
||||||
|
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: white;
|
||||||
|
color: #4a5568;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button:hover {
|
||||||
|
background-color: #f7fafc;
|
||||||
|
border-color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button:first-child {
|
||||||
|
background-color: #3182ce;
|
||||||
|
color: white;
|
||||||
|
border-color: #3182ce;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions button:first-child:hover {
|
||||||
|
background-color: #2c5aa0;
|
||||||
|
border-color: #2c5aa0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
width: 90%;
|
||||||
|
margin: 20% auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
.file-tree::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar-track {
|
||||||
|
background: #2d3748;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar-thumb {
|
||||||
|
background: #4a5568;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-tree::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #718096;
|
||||||
|
}
|
||||||
|
|
||||||
373
tests/test_api.py
Normal file
373
tests/test_api.py
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import importlib
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(tmp_path):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.main as main
|
||||||
|
import editor_app.services.python_runner as runner
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
importlib.reload(main)
|
||||||
|
|
||||||
|
# Reset runner state to avoid cross-test contamination.
|
||||||
|
with runner.python_runner.lock:
|
||||||
|
runner.python_runner.process = None
|
||||||
|
runner.python_runner.output_lines = []
|
||||||
|
runner.python_runner.return_code = None
|
||||||
|
runner.python_runner.running = False
|
||||||
|
|
||||||
|
return TestClient(main.app)
|
||||||
|
|
||||||
|
|
||||||
|
def test_root_serves_html(client):
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "text/html" in response.headers["content-type"]
|
||||||
|
assert "Connection Machine" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_editor_route_serves_editor_html(client):
|
||||||
|
response = client.get("/editor")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "text/html" in response.headers["content-type"]
|
||||||
|
assert "Connection Machine Editor" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_files_hides_dotfiles_and_reports_sizes(client, tmp_path):
|
||||||
|
(tmp_path / ".hidden.txt").write_text("secret", encoding="utf-8")
|
||||||
|
(tmp_path / "visible.txt").write_text("hello", encoding="utf-8")
|
||||||
|
(tmp_path / "folder").mkdir()
|
||||||
|
|
||||||
|
response = client.get("/api/files")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
files = response.json()["files"]
|
||||||
|
names = {item["name"] for item in files}
|
||||||
|
assert ".hidden.txt" not in names
|
||||||
|
assert "visible.txt" in names
|
||||||
|
assert "folder" in names
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_files_missing_directory_returns_404(client):
|
||||||
|
response = client.get("/api/files", params={"path": "does-not-exist"})
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_and_read_file_roundtrip(client, tmp_path):
|
||||||
|
response = client.post("/api/file/code/docs/readme.txt", json={"content": "doc body"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert (tmp_path / "code" / "docs" / "readme.txt").read_text(encoding="utf-8") == "doc body"
|
||||||
|
|
||||||
|
read_response = client.get("/api/file/code/docs/readme.txt")
|
||||||
|
assert read_response.status_code == 200
|
||||||
|
assert read_response.json()["content"] == "doc body"
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_file_collapses_duplicate_scoped_prefix(client, tmp_path):
|
||||||
|
response = client.post("/api/file/code/code/main.py", json={"content": "print('ok')"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert (tmp_path / "code" / "main.py").read_text(encoding="utf-8") == "print('ok')"
|
||||||
|
assert not (tmp_path / "code" / "code" / "main.py").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_lib_folder_is_read_only_for_mutations(client, tmp_path):
|
||||||
|
lib_dir = tmp_path / "lib"
|
||||||
|
lib_dir.mkdir()
|
||||||
|
(lib_dir / "helper.py").write_text("x = 1\n", encoding="utf-8")
|
||||||
|
code_dir = tmp_path / "code"
|
||||||
|
code_dir.mkdir()
|
||||||
|
(code_dir / "main.py").write_text("print('ok')\n", encoding="utf-8")
|
||||||
|
|
||||||
|
save_blocked = client.post("/api/file/lib/new.txt", json={"content": "nope"})
|
||||||
|
assert save_blocked.status_code == 403
|
||||||
|
|
||||||
|
delete_blocked = client.delete("/api/file/lib/helper.py")
|
||||||
|
assert delete_blocked.status_code == 403
|
||||||
|
|
||||||
|
move_blocked = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/main.py", "destination_folder": "lib"},
|
||||||
|
)
|
||||||
|
assert move_blocked.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
def test_only_code_and_prompts_are_writable(client, tmp_path):
|
||||||
|
blocked_file = client.post("/api/file/notes.txt", json={"content": "nope"})
|
||||||
|
assert blocked_file.status_code == 403
|
||||||
|
|
||||||
|
blocked_folder = client.post("/api/folder/new/archive", json={"path": "ignored"})
|
||||||
|
assert blocked_folder.status_code == 403
|
||||||
|
|
||||||
|
allowed_prompt = client.post("/api/file/prompts/a.txt", json={"content": "ok"})
|
||||||
|
assert allowed_prompt.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_errors_for_directory_and_missing(client, tmp_path):
|
||||||
|
(tmp_path / "docs").mkdir()
|
||||||
|
|
||||||
|
dir_response = client.get("/api/file/docs")
|
||||||
|
assert dir_response.status_code == 400
|
||||||
|
|
||||||
|
missing_response = client.get("/api/file/missing.txt")
|
||||||
|
assert missing_response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_file_non_utf8_returns_400(client, tmp_path):
|
||||||
|
(tmp_path / "bin.dat").write_bytes(b"\xff\xfe\x00")
|
||||||
|
response = client.get("/api/file/bin.dat")
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_file_success_and_errors(client, tmp_path):
|
||||||
|
target = tmp_path / "code" / "delete-me.txt"
|
||||||
|
target.parent.mkdir()
|
||||||
|
target.write_text("x", encoding="utf-8")
|
||||||
|
|
||||||
|
ok = client.delete("/api/file/code/delete-me.txt")
|
||||||
|
assert ok.status_code == 200
|
||||||
|
assert not target.exists()
|
||||||
|
|
||||||
|
missing = client.delete("/api/file/code/delete-me.txt")
|
||||||
|
assert missing.status_code == 404
|
||||||
|
|
||||||
|
(tmp_path / "code" / "dir").mkdir(parents=True)
|
||||||
|
directory = client.delete("/api/file/code/dir")
|
||||||
|
assert directory.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_file_to_another_folder(client, tmp_path):
|
||||||
|
source = tmp_path / "code" / "docs" / "note.txt"
|
||||||
|
source.parent.mkdir(parents=True)
|
||||||
|
source.write_text("hello", encoding="utf-8")
|
||||||
|
(tmp_path / "code" / "archive").mkdir(parents=True)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/docs/note.txt", "destination_folder": "code/archive"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["new_path"] == "code/archive/note.txt"
|
||||||
|
assert not source.exists()
|
||||||
|
assert (tmp_path / "code" / "archive" / "note.txt").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_file_errors(client, tmp_path):
|
||||||
|
(tmp_path / "code" / "docs").mkdir(parents=True)
|
||||||
|
(tmp_path / "code" / "docs" / "note.txt").write_text("x", encoding="utf-8")
|
||||||
|
(tmp_path / "code" / "archive").mkdir(parents=True)
|
||||||
|
(tmp_path / "code" / "archive" / "note.txt").write_text("x", encoding="utf-8")
|
||||||
|
|
||||||
|
conflict = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/docs/note.txt", "destination_folder": "code/archive"},
|
||||||
|
)
|
||||||
|
assert conflict.status_code == 409
|
||||||
|
|
||||||
|
missing = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/missing.txt", "destination_folder": "code/archive"},
|
||||||
|
)
|
||||||
|
assert missing.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_folder_to_another_folder(client, tmp_path):
|
||||||
|
(tmp_path / "code" / "docs").mkdir(parents=True)
|
||||||
|
(tmp_path / "code" / "docs" / "note.txt").write_text("x", encoding="utf-8")
|
||||||
|
(tmp_path / "code" / "archive").mkdir(parents=True)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/docs", "destination_folder": "code/archive"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["new_path"] == "code/archive/docs"
|
||||||
|
assert response.json()["moved_type"] == "folder"
|
||||||
|
assert (tmp_path / "code" / "archive" / "docs" / "note.txt").exists()
|
||||||
|
assert not (tmp_path / "code" / "docs").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_move_folder_errors(client, tmp_path):
|
||||||
|
(tmp_path / "code" / "docs").mkdir(parents=True)
|
||||||
|
(tmp_path / "code" / "docs" / "nested").mkdir()
|
||||||
|
(tmp_path / "code" / "archive").mkdir(parents=True)
|
||||||
|
(tmp_path / "code" / "archive" / "docs").mkdir()
|
||||||
|
|
||||||
|
into_child = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/docs", "destination_folder": "code/docs/nested"},
|
||||||
|
)
|
||||||
|
assert into_child.status_code == 400
|
||||||
|
|
||||||
|
name_conflict = client.post(
|
||||||
|
"/api/file-move",
|
||||||
|
json={"source_path": "code/docs", "destination_folder": "code/archive"},
|
||||||
|
)
|
||||||
|
assert name_conflict.status_code == 409
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_escape_is_blocked(client):
|
||||||
|
response = client.post("/api/file/%2E%2E/evil.txt", json={"content": "nope"})
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_folder_create_and_delete(client, tmp_path):
|
||||||
|
create = client.post("/api/folder/new/code/new-folder", json={"path": "ignored"})
|
||||||
|
assert create.status_code == 200
|
||||||
|
assert (tmp_path / "code" / "new-folder").is_dir()
|
||||||
|
|
||||||
|
exists = client.post("/api/folder/new/code/new-folder", json={"path": "ignored"})
|
||||||
|
assert exists.status_code == 400
|
||||||
|
|
||||||
|
delete = client.delete("/api/folder/code/new-folder")
|
||||||
|
assert delete.status_code == 200
|
||||||
|
assert not (tmp_path / "code" / "new-folder").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_folder_collapses_duplicate_scoped_prefix(client, tmp_path):
|
||||||
|
create = client.post("/api/folder/new/prompts/prompts/drafts", json={"path": "ignored"})
|
||||||
|
assert create.status_code == 200
|
||||||
|
assert (tmp_path / "prompts" / "drafts").is_dir()
|
||||||
|
assert not (tmp_path / "prompts" / "prompts").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_folder_delete_errors(client, tmp_path):
|
||||||
|
missing = client.delete("/api/folder/code/missing")
|
||||||
|
assert missing.status_code == 404
|
||||||
|
|
||||||
|
(tmp_path / "code").mkdir()
|
||||||
|
(tmp_path / "code" / "file.txt").write_text("x", encoding="utf-8")
|
||||||
|
not_dir = client.delete("/api/folder/code/file.txt")
|
||||||
|
assert not_dir.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_run_output_and_stop(client, tmp_path):
|
||||||
|
script = tmp_path / "demo.py"
|
||||||
|
script.write_text(
|
||||||
|
"import time\nprint('hello')\ntime.sleep(0.3)\nprint('bye')\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
run = client.post("/api/python/run", json={"file_path": "demo.py"})
|
||||||
|
assert run.status_code == 200
|
||||||
|
|
||||||
|
# Eventually process is running and output starts arriving.
|
||||||
|
output = {"running": True, "lines": []}
|
||||||
|
for _ in range(30):
|
||||||
|
output = client.get("/api/python/output", params={"offset": 0}).json()
|
||||||
|
if output["lines"]:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
assert output["lines"]
|
||||||
|
assert output["lines"][0].startswith("$ ")
|
||||||
|
|
||||||
|
stop = client.post("/api/python/stop")
|
||||||
|
assert stop.status_code == 200
|
||||||
|
|
||||||
|
final = client.get("/api/python/output", params={"offset": 0}).json()
|
||||||
|
assert final["running"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_run_accepts_args_and_logs_command(client, tmp_path):
|
||||||
|
script = tmp_path / "demo_args.py"
|
||||||
|
script.write_text("import sys\nprint('args=' + '|'.join(sys.argv[1:]))\n", encoding="utf-8")
|
||||||
|
|
||||||
|
run = client.post("/api/python/run", json={"file_path": "demo_args.py", "args": ["child-a"]})
|
||||||
|
assert run.status_code == 200
|
||||||
|
|
||||||
|
output = {"running": True, "lines": []}
|
||||||
|
for _ in range(30):
|
||||||
|
output = client.get("/api/python/output", params={"offset": 0}).json()
|
||||||
|
joined = "".join(output.get("lines", []))
|
||||||
|
if "args=child-a" in joined:
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
joined = "".join(output.get("lines", []))
|
||||||
|
assert "demo_args.py child-a" in joined
|
||||||
|
assert "args=child-a" in joined
|
||||||
|
|
||||||
|
client.post("/api/python/stop")
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_run_rejects_missing_and_non_python(client, tmp_path):
|
||||||
|
missing = client.post("/api/python/run", json={"file_path": "missing.py"})
|
||||||
|
assert missing.status_code == 404
|
||||||
|
|
||||||
|
(tmp_path / "note.txt").write_text("hello", encoding="utf-8")
|
||||||
|
wrong_ext = client.post("/api/python/run", json={"file_path": "note.txt"})
|
||||||
|
assert wrong_ext.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_run_rejects_when_already_running(client, tmp_path):
|
||||||
|
script = tmp_path / "long.py"
|
||||||
|
script.write_text("import time\ntime.sleep(1.0)\n", encoding="utf-8")
|
||||||
|
|
||||||
|
first = client.post("/api/python/run", json={"file_path": "long.py"})
|
||||||
|
assert first.status_code == 200
|
||||||
|
|
||||||
|
second = client.post("/api/python/run", json={"file_path": "long.py"})
|
||||||
|
assert second.status_code == 409
|
||||||
|
|
||||||
|
client.post("/api/python/stop")
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_stop_when_nothing_running(client):
|
||||||
|
response = client.post("/api/python/stop")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["message"] == "No running process"
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_completions_returns_results(client):
|
||||||
|
response = client.post(
|
||||||
|
"/api/python/completions",
|
||||||
|
json={
|
||||||
|
"file_path": "scratch.py",
|
||||||
|
"content": "import os\nos.pa",
|
||||||
|
"line": 2,
|
||||||
|
"column": 5,
|
||||||
|
"max_results": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
names = {item["name"] for item in response.json()["completions"]}
|
||||||
|
assert "path" in names
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_completions_bad_position_returns_400(client):
|
||||||
|
response = client.post(
|
||||||
|
"/api/python/completions",
|
||||||
|
json={
|
||||||
|
"file_path": "scratch.py",
|
||||||
|
"content": "x = 1",
|
||||||
|
"line": 99,
|
||||||
|
"column": 1,
|
||||||
|
"max_results": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_scripts_list_and_startup_selection(client, tmp_path):
|
||||||
|
(tmp_path / "code").mkdir()
|
||||||
|
(tmp_path / "code" / "job.py").write_text("print('ok')\n", encoding="utf-8")
|
||||||
|
|
||||||
|
scripts = client.get("/api/python/scripts")
|
||||||
|
assert scripts.status_code == 200
|
||||||
|
assert "code/job.py" in scripts.json()["scripts"]
|
||||||
|
|
||||||
|
set_startup = client.post(
|
||||||
|
"/api/python/startup-script",
|
||||||
|
json={"file_path": "code/job.py"},
|
||||||
|
)
|
||||||
|
assert set_startup.status_code == 200
|
||||||
|
|
||||||
|
startup = client.get("/api/python/startup-script")
|
||||||
|
assert startup.status_code == 200
|
||||||
|
assert startup.json()["file_path"] == "code/job.py"
|
||||||
91
tests/test_browser.py
Normal file
91
tests/test_browser.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import importlib
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _is_port_open(port: int) -> bool:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.settimeout(0.3)
|
||||||
|
return sock.connect_ex(("127.0.0.1", port)) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_browser_create_file_not_forced_into_new(tmp_path):
|
||||||
|
playwright = pytest.importorskip("playwright.sync_api")
|
||||||
|
sync_playwright = playwright.sync_playwright
|
||||||
|
|
||||||
|
editor_dir = Path(__file__).resolve().parents[1]
|
||||||
|
port = 8123
|
||||||
|
env = dict(**__import__("os").environ, WORKSPACE_ROOT=str(tmp_path))
|
||||||
|
|
||||||
|
server = subprocess.Popen(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
"uvicorn",
|
||||||
|
"app:app",
|
||||||
|
"--app-dir",
|
||||||
|
"src",
|
||||||
|
"--host",
|
||||||
|
"127.0.0.1",
|
||||||
|
"--port",
|
||||||
|
str(port),
|
||||||
|
],
|
||||||
|
cwd=str(editor_dir),
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
for _ in range(50):
|
||||||
|
if _is_port_open(port):
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
pytest.fail("Server did not start in time")
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
try:
|
||||||
|
browser = p.chromium.launch()
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
pytest.skip(f"Playwright browser not installed: {exc}")
|
||||||
|
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(f"http://127.0.0.1:{port}/editor", wait_until="networkidle")
|
||||||
|
page.click("#new-file-btn")
|
||||||
|
page.fill("#new-filename", "browser-test.txt")
|
||||||
|
page.click("#create-file-btn")
|
||||||
|
page.wait_for_timeout(500)
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
assert (tmp_path / "browser-test.txt").exists()
|
||||||
|
assert not (tmp_path / "new" / "browser-test.txt").exists()
|
||||||
|
finally:
|
||||||
|
server.terminate()
|
||||||
|
try:
|
||||||
|
server.wait(timeout=3)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
server.kill()
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_file_uses_api_file_route(tmp_path):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.main as main
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
importlib.reload(main)
|
||||||
|
client = TestClient(main.app)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/file/code/routing-check.txt",
|
||||||
|
json={"content": "ok"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert (tmp_path / "code" / "routing-check.txt").exists()
|
||||||
|
assert not (tmp_path / "new" / "routing-check.txt").exists()
|
||||||
36
tests/test_completion_api.py
Normal file
36
tests/test_completion_api.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_completion_returns_basic_suggestions(tmp_path):
|
||||||
|
editor_dir = Path(__file__).resolve().parents[1]
|
||||||
|
src_dir = editor_dir / "src"
|
||||||
|
if str(src_dir) not in sys.path:
|
||||||
|
sys.path.insert(0, str(src_dir))
|
||||||
|
|
||||||
|
os.environ["WORKSPACE_ROOT"] = str(tmp_path)
|
||||||
|
import app as editor_app
|
||||||
|
|
||||||
|
editor_app = importlib.reload(editor_app)
|
||||||
|
editor_app.WORKSPACE_ROOT = tmp_path
|
||||||
|
client = TestClient(editor_app.app)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/python/completions",
|
||||||
|
json={
|
||||||
|
"file_path": "example.py",
|
||||||
|
"content": "import os\nos.pa",
|
||||||
|
"line": 2,
|
||||||
|
"column": 5,
|
||||||
|
"max_results": 20,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
names = [item["name"] for item in body["completions"]]
|
||||||
|
assert "path" in names
|
||||||
237
tests/test_internal.py
Normal file
237
tests/test_internal.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import builtins
|
||||||
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_env_file_sets_missing_keys_only(tmp_path, monkeypatch):
|
||||||
|
import editor_app.config as config
|
||||||
|
|
||||||
|
env_file = tmp_path / ".env"
|
||||||
|
env_file.write_text(
|
||||||
|
"# comment\nFOO=bar\nBAZ='quoted'\nEXISTING=from_file\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setenv("EXISTING", "kept")
|
||||||
|
monkeypatch.delenv("FOO", raising=False)
|
||||||
|
monkeypatch.delenv("BAZ", raising=False)
|
||||||
|
|
||||||
|
config.load_env_file(env_file)
|
||||||
|
|
||||||
|
assert __import__("os").environ["FOO"] == "bar"
|
||||||
|
assert __import__("os").environ["BAZ"] == "quoted"
|
||||||
|
assert __import__("os").environ["EXISTING"] == "kept"
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_env_file_ignores_missing_file(tmp_path):
|
||||||
|
import editor_app.config as config
|
||||||
|
|
||||||
|
config.load_env_file(tmp_path / "missing.env")
|
||||||
|
|
||||||
|
|
||||||
|
class _ProcNoStdout:
|
||||||
|
stdout = None
|
||||||
|
|
||||||
|
|
||||||
|
class _ProcWithLines:
|
||||||
|
def __init__(self):
|
||||||
|
self.stdout = iter(["line1\n", "line2\n"])
|
||||||
|
|
||||||
|
|
||||||
|
class _ProcWait:
|
||||||
|
def __init__(self, code=0):
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
return self.code
|
||||||
|
|
||||||
|
|
||||||
|
class _ProcStop:
|
||||||
|
def __init__(self):
|
||||||
|
self.returncode = 0
|
||||||
|
self.terminated = False
|
||||||
|
self.killed = False
|
||||||
|
self.wait_calls = 0
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.terminated = True
|
||||||
|
|
||||||
|
def wait(self, timeout=None):
|
||||||
|
self.wait_calls += 1
|
||||||
|
if self.wait_calls == 1:
|
||||||
|
raise subprocess.TimeoutExpired("cmd", timeout)
|
||||||
|
return self.returncode
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
self.killed = True
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_runner_stream_helpers_and_wait():
|
||||||
|
import editor_app.services.python_runner as runner
|
||||||
|
|
||||||
|
with runner.python_runner.lock:
|
||||||
|
runner.python_runner.output_lines = []
|
||||||
|
|
||||||
|
runner.stream_process_output(_ProcNoStdout())
|
||||||
|
runner.stream_process_output(_ProcWithLines())
|
||||||
|
|
||||||
|
with runner.python_runner.lock:
|
||||||
|
assert runner.python_runner.output_lines[-2:] == ["line1\n", "line2\n"]
|
||||||
|
|
||||||
|
proc = _ProcWait(code=7)
|
||||||
|
runner.wait_for_process(proc)
|
||||||
|
with runner.python_runner.lock:
|
||||||
|
assert runner.python_runner.return_code == 7
|
||||||
|
assert runner.python_runner.running is False
|
||||||
|
assert runner.python_runner.process is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_runner_run_failure_raises_http(monkeypatch, tmp_path):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.services.python_runner as runner
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
|
||||||
|
def _boom(*args, **kwargs):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
monkeypatch.setattr(runner.subprocess, "Popen", _boom)
|
||||||
|
|
||||||
|
with pytest.raises(HTTPException) as exc:
|
||||||
|
runner.run_python_file(tmp_path / "x.py", "x.py")
|
||||||
|
|
||||||
|
assert exc.value.status_code == 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_runner_stop_kill_path():
|
||||||
|
import editor_app.services.python_runner as runner
|
||||||
|
|
||||||
|
proc = _ProcStop()
|
||||||
|
with runner.python_runner.lock:
|
||||||
|
runner.python_runner.process = proc
|
||||||
|
runner.python_runner.output_lines = []
|
||||||
|
runner.python_runner.running = True
|
||||||
|
|
||||||
|
message = runner.stop_python_process()
|
||||||
|
assert message == "Python process stopped"
|
||||||
|
assert proc.terminated is True
|
||||||
|
assert proc.killed is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_completions_import_error_path(tmp_path, monkeypatch):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.main as main
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
importlib.reload(main)
|
||||||
|
client = TestClient(main.app)
|
||||||
|
|
||||||
|
real_import = builtins.__import__
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name == "jedi":
|
||||||
|
raise ImportError("forced")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
monkeypatch.setattr(builtins, "__import__", fake_import)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/python/completions",
|
||||||
|
json={
|
||||||
|
"file_path": "scratch.py",
|
||||||
|
"content": "x = 1",
|
||||||
|
"line": 1,
|
||||||
|
"column": 1,
|
||||||
|
"max_results": 10,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_websocket_output_status_message(tmp_path):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.main as main
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
importlib.reload(main)
|
||||||
|
|
||||||
|
with TestClient(main.app) as client:
|
||||||
|
with client.websocket_connect("/api/python/ws/output") as ws:
|
||||||
|
payload = ws.receive_json()
|
||||||
|
assert "lines" in payload
|
||||||
|
assert "running" in payload
|
||||||
|
assert payload["running"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_app_startup_creates_lib(tmp_path):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.main as main
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
importlib.reload(main)
|
||||||
|
|
||||||
|
assert not (tmp_path / "lib").exists()
|
||||||
|
with TestClient(main.app):
|
||||||
|
pass
|
||||||
|
assert (tmp_path / "lib").is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
def test_python_runner_helpers_and_startup_paths(tmp_path, monkeypatch):
|
||||||
|
import editor_app.config as config
|
||||||
|
import editor_app.services.python_runner as runner
|
||||||
|
|
||||||
|
config.WORKSPACE_ROOT = tmp_path
|
||||||
|
runner.STARTUP_SCRIPT_FILE = tmp_path / ".connectionmachine_startup_script"
|
||||||
|
(tmp_path / "lib").mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# list_python_scripts should skip hidden paths.
|
||||||
|
(tmp_path / "code").mkdir(exist_ok=True)
|
||||||
|
(tmp_path / "code" / "a.py").write_text("print('x')\n", encoding="utf-8")
|
||||||
|
(tmp_path / ".hidden.py").write_text("print('x')\n", encoding="utf-8")
|
||||||
|
scripts = runner.list_python_scripts()
|
||||||
|
assert "code/a.py" in scripts
|
||||||
|
assert ".hidden.py" not in scripts
|
||||||
|
|
||||||
|
# startup script getters/setters.
|
||||||
|
assert runner.get_startup_script() is None
|
||||||
|
runner.STARTUP_SCRIPT_FILE.write_text("", encoding="utf-8")
|
||||||
|
assert runner.get_startup_script() is None
|
||||||
|
|
||||||
|
missing = tmp_path / "code" / "missing.py"
|
||||||
|
with pytest.raises(HTTPException) as missing_exc:
|
||||||
|
runner.set_startup_script(str(missing.relative_to(tmp_path)))
|
||||||
|
assert missing_exc.value.status_code == 404
|
||||||
|
|
||||||
|
(tmp_path / "code" / "note.txt").write_text("x", encoding="utf-8")
|
||||||
|
with pytest.raises(HTTPException) as nonpy_exc:
|
||||||
|
runner.set_startup_script("code/note.txt")
|
||||||
|
assert nonpy_exc.value.status_code == 400
|
||||||
|
|
||||||
|
selected = runner.set_startup_script("code/a.py")
|
||||||
|
assert selected == "code/a.py"
|
||||||
|
assert runner.get_startup_script() == "code/a.py"
|
||||||
|
|
||||||
|
# run_startup_script_if_configured no-op branches.
|
||||||
|
runner.STARTUP_SCRIPT_FILE.write_text("code/missing.py", encoding="utf-8")
|
||||||
|
runner.run_startup_script_if_configured()
|
||||||
|
runner.STARTUP_SCRIPT_FILE.write_text("code/note.txt", encoding="utf-8")
|
||||||
|
runner.run_startup_script_if_configured()
|
||||||
|
|
||||||
|
# run_startup_script_if_configured should call run_python_file on valid startup script.
|
||||||
|
called = {"count": 0}
|
||||||
|
|
||||||
|
def fake_run(target_path, requested_path):
|
||||||
|
called["count"] += 1
|
||||||
|
assert requested_path == "code/a.py"
|
||||||
|
assert str(target_path).endswith("code/a.py")
|
||||||
|
|
||||||
|
monkeypatch.setattr(runner, "run_python_file", fake_run)
|
||||||
|
runner.STARTUP_SCRIPT_FILE.write_text("code/a.py", encoding="utf-8")
|
||||||
|
runner.run_startup_script_if_configured()
|
||||||
|
assert called["count"] == 1
|
||||||
Reference in New Issue
Block a user