diff --git a/backend/app.py b/backend/app.py index 8c0ae4d..812f896 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,33 +1,30 @@ -from fastapi import FastAPI +import asyncio +from collections.abc import AsyncGenerator +from contextlib import asynccontextmanager -from models import MsgPayload +from fastapi import FastAPI, status -app = FastAPI() -messages_list: dict[int, MsgPayload] = {} +import settings +from trading_journal import db +from trading_journal.dto import TradeCreate, TradeRead + +API_BASE = "/api/v1" + +_db = db.create_database(settings.settings.database_url) -@app.get("/") -def root() -> dict[str, str]: - return {"message": "Hello"} +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa: ARG001 + await asyncio.to_thread(_db.init_db) + try: + yield + finally: + await asyncio.to_thread(_db.dispose) -# About page route -@app.get("/about") -def about() -> dict[str, str]: - return {"message": "This is the about page."} +app = FastAPI(lifespan=lifespan) -# Route to add a message -@app.post("/messages/{msg_name}/") -def add_msg(msg_name: str) -> dict[str, MsgPayload]: - # Generate an ID for the item based on the highest ID in the messages_list - msg_id = max(messages_list.keys()) + 1 if messages_list else 0 - messages_list[msg_id] = MsgPayload(msg_id=msg_id, msg_name=msg_name) - - return {"message": messages_list[msg_id]} - - -# Route to list all messages -@app.get("/messages") -def message_items() -> dict[str, dict[int, MsgPayload]]: - return {"messages:": messages_list} +@app.get(f"{API_BASE}/status") +async def get_status() -> dict[str, str]: + return {"status": "ok"} diff --git a/backend/dev-requirements.txt b/backend/dev-requirements.txt index 0cab2f5..aebd637 100644 --- a/backend/dev-requirements.txt +++ b/backend/dev-requirements.txt @@ -14,12 +14,130 @@ anyio==4.10.0 \ # via # httpx # starlette +argon2-cffi==25.1.0 \ + --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \ + --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + # via passlib +argon2-cffi-bindings==25.1.0 \ + --hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \ + --hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \ + --hash=sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d \ + --hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \ + --hash=sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a \ + --hash=sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f \ + --hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \ + --hash=sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690 \ + --hash=sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584 \ + --hash=sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e \ + --hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \ + --hash=sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f \ + --hash=sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623 \ + --hash=sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b \ + --hash=sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44 \ + --hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \ + --hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \ + --hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \ + --hash=sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6 \ + --hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \ + --hash=sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85 \ + --hash=sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92 \ + --hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \ + --hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a \ + --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \ + --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb + # via argon2-cffi certifi==2025.8.3 \ --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 # via # httpcore # httpx +cffi==2.0.0 \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ + --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ + --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ + --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ + --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ + --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ + --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ + --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ + --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ + --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ + --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ + --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ + --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ + --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ + --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ + --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ + --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ + --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ + --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf + # via argon2-cffi-bindings click==8.2.1 \ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b @@ -112,10 +230,18 @@ packaging==25.0 \ --hash=sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484 \ --hash=sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f # via pytest +passlib[argon2]==1.7.4 \ + --hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \ + --hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04 + # via -r requirements.in pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest +pycparser==2.23 \ + --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ + --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 + # via cffi pydantic==2.11.7 \ --hash=sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db \ --hash=sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b diff --git a/backend/models.py b/backend/models.py deleted file mode 100644 index ca1a882..0000000 --- a/backend/models.py +++ /dev/null @@ -1,7 +0,0 @@ -from typing import Optional -from pydantic import BaseModel - - -class MsgPayload(BaseModel): - msg_id: Optional[int] - msg_name: str diff --git a/backend/requirements.in b/backend/requirements.in index 76cec58..dc105e4 100644 --- a/backend/requirements.in +++ b/backend/requirements.in @@ -3,4 +3,5 @@ uvicorn httpx pyyaml pydantic-settings -sqlmodel \ No newline at end of file +sqlmodel +passlib[argon2] \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt index 18a2164..6131b3f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -14,12 +14,130 @@ anyio==4.10.0 \ # via # httpx # starlette +argon2-cffi==25.1.0 \ + --hash=sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1 \ + --hash=sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741 + # via passlib +argon2-cffi-bindings==25.1.0 \ + --hash=sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99 \ + --hash=sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6 \ + --hash=sha256:21378b40e1b8d1655dd5310c84a40fc19a9aa5e6366e835ceb8576bf0fea716d \ + --hash=sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44 \ + --hash=sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a \ + --hash=sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f \ + --hash=sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2 \ + --hash=sha256:5acb4e41090d53f17ca1110c3427f0a130f944b896fc8c83973219c97f57b690 \ + --hash=sha256:5d588dec224e2a83edbdc785a5e6f3c6cd736f46bfd4b441bbb5aa1f5085e584 \ + --hash=sha256:6dca33a9859abf613e22733131fc9194091c1fa7cb3e131c143056b4856aa47e \ + --hash=sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0 \ + --hash=sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f \ + --hash=sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623 \ + --hash=sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b \ + --hash=sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44 \ + --hash=sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98 \ + --hash=sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500 \ + --hash=sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94 \ + --hash=sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6 \ + --hash=sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d \ + --hash=sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85 \ + --hash=sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92 \ + --hash=sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d \ + --hash=sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a \ + --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \ + --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb + # via argon2-cffi certifi==2025.8.3 \ --hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \ --hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5 # via # httpcore # httpx +cffi==2.0.0 \ + --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ + --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ + --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ + --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ + --hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \ + --hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \ + --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ + --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ + --hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \ + --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ + --hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \ + --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ + --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ + --hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \ + --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ + --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ + --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ + --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ + --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ + --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ + --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ + --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ + --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ + --hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \ + --hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \ + --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ + --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ + --hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \ + --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ + --hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \ + --hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \ + --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ + --hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \ + --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ + --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ + --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ + --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ + --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ + --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ + --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ + --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ + --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ + --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ + --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ + --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ + --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ + --hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \ + --hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \ + --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ + --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ + --hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \ + --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ + --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ + --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ + --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ + --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ + --hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \ + --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ + --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ + --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ + --hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \ + --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ + --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ + --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ + --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ + --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ + --hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \ + --hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \ + --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ + --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ + --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ + --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ + --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ + --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ + --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ + --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ + --hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \ + --hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \ + --hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \ + --hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \ + --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ + --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \ + --hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \ + --hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf + # via argon2-cffi-bindings click==8.2.1 \ --hash=sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202 \ --hash=sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b @@ -104,6 +222,14 @@ idna==3.10 \ # via # anyio # httpx +passlib[argon2]==1.7.4 \ + --hash=sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1 \ + --hash=sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04 + # via -r requirements.in +pycparser==2.23 \ + --hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \ + --hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934 + # via cffi pydantic==2.11.7 \ --hash=sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db \ --hash=sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b diff --git a/backend/settings.py b/backend/settings.py index 2096af8..25ad0dc 100644 --- a/backend/settings.py +++ b/backend/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): port: int = 8000 workers: int = 1 log_level: str = "info" + database_url: str = "sqlite:///:memory:" model_config = ConfigDict(env_file=".env", env_file_encoding="utf-8") diff --git a/backend/tests/test_app.py b/backend/tests/test_app.py new file mode 100644 index 0000000..d6123a6 --- /dev/null +++ b/backend/tests/test_app.py @@ -0,0 +1,18 @@ +from collections.abc import Generator + +import pytest +from fastapi.testclient import TestClient + +from app import API_BASE, app + + +@pytest.fixture +def client() -> Generator[TestClient, None, None]: + with TestClient(app) as client: + yield client + + +def test_get_status(client: TestClient) -> None: + response = client.get(f"{API_BASE}/status") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} diff --git a/backend/tests/test_crud.py b/backend/tests/test_crud.py index 2ba27f7..5eac231 100644 --- a/backend/tests/test_crud.py +++ b/backend/tests/test_crud.py @@ -33,8 +33,11 @@ def engine() -> Generator[Engine, None, None]: @pytest.fixture def session(engine: Engine) -> Generator[Session, None, None]: - with Session(engine) as s: - yield s + session = Session(engine) + try: + yield session + finally: + session.close() def make_user(session: Session, username: str = "testuser") -> int: @@ -45,12 +48,20 @@ def make_user(session: Session, username: str = "testuser") -> int: return user.id -def make_cycle(session: Session, user_id: int, friendly_name: str = "Test Cycle") -> int: +def make_exchange(session: Session, name: str = "NASDAQ") -> int: + exchange = models.Exchanges(name=name, notes="Test exchange") + session.add(exchange) + session.commit() + session.refresh(exchange) + return exchange.id + + +def make_cycle(session: Session, user_id: int, exchange_id: int, friendly_name: str = "Test Cycle") -> int: cycle = models.Cycles( user_id=user_id, friendly_name=friendly_name, symbol="AAPL", - exchange="NASDAQ", + exchange_id=exchange_id, underlying_currency=models.UnderlyingCurrency.USD, status=models.CycleStatus.OPEN, start_date=datetime.now(timezone.utc).date(), @@ -62,11 +73,13 @@ def make_cycle(session: Session, user_id: int, friendly_name: str = "Test Cycle" def make_trade(session: Session, user_id: int, cycle_id: int, friendly_name: str = "Test Trade") -> int: + cycle: models.Cycles = session.get(models.Cycles, cycle_id) + exchange_id = cycle.exchange_id trade = models.Trades( user_id=user_id, friendly_name=friendly_name, symbol="AAPL", - exchange="NASDAQ", + exchange_id=exchange_id, underlying_currency=models.UnderlyingCurrency.USD, trade_type=models.TradeType.LONG_SPOT, trade_strategy=models.TradeStrategy.SPOT, @@ -125,13 +138,13 @@ def _ensure_utc_aware(dt: datetime) -> datetime | None: def test_create_trade_success_with_cycle(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) trade_data = { "user_id": user_id, "friendly_name": "Test Trade", "symbol": "AAPL", - "exchange": "NASDAQ", "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -167,12 +180,13 @@ def test_create_trade_success_with_cycle(session: Session) -> None: def test_create_trade_with_auto_created_cycle(session: Session) -> None: user_id = make_user(session) + exchange_id = make_exchange(session) trade_data = { "user_id": user_id, "friendly_name": "Test Trade with Auto Cycle", "symbol": "AAPL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -210,12 +224,13 @@ def test_create_trade_with_auto_created_cycle(session: Session) -> None: def test_create_trade_missing_required_fields(session: Session) -> None: user_id = make_user(session) + exchange_id = make_exchange(session) base_trade_data = { "user_id": user_id, "friendly_name": "Incomplete Trade", "symbol": "AAPL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -231,12 +246,12 @@ def test_create_trade_missing_required_fields(session: Session) -> None: crud.create_trade(session, trade_data) assert "symbol is required" in str(excinfo.value) - # Missing exchange + # Missing exchange and cycle together trade_data = base_trade_data.copy() - trade_data.pop("exchange", None) + trade_data.pop("exchange_id", None) with pytest.raises(ValueError) as excinfo: crud.create_trade(session, trade_data) - assert "exchange is required" in str(excinfo.value) + assert "exchange_id is required when no cycle is attached" in str(excinfo.value) # Missing underlying_currency trade_data = base_trade_data.copy() @@ -276,12 +291,13 @@ def test_create_trade_missing_required_fields(session: Session) -> None: def test_get_trade_by_id(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) trade_data = { "user_id": user_id, "friendly_name": "Test Trade for Get", "symbol": "AAPL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -314,13 +330,14 @@ def test_get_trade_by_id(session: Session) -> None: def test_get_trade_by_user_id_and_friendly_name(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) friendly_name = "Unique Trade Name" trade_data = { "user_id": user_id, "friendly_name": friendly_name, "symbol": "AAPL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -342,12 +359,13 @@ def test_get_trade_by_user_id_and_friendly_name(session: Session) -> None: def test_get_trades_by_user_id(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) trade_data_1 = { "user_id": user_id, "friendly_name": "Trade One", "symbol": "AAPL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -364,7 +382,7 @@ def test_get_trades_by_user_id(session: Session) -> None: "user_id": user_id, "friendly_name": "Trade Two", "symbol": "GOOGL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.SHORT_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -388,7 +406,8 @@ def test_get_trades_by_user_id(session: Session) -> None: def test_update_trade_note(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) trade_id = make_trade(session, user_id, cycle_id) new_note = "This is an updated note." @@ -405,7 +424,8 @@ def test_update_trade_note(session: Session) -> None: def test_invalidate_trade(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) trade_id = make_trade(session, user_id, cycle_id) invalidated_trade = crud.invalidate_trade(session, trade_id) @@ -421,14 +441,15 @@ def test_invalidate_trade(session: Session) -> None: def test_replace_trade(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id) + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id) old_trade_id = make_trade(session, user_id, cycle_id) new_trade_data = { "user_id": user_id, "friendly_name": "Replaced Trade", "symbol": "MSFT", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "trade_type": models.TradeType.LONG_SPOT, "trade_strategy": models.TradeStrategy.SPOT, @@ -465,11 +486,12 @@ def test_replace_trade(session: Session) -> None: def test_create_cycle(session: Session) -> None: user_id = make_user(session) + exchange_id = make_exchange(session) cycle_data = { "user_id": user_id, "friendly_name": "My First Cycle", "symbol": "GOOGL", - "exchange": "NASDAQ", + "exchange_id": exchange_id, "underlying_currency": models.UnderlyingCurrency.USD, "status": models.CycleStatus.OPEN, "start_date": datetime.now(timezone.utc).date(), @@ -495,7 +517,8 @@ def test_create_cycle(session: Session) -> None: def test_update_cycle(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id, friendly_name="Initial Cycle Name") + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id, friendly_name="Initial Cycle Name") update_data = { "friendly_name": "Updated Cycle Name", @@ -516,7 +539,8 @@ def test_update_cycle(session: Session) -> None: def test_update_cycle_immutable_fields(session: Session) -> None: user_id = make_user(session) - cycle_id = make_cycle(session, user_id, friendly_name="Initial Cycle Name") + exchange_id = make_exchange(session) + cycle_id = make_cycle(session, user_id, exchange_id, friendly_name="Initial Cycle Name") # Attempt to update immutable fields update_data = { diff --git a/backend/tests/test_db_migration.py b/backend/tests/test_db_migration.py index 655cf54..15c7fba 100644 --- a/backend/tests/test_db_migration.py +++ b/backend/tests/test_db_migration.py @@ -36,6 +36,7 @@ def test_run_migrations_0_to_1(monkeypatch: pytest.MonkeyPatch) -> None: "user_id": ("INTEGER", 1, 0), "friendly_name": ("TEXT", 0, 0), "symbol": ("TEXT", 1, 0), + "exchange_id": ("INTEGER", 1, 0), "underlying_currency": ("TEXT", 1, 0), "status": ("TEXT", 1, 0), "funding_source": ("TEXT", 0, 0), @@ -50,9 +51,11 @@ def test_run_migrations_0_to_1(monkeypatch: pytest.MonkeyPatch) -> None: "user_id": ("INTEGER", 1, 0), "friendly_name": ("TEXT", 0, 0), "symbol": ("TEXT", 1, 0), + "exchange_id": ("INTEGER", 1, 0), "underlying_currency": ("TEXT", 1, 0), "trade_type": ("TEXT", 1, 0), "trade_strategy": ("TEXT", 1, 0), + "trade_date": ("DATE", 1, 0), "trade_time_utc": ("DATETIME", 1, 0), "expiry_date": ("DATE", 0, 0), "strike_price_cents": ("INTEGER", 0, 0), @@ -61,6 +64,10 @@ def test_run_migrations_0_to_1(monkeypatch: pytest.MonkeyPatch) -> None: "gross_cash_flow_cents": ("INTEGER", 1, 0), "commission_cents": ("INTEGER", 1, 0), "net_cash_flow_cents": ("INTEGER", 1, 0), + "is_invalidated": ("BOOLEAN", 1, 0), + "invalidated_at": ("DATETIME", 0, 0), + "replaced_by_trade_id": ("INTEGER", 0, 0), + "notes": ("TEXT", 0, 0), "cycle_id": ("INTEGER", 0, 0), }, "sessions": { @@ -80,10 +87,17 @@ def test_run_migrations_0_to_1(monkeypatch: pytest.MonkeyPatch) -> None: "trades": [ {"table": "cycles", "from": "cycle_id", "to": "id"}, {"table": "users", "from": "user_id", "to": "id"}, + {"table": "exchanges", "from": "exchange_id", "to": "id"}, ], "cycles": [ {"table": "users", "from": "user_id", "to": "id"}, + {"table": "exchanges", "from": "exchange_id", "to": "id"}, ], + "sessions": [ + {"table": "users", "from": "user_id", "to": "id"}, + ], + "users": [], + "exchanges": [], } with engine.connect() as conn: diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py deleted file mode 100644 index b4dd74c..0000000 --- a/backend/tests/test_main.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest -from fastapi.testclient import TestClient - -from app import app - - -@pytest.fixture -def client(): - with TestClient(app) as client: - yield client - - -def test_home_route(client): - response = client.get("/") - assert response.status_code == 200 - assert response.json() == {"message": "Hello"} - - -def test_about_route(client): - response = client.get("/about") - assert response.status_code == 200 - assert response.json() == {"message": "This is the about page."} diff --git a/backend/tests/test_security.py b/backend/tests/test_security.py new file mode 100644 index 0000000..cab62d7 --- /dev/null +++ b/backend/tests/test_security.py @@ -0,0 +1,4 @@ +from trading_journal import security + +def test_hash_password() -> None: + plain = "password" diff --git a/backend/trading_journal/crud.py b/backend/trading_journal/crud.py index 37973ce..5ce6fb4 100644 --- a/backend/trading_journal/crud.py +++ b/backend/trading_journal/crud.py @@ -35,10 +35,11 @@ def create_trade(session: Session, trade_data: Mapping) -> models.Trades: data = dict(trade_data) allowed = {c.name for c in models.Trades.__table__.columns} payload = {k: v for k, v in data.items() if k in allowed} + cycle_id = payload.get("cycle_id") if "symbol" not in payload: raise ValueError("symbol is required") - if "exchange" not in payload: - raise ValueError("exchange is required") + if "exchange_id" not in payload and cycle_id is None: + raise ValueError("exchange_id is required when no cycle is attached") if "underlying_currency" not in payload: raise ValueError("underlying_currency is required") payload["underlying_currency"] = _check_enum(models.UnderlyingCurrency, payload["underlying_currency"], "underlying_currency") @@ -54,7 +55,6 @@ def create_trade(session: Session, trade_data: Mapping) -> models.Trades: payload["trade_time_utc"] = now if "trade_date" not in payload or payload.get("trade_date") is None: payload["trade_date"] = payload["trade_time_utc"].date() - cycle_id = payload.get("cycle_id") user_id = payload.get("user_id") if "quantity" not in payload: raise ValueError("quantity is required") @@ -76,7 +76,7 @@ def create_trade(session: Session, trade_data: Mapping) -> models.Trades: c_payload = { "user_id": user_id, "symbol": payload["symbol"], - "exchange": payload["exchange"], + "exchange_id": payload["exchange_id"], "underlying_currency": payload["underlying_currency"], "friendly_name": "Auto-created Cycle by trade " + payload.get("friendly_name", ""), "status": models.CycleStatus.OPEN, @@ -89,8 +89,11 @@ def create_trade(session: Session, trade_data: Mapping) -> models.Trades: # If cycle_id provided, validate existence and ownership if cycle_id is not None: cycle = session.get(models.Cycles, cycle_id) + if cycle is None: raise ValueError("cycle_id does not exist") + payload.pop("exchange_id", None) # ignore exchange_id if provided; use cycle's exchange_id + payload["exchange_id"] = cycle.exchange_id if cycle.user_id != user_id: raise ValueError("cycle.user_id does not match trade.user_id") @@ -187,8 +190,8 @@ def create_cycle(session: Session, cycle_data: Mapping) -> models.Cycles: raise ValueError("user_id is required") if "symbol" not in payload: raise ValueError("symbol is required") - if "exchange" not in payload: - raise ValueError("exchange is required") + if "exchange_id" not in payload: + raise ValueError("exchange_id is required") if "underlying_currency" not in payload: raise ValueError("underlying_currency is required") payload["underlying_currency"] = _check_enum(models.UnderlyingCurrency, payload["underlying_currency"], "underlying_currency") diff --git a/backend/trading_journal/db.py b/backend/trading_journal/db.py index d952a88..039727c 100644 --- a/backend/trading_journal/db.py +++ b/backend/trading_journal/db.py @@ -58,7 +58,8 @@ class Database: event.listen(self._engine, "connect", _enable_sqlite_pragmas) def init_db(self) -> None: - db_migration.run_migrations(self._engine) + # db_migration.run_migrations(self._engine) + pass def get_session(self) -> Generator[Session, None, None]: session = Session(self._engine) diff --git a/backend/trading_journal/dto.py b/backend/trading_journal/dto.py index b372474..1a9d478 100644 --- a/backend/trading_journal/dto.py +++ b/backend/trading_journal/dto.py @@ -14,8 +14,43 @@ class TradeBase(SQLModel): user_id: int friendly_name: str | None symbol: str + exchange: str underlying_currency: UnderlyingCurrency trade_type: TradeType trade_strategy: TradeStrategy trade_date: date trade_time_utc: datetime + quantity: int + price_cents: int + gross_cash_flow_cents: int + commission_cents: int + net_cash_flow_cents: int + notes: str | None + cycle_id: int | None = None + + +class TradeCreate(TradeBase): + expiry_date: date | None = None + strike_price_cents: int | None = None + is_invalidated: bool = False + invalidated_at: datetime | None = None + replaced_by_trade_id: int | None = None + + +class TradeRead(TradeBase): + id: int + is_invalidated: bool + invalidated_at: datetime | None + + +class UserBase(SQLModel): + username: str + is_active: bool = True + + +class UserCreate(UserBase): + password: str + + +class UserRead(UserBase): + id: int diff --git a/backend/trading_journal/models.py b/backend/trading_journal/models.py index 6659361..0238a81 100644 --- a/backend/trading_journal/models.py +++ b/backend/trading_journal/models.py @@ -72,7 +72,8 @@ class Trades(SQLModel, table=True): # allow null while user may omit friendly_name; uniqueness enforced per-user by constraint friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) - exchange: str = Field(sa_column=Column(Text, nullable=False)) + exchange_id: int = Field(foreign_key="exchanges.id", nullable=False, index=True) + exchange: "Exchanges" = Relationship(back_populates="trades") underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) trade_type: TradeType = Field(sa_column=Column(Text, nullable=False)) trade_strategy: TradeStrategy = Field(sa_column=Column(Text, nullable=False)) @@ -101,7 +102,8 @@ class Cycles(SQLModel, table=True): user_id: int = Field(foreign_key="users.id", nullable=False, index=True) friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) - exchange: str = Field(sa_column=Column(Text, nullable=False)) + exchange_id: int = Field(foreign_key="exchanges.id", nullable=False, index=True) + exchange: "Exchanges" = Relationship(back_populates="cycles") underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) status: CycleStatus = Field(sa_column=Column(Text, nullable=False)) funding_source: FundingSource = Field(sa_column=Column(Text, nullable=True)) @@ -113,6 +115,15 @@ class Cycles(SQLModel, table=True): trades: list["Trades"] = Relationship(back_populates="cycle") +class Exchanges(SQLModel, table=True): + __tablename__ = "exchanges" + id: int | None = Field(default=None, primary_key=True) + name: str = Field(sa_column=Column(Text, nullable=False, unique=True)) + notes: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) + trades: list["Trades"] = Relationship(back_populates="exchange") + cycles: list["Cycles"] = Relationship(back_populates="exchange") + + class Users(SQLModel, table=True): __tablename__ = "users" id: int | None = Field(default=None, primary_key=True) diff --git a/backend/trading_journal/models_v1.py b/backend/trading_journal/models_v1.py index 6659361..0238a81 100644 --- a/backend/trading_journal/models_v1.py +++ b/backend/trading_journal/models_v1.py @@ -72,7 +72,8 @@ class Trades(SQLModel, table=True): # allow null while user may omit friendly_name; uniqueness enforced per-user by constraint friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) - exchange: str = Field(sa_column=Column(Text, nullable=False)) + exchange_id: int = Field(foreign_key="exchanges.id", nullable=False, index=True) + exchange: "Exchanges" = Relationship(back_populates="trades") underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) trade_type: TradeType = Field(sa_column=Column(Text, nullable=False)) trade_strategy: TradeStrategy = Field(sa_column=Column(Text, nullable=False)) @@ -101,7 +102,8 @@ class Cycles(SQLModel, table=True): user_id: int = Field(foreign_key="users.id", nullable=False, index=True) friendly_name: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) symbol: str = Field(sa_column=Column(Text, nullable=False)) - exchange: str = Field(sa_column=Column(Text, nullable=False)) + exchange_id: int = Field(foreign_key="exchanges.id", nullable=False, index=True) + exchange: "Exchanges" = Relationship(back_populates="cycles") underlying_currency: UnderlyingCurrency = Field(sa_column=Column(Text, nullable=False)) status: CycleStatus = Field(sa_column=Column(Text, nullable=False)) funding_source: FundingSource = Field(sa_column=Column(Text, nullable=True)) @@ -113,6 +115,15 @@ class Cycles(SQLModel, table=True): trades: list["Trades"] = Relationship(back_populates="cycle") +class Exchanges(SQLModel, table=True): + __tablename__ = "exchanges" + id: int | None = Field(default=None, primary_key=True) + name: str = Field(sa_column=Column(Text, nullable=False, unique=True)) + notes: str | None = Field(default=None, sa_column=Column(Text, nullable=True)) + trades: list["Trades"] = Relationship(back_populates="exchange") + cycles: list["Cycles"] = Relationship(back_populates="exchange") + + class Users(SQLModel, table=True): __tablename__ = "users" id: int | None = Field(default=None, primary_key=True) diff --git a/backend/trading_journal/security.py b/backend/trading_journal/security.py new file mode 100644 index 0000000..edb32ce --- /dev/null +++ b/backend/trading_journal/security.py @@ -0,0 +1,11 @@ +from passlib.context import CryptContext + +pwd_ctx = CryptContext(schemes=["argon2"], deprecated="auto") + + +def hash_password(plain: str) -> str: + return pwd_ctx.hash(plain) + +def verify_password(plain: str, hashed: str) -> bool: + return pwd_ctx.verify(plain, hashed) +