diff --git a/poetry.lock b/poetry.lock index 128fbe0..fd8dc23 100644 --- a/poetry.lock +++ b/poetry.lock @@ -43,6 +43,21 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] +[[package]] +name = "authlib" +version = "1.6.6" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd"}, + {file = "authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e"}, +] + +[package.dependencies] +cryptography = "*" + [[package]] name = "babel" version = "2.17.0" @@ -90,6 +105,104 @@ files = [ {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, ] +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "cfgv" version = "3.5.0" @@ -253,6 +366,30 @@ files = [ ] markers = {main = "sys_platform == \"win32\" or platform_system == \"Windows\""} +[[package]] +name = "coreason-identity" +version = "0.4.2" +description = "Decoupled authentication middleware, abstracting OIDC and OAuth2 protocols from the main application." +optional = false +python-versions = "<3.15,>=3.11" +groups = ["main"] +files = [ + {file = "coreason_identity-0.4.2-py3-none-any.whl", hash = "sha256:cdf141ab9e7d6b789795e5b79f9ac5458dd8ef6f5e8f400fb7570b4254c10499"}, + {file = "coreason_identity-0.4.2.tar.gz", hash = "sha256:efceb13345994b575b90ecd51d9307508d88c471ee18df1e15e74a6c5dc94ab8"}, +] + +[package.dependencies] +aiofiles = ">=25.1.0,<26.0.0" +anyio = ">=4.12.1,<5.0.0" +authlib = ">=1.6.6,<2.0.0" +email-validator = ">=2.3.0,<3.0.0" +httpx = ">=0.28.1,<0.29.0" +loguru = ">=0.7.2,<0.8.0" +opentelemetry-api = ">=1.39.1,<2.0.0" +pydantic = ">=2.12.5,<3.0.0" +pydantic-settings = ">=2.12.0,<3.0.0" +types-aiofiles = ">=25.1.0,<26.0.0" + [[package]] name = "coverage" version = "7.13.1" @@ -358,6 +495,78 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "cryptography" +version = "46.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +files = [ + {file = "cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616"}, + {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0"}, + {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0"}, + {file = "cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5"}, + {file = "cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b"}, + {file = "cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f"}, + {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82"}, + {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c"}, + {file = "cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061"}, + {file = "cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7"}, + {file = "cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019"}, + {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4"}, + {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b"}, + {file = "cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc"}, + {file = "cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3"}, + {file = "cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "distlib" version = "0.4.0" @@ -382,6 +591,43 @@ files = [ {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + +[[package]] +name = "email-validator" +version = "2.3.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, + {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "filelock" version = "3.20.3" @@ -501,6 +747,30 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] +[[package]] +name = "importlib-metadata" +version = "8.7.1" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +perf = ["ipython"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + [[package]] name = "iniconfig" version = "2.3.0" @@ -996,6 +1266,22 @@ datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] realtime = ["websockets (>=13,<16)"] voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, + {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, +] + +[package.dependencies] +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" + [[package]] name = "packaging" version = "25.0" @@ -1094,6 +1380,19 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -1250,6 +1549,30 @@ files = [ [package.dependencies] typing-extensions = ">=4.14.1" +[[package]] +name = "pydantic-settings" +version = "2.12.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809"}, + {file = "pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" +typing-inspection = ">=0.4.0" + +[package.extras] +aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pygments" version = "2.19.2" @@ -1360,6 +1683,21 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.2.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pyyaml" version = "6.0.3" @@ -1833,7 +2171,27 @@ files = [ [package.extras] dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + [metadata] lock-version = "2.1" python-versions = ">=3.12, <3.15" -content-hash = "0fcb8559e0f59a4650ca8f299c5654477f9c61e0e7eb9e9179828b5dc599a3e1" +content-hash = "56a22c8b2929e53a2b95e407c0b74de66fcb440f6cd09a31ae5e3e8b5e9b19ea" diff --git a/pyproject.toml b/pyproject.toml index a607580..de8317f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "coreason_optimizer" -version = "0.1.0" +version = "0.2.0" description = "coreason-optimizer" authors = ["Gowtham A Rao "] license = "Prosperity-3.0" @@ -16,10 +16,11 @@ openai = "^1.50" click = "^8.1.7" numpy = "^2.1" scikit-learn = "^1.5.2" -anyio = "*" +anyio = "^4.12.1" httpx = "*" aiofiles = "*" types-aiofiles = "*" +coreason-identity = "^0.4.1" [tool.poetry.scripts] coreason-opt = "coreason_optimizer.main:cli" diff --git a/src/coreason_optimizer/__init__.py b/src/coreason_optimizer/__init__.py index f7fffca..68b0201 100644 --- a/src/coreason_optimizer/__init__.py +++ b/src/coreason_optimizer/__init__.py @@ -18,7 +18,7 @@ from coreason_optimizer.core.config import OptimizerConfig from coreason_optimizer.core.interfaces import PromptOptimizer -__version__ = "0.1.0" +__version__ = "0.2.0" __author__ = "Gowtham A Rao" __email__ = "gowtham.rao@coreason.ai" diff --git a/src/coreason_optimizer/core/client.py b/src/coreason_optimizer/core/client.py index e6480db..25d5234 100644 --- a/src/coreason_optimizer/core/client.py +++ b/src/coreason_optimizer/core/client.py @@ -16,10 +16,12 @@ """ import os +import uuid from typing import Any, Optional import anyio import httpx +from coreason_identity.models import UserContext from openai import AsyncOpenAI from coreason_optimizer.core.budget import BudgetManager @@ -462,3 +464,106 @@ def embed(self, texts: list[str], model: str | None = None) -> EmbeddingResponse response = self.provider.embed(texts, model) self.budget_manager.consume(response.usage) return response + + +class OptimizationClient: + """ + Client for managing optimization studies with identity awareness. + + This client serves as a centralized manager for optimization studies, + ensuring all operations are audited and authorized against a UserContext. + """ + + def __init__(self) -> None: + # In-memory store for studies (simulating backend) + self._studies: dict[str, dict[str, Any]] = {} + + def register_study(self, study_name: str, *, context: UserContext) -> str: + """ + Register a new optimization study. + + Args: + study_name: The name of the study. + context: The user context authorizing this operation. + + Returns: + The study ID. + + Raises: + ValueError: If context is missing. + """ + if context is None: + raise ValueError("UserContext is required.") + + # Simulate study creation + study_id = f"study_{uuid.uuid4().hex[:8]}" + self._studies[study_id] = { + "name": study_name, + "owner": context.user_id, + "trials": [], + } + + # Audit the operation + # Note: context.user_id is a string in coreason-identity 0.4.x, not SecretStr. + # We log it as the authenticated user identifier. + logger.info( + "Registering optimization study", + user_id=context.user_id, + study_name=study_name, + study_id=study_id, + ) + return study_id + + def get_suggestion(self, study_id: str, *, context: UserContext) -> dict[str, Any]: + """ + Get the next parameter suggestion for a study. + + Args: + study_id: The ID of the study. + context: The user context authorizing this operation. + + Returns: + A dictionary of suggested parameters. + + Raises: + ValueError: If context is missing. + """ + if context is None: + raise ValueError("UserContext is required.") + + # Verify access (simple check) + # In a real system, we would check RLS/permissions here. + + logger.debug( + "Requesting parameter suggestion", + user_id=context.user_id, + study_id=str(study_id), + ) + + # Return a dummy suggestion or based on prior trials (simulated) + return {"param_a": 0.1, "param_b": "strategy_v1"} + + def report_metric(self, study_id: str, metric: float, *, context: UserContext) -> None: + """ + Report a metric for a trial. + + Args: + study_id: The ID of the study. + metric: The metric value. + context: The user context authorizing this operation. + + Raises: + ValueError: If context is missing. + """ + if context is None: + raise ValueError("UserContext is required.") + + if study_id in self._studies: + self._studies[study_id]["trials"].append({"metric": metric, "user": context.user_id}) + + logger.debug( + "Reporting metric", + user_id=context.user_id, + study_id=study_id, + metric=metric, + ) diff --git a/src/coreason_optimizer/data/loader.py b/src/coreason_optimizer/data/loader.py index 70a1bf2..81b26b7 100644 --- a/src/coreason_optimizer/data/loader.py +++ b/src/coreason_optimizer/data/loader.py @@ -21,7 +21,10 @@ from collections.abc import Iterator from pathlib import Path +from coreason_identity.models import UserContext + from coreason_optimizer.core.models import TrainingExample +from coreason_optimizer.utils.logger import logger class Dataset: @@ -46,7 +49,14 @@ def __iter__(self) -> Iterator[TrainingExample]: return iter(self.examples) @classmethod - def from_csv(cls, filepath: str | Path, input_cols: list[str], reference_col: str) -> "Dataset": + def from_csv( + cls, + filepath: str | Path, + input_cols: list[str], + reference_col: str, + *, + context: UserContext, + ) -> "Dataset": """ Load a dataset from a CSV file. @@ -54,17 +64,28 @@ def from_csv(cls, filepath: str | Path, input_cols: list[str], reference_col: st filepath: Path to the CSV file. input_cols: List of column names to treat as inputs. reference_col: Column name to treat as the reference output. + context: The user context authorizing this operation. Returns: A Dataset instance. Raises: FileNotFoundError: If the file does not exist. + ValueError: If context is missing. """ + if context is None: + raise ValueError("UserContext is required.") + path = Path(filepath) if not path.exists(): raise FileNotFoundError(f"File not found: {path}") + logger.info( + "Loading dataset from CSV", + user_id=context.user_id, + filepath=str(path), + ) + examples = [] with path.open("r", encoding="utf-8") as f: reader = csv.DictReader(f) @@ -88,7 +109,7 @@ def from_csv(cls, filepath: str | Path, input_cols: list[str], reference_col: st return cls(examples) @classmethod - def from_jsonl(cls, filepath: str | Path) -> "Dataset": + def from_jsonl(cls, filepath: str | Path, *, context: UserContext) -> "Dataset": """ Load a dataset from a JSONL file. @@ -99,17 +120,28 @@ def from_jsonl(cls, filepath: str | Path) -> "Dataset": Args: filepath: Path to the JSONL file. + context: The user context authorizing this operation. Returns: A Dataset instance. Raises: FileNotFoundError: If the file does not exist. + ValueError: If context is missing. """ + if context is None: + raise ValueError("UserContext is required.") + path = Path(filepath) if not path.exists(): raise FileNotFoundError(f"File not found: {path}") + logger.info( + "Loading dataset from JSONL", + user_id=context.user_id, + filepath=str(path), + ) + examples = [] with path.open("r", encoding="utf-8") as f: for line in f: diff --git a/src/coreason_optimizer/main.py b/src/coreason_optimizer/main.py index 7f4b777..88ed32a 100644 --- a/src/coreason_optimizer/main.py +++ b/src/coreason_optimizer/main.py @@ -19,8 +19,9 @@ from pathlib import Path import click +from coreason_identity.models import UserContext -from coreason_optimizer.core.client import OpenAIClient, OpenAIEmbeddingClient +from coreason_optimizer.core.client import OpenAIClient, OpenAIEmbeddingClient, OptimizationClient from coreason_optimizer.core.config import OptimizerConfig from coreason_optimizer.core.formatter import format_prompt from coreason_optimizer.core.interfaces import PromptOptimizer @@ -29,6 +30,7 @@ from coreason_optimizer.data.loader import Dataset from coreason_optimizer.strategies.bootstrap import BootstrapFewShot from coreason_optimizer.strategies.mipro import MiproOptimizer +from coreason_optimizer.strategies.selector import StrategySelector from coreason_optimizer.utils.import_utils import load_agent_from_path from coreason_optimizer.utils.logger import logger @@ -88,7 +90,28 @@ def tune( strategy: Optimization strategy to use ('mipro' or 'bootstrap'). selector: Few-shot example selection strategy ('random' or 'semantic'). """ - logger.info(f"Starting optimization for agent: {agent}") + # Create System Context + system_context = UserContext( + user_id="cli-user", + email="cli-user@coreason.ai", + groups=["system"], + claims={"source": "cli"}, + ) + + logger.info( + f"Starting optimization for agent: {agent}", + user_id=system_context.user_id, + ) + + # Initialize Optimization Client (Audit) + opt_client = OptimizationClient() + # We store the study_id but don't strictly use it yet in this version of the optimizer, + # but we register it for audit compliance. + _ = opt_client.register_study(f"opt-{Path(agent).stem}", context=system_context) + + # Validate Strategy + strat_selector = StrategySelector() + strategy = strat_selector.select_strategy(strategy, context=system_context) # Load Agent try: @@ -101,11 +124,13 @@ def tune( try: ds_path = Path(dataset) if ds_path.suffix.lower() == ".jsonl": - full_ds = Dataset.from_jsonl(ds_path) + full_ds = Dataset.from_jsonl(ds_path, context=system_context) elif ds_path.suffix.lower() == ".csv": # Assume reference col is 'reference' and inputs are from construct input_cols = construct.inputs - full_ds = Dataset.from_csv(ds_path, input_cols=input_cols, reference_col="reference") + full_ds = Dataset.from_csv( + ds_path, input_cols=input_cols, reference_col="reference", context=system_context + ) else: raise click.ClickException("Unsupported file format. Use .csv or .jsonl") except Exception as e: @@ -202,16 +227,29 @@ def evaluate(manifest: str, dataset: str, metric: str) -> None: except Exception as e: raise click.ClickException(f"Failed to load manifest: {e}") from e + # Create System Context for Evaluation + system_context = UserContext( + user_id="cli-evaluator", + email="evaluator@coreason.ai", + groups=["system"], + claims={"source": "cli-eval"}, + ) + # Load Dataset try: ds_path = Path(dataset) if ds_path.suffix.lower() == ".jsonl": - eval_ds = Dataset.from_jsonl(ds_path) + eval_ds = Dataset.from_jsonl(ds_path, context=system_context) else: # Fallback for CSV: try to use keys from first few-shot example if available if manifest_obj.few_shot_examples: input_cols = list(manifest_obj.few_shot_examples[0].inputs.keys()) - eval_ds = Dataset.from_csv(ds_path, input_cols=input_cols, reference_col="reference") + eval_ds = Dataset.from_csv( + ds_path, + input_cols=input_cols, + reference_col="reference", + context=system_context, + ) else: raise click.ClickException( "Cannot infer CSV schema for evaluation without few-shot examples in manifest. Use JSONL." diff --git a/src/coreason_optimizer/strategies/selector.py b/src/coreason_optimizer/strategies/selector.py index eae2c5b..7e3ca8a 100644 --- a/src/coreason_optimizer/strategies/selector.py +++ b/src/coreason_optimizer/strategies/selector.py @@ -20,11 +20,13 @@ from abc import ABC, abstractmethod import numpy as np +from coreason_identity.models import UserContext from sklearn.cluster import KMeans from coreason_optimizer.core.interfaces import EmbeddingProvider from coreason_optimizer.core.models import TrainingExample from coreason_optimizer.data.loader import Dataset +from coreason_optimizer.utils.logger import logger class BaseSelector(ABC): @@ -165,3 +167,32 @@ def select(self, trainset: Dataset, k: int = 4) -> list[TrainingExample]: selected_indices.sort() return [trainset[idx] for idx in selected_indices] + + +class StrategySelector: + """Selector for choosing the optimization strategy based on identity and policy.""" + + def select_strategy(self, strategy: str, context: UserContext) -> str: + """ + Select and validate the optimization strategy. + + Args: + strategy: The requested strategy name. + context: The user context. + + Returns: + The authorized strategy name. + + Raises: + ValueError: If context is missing. + """ + if context is None: + raise ValueError("UserContext is required.") + + logger.info( + "Selecting optimization strategy", + user_id=context.user_id, + authorized_strategies=context.claims.get("strategies", "all"), + ) + + return strategy diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..5f84374 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest +from coreason_identity.models import UserContext + + +@pytest.fixture +def mock_context() -> UserContext: + return UserContext( + user_id="test-user", + email="test@example.com", + groups=["test-group"], + claims={"source": "test"}, + ) diff --git a/tests/data/test_loader.py b/tests/data/test_loader.py index e1f83ec..f73283b 100644 --- a/tests/data/test_loader.py +++ b/tests/data/test_loader.py @@ -14,6 +14,7 @@ from pathlib import Path import pytest +from coreason_identity.models import UserContext from coreason_optimizer.core.models import TrainingExample from coreason_optimizer.data.loader import Dataset @@ -30,7 +31,7 @@ def test_dataset_initialization() -> None: assert [e.reference for e in ds] == ["A", "B"] -def test_load_from_csv() -> None: +def test_load_from_csv(mock_context: UserContext) -> None: with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f: writer = csv.writer(f) writer.writerow(["question", "answer", "extra"]) @@ -42,7 +43,7 @@ def test_load_from_csv() -> None: filepath = Path(f.name) try: - ds = Dataset.from_csv(filepath, input_cols=["question"], reference_col="answer") + ds = Dataset.from_csv(filepath, input_cols=["question"], reference_col="answer", context=mock_context) assert len(ds) == 2 assert ds[0].inputs["question"] == "What is 1+1?" assert ds[0].reference == "2" @@ -51,12 +52,12 @@ def test_load_from_csv() -> None: filepath.unlink() -def test_load_from_csv_missing_file() -> None: +def test_load_from_csv_missing_file(mock_context: UserContext) -> None: with pytest.raises(FileNotFoundError): - Dataset.from_csv("non_existent.csv", [], "") + Dataset.from_csv("non_existent.csv", [], "", context=mock_context) -def test_load_from_jsonl() -> None: +def test_load_from_jsonl(mock_context: UserContext) -> None: with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: f.write(json.dumps({"inputs": {"q": "foo"}, "reference": "bar"}) + "\n") f.write(json.dumps({"input": {"q": "baz"}, "output": "qux"}) + "\n") @@ -66,7 +67,7 @@ def test_load_from_jsonl() -> None: filepath = Path(f.name) try: - ds = Dataset.from_jsonl(filepath) + ds = Dataset.from_jsonl(filepath, context=mock_context) assert len(ds) == 3 # Check first format @@ -84,9 +85,9 @@ def test_load_from_jsonl() -> None: filepath.unlink() -def test_load_from_jsonl_missing_file() -> None: +def test_load_from_jsonl_missing_file(mock_context: UserContext) -> None: with pytest.raises(FileNotFoundError): - Dataset.from_jsonl("non_existent.jsonl") + Dataset.from_jsonl("non_existent.jsonl", context=mock_context) def test_split_dataset() -> None: diff --git a/tests/data/test_loader_edge_cases.py b/tests/data/test_loader_edge_cases.py index 1ea3419..0ddd390 100644 --- a/tests/data/test_loader_edge_cases.py +++ b/tests/data/test_loader_edge_cases.py @@ -14,11 +14,12 @@ from pathlib import Path import pytest +from coreason_identity.models import UserContext from coreason_optimizer.data.loader import Dataset -def test_csv_missing_columns() -> None: +def test_csv_missing_columns(mock_context: UserContext) -> None: """Test that missing columns in CSV results in skipped rows or empty inputs.""" with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f: writer = csv.writer(f) @@ -30,27 +31,27 @@ def test_csv_missing_columns() -> None: try: # csv.DictReader behavior: if row has fewer fields than fieldnames, values are None - ds = Dataset.from_csv(filepath, input_cols=["q"], reference_col="a") + ds = Dataset.from_csv(filepath, input_cols=["q"], reference_col="a", context=mock_context) # Should skip because reference 'a' is None assert len(ds) == 0 finally: filepath.unlink() -def test_csv_empty_file() -> None: +def test_csv_empty_file(mock_context: UserContext) -> None: """Test loading an empty CSV file (header only or completely empty).""" with tempfile.NamedTemporaryFile(mode="w", suffix=".csv", delete=False) as f: f.write("q,a\n") # Header only filepath = Path(f.name) try: - ds = Dataset.from_csv(filepath, input_cols=["q"], reference_col="a") + ds = Dataset.from_csv(filepath, input_cols=["q"], reference_col="a", context=mock_context) assert len(ds) == 0 finally: filepath.unlink() -def test_jsonl_malformed_line() -> None: +def test_jsonl_malformed_line(mock_context: UserContext) -> None: """Test that malformed JSON lines cause a failure (or check specific behavior).""" with tempfile.NamedTemporaryFile(mode="w", suffix=".jsonl", delete=False) as f: f.write('{"q": "good", "reference": "ok"}\n') @@ -61,7 +62,7 @@ def test_jsonl_malformed_line() -> None: # Current implementation uses json.loads inside a loop without try-except block for parsing # So it should raise JSONDecodeError with pytest.raises(json.JSONDecodeError): - Dataset.from_jsonl(filepath) + Dataset.from_jsonl(filepath, context=mock_context) finally: filepath.unlink() diff --git a/tests/test_identity_enforcement.py b/tests/test_identity_enforcement.py new file mode 100644 index 0000000..57f4ccf --- /dev/null +++ b/tests/test_identity_enforcement.py @@ -0,0 +1,78 @@ +from pathlib import Path + +import pytest +from coreason_identity.models import UserContext + +from coreason_optimizer.core.client import OptimizationClient +from coreason_optimizer.data.loader import Dataset +from coreason_optimizer.strategies.selector import StrategySelector + + +def test_optimization_client_enforcement() -> None: + client = OptimizationClient() + + # Should fail without context + with pytest.raises(ValueError, match="UserContext is required"): + client.register_study("test-study", context=None) + + # Should pass with context + context = UserContext(user_id="test-user", email="test@example.com") + + study_id = client.register_study("test-study", context=context) + assert study_id.startswith("study_") + + # Test get_suggestion + with pytest.raises(ValueError, match="UserContext is required"): + client.get_suggestion("study_1", context=None) + + sugg = client.get_suggestion("study_1", context=context) + assert isinstance(sugg, dict) + assert "param_a" in sugg + + # Test report_metric + with pytest.raises(ValueError, match="UserContext is required"): + client.report_metric("study_1", 1.0, context=None) + + client.report_metric("study_1", 1.0, context=context) + # Also test reporting to an existing study to cover that branch + client.report_metric(study_id, 0.95, context=context) + + +def test_strategy_selector_enforcement() -> None: + selector = StrategySelector() + + # Should fail without context + with pytest.raises(ValueError, match="UserContext is required"): + selector.select_strategy("mipro", context=None) + + # Should pass with context + context = UserContext(user_id="test-user", email="test@example.com") + strategy = selector.select_strategy("mipro", context=context) + assert strategy == "mipro" + + +def test_dataset_loader_enforcement(tmp_path: Path) -> None: + # Create dummy csv + csv_file = tmp_path / "data.csv" + csv_file.write_text("input,reference\na,b\n", encoding="utf-8") + + # Should fail without context + with pytest.raises(ValueError, match="UserContext is required"): + Dataset.from_csv(csv_file, input_cols=["input"], reference_col="reference", context=None) + + # Should pass with context + context = UserContext(user_id="test-user", email="test@example.com") + ds = Dataset.from_csv(csv_file, input_cols=["input"], reference_col="reference", context=context) + assert len(ds) == 1 + + +def test_dataset_loader_jsonl_enforcement(tmp_path: Path) -> None: + jsonl_file = tmp_path / "data.jsonl" + jsonl_file.write_text('{"input": "a", "output": "b"}\n', encoding="utf-8") + + with pytest.raises(ValueError, match="UserContext is required"): + Dataset.from_jsonl(jsonl_file, context=None) + + context = UserContext(user_id="test-user", email="test@example.com") + ds = Dataset.from_jsonl(jsonl_file, context=context) + assert len(ds) == 1 diff --git a/tests/test_logger_coverage.py b/tests/test_logger_coverage.py index 4ce3e8a..f3c4a34 100644 --- a/tests/test_logger_coverage.py +++ b/tests/test_logger_coverage.py @@ -1,40 +1,36 @@ -# Copyright (c) 2025 CoReason, Inc. -# -# This software is proprietary and dual-licensed. -# Licensed under the Prosperity Public License 3.0 (the "License"). -# A copy of the license is available at https://prosperitylicense.com/versions/3.0.0 -# For details, see the LICENSE file. -# Commercial use beyond a 30-day trial requires a separate license. -# -# Source Code: https://github.com/CoReason-AI/coreason_optimizer - -import importlib import shutil +from importlib import reload from pathlib import Path -import coreason_optimizer.utils.logger +from coreason_optimizer.utils import logger -def test_logger_creates_directory_coverage() -> None: - """ - Test that the logger module creates the 'logs' directory if it doesn't exist. - This is to ensure 100% coverage of the 'if not log_path.exists():' block. - """ - log_path = Path("logs") +def test_logger_directory_creation() -> None: + """Test that the logger creates the logs directory if it doesn't exist.""" + # Ensure any previous logger handlers are removed to close file handles + logger.logger.remove() - # 1. Clean up existing logs directory if possible - # Note: On some systems, this might fail if a file is locked, but on Linux (CI) it usually works. + # Ensure logs directory does NOT exist initially + log_path = Path("logs") if log_path.exists(): - try: - shutil.rmtree(log_path) - except OSError: - # If we can't delete it (e.g. open file), we might skip this test or try another way. - # But for coverage we really want to hit that line. - pass + shutil.rmtree(log_path) + + try: + # Reload the module to trigger the top-level code execution + # We need to reload the submodule directly + reload(logger) - # 2. Reload the module. This should re-execute the module-level code. - importlib.reload(coreason_optimizer.utils.logger) + # Assert directory was created + assert log_path.exists() + assert log_path.is_dir() + + finally: + # Cleanup (optional, but good practice) + # Remove the handler again to release the file lock + logger.logger.remove() + + if log_path.exists(): + shutil.rmtree(log_path) - # 3. Verify directory was created - assert log_path.exists() - assert log_path.is_dir() + # Ensure it exists again for other tests if they rely on it (though they shouldn't rely on global state ideally) + log_path.mkdir(parents=True, exist_ok=True) diff --git a/tests/test_readme_usage.py b/tests/test_readme_usage.py index 68a90b3..58f3367 100644 --- a/tests/test_readme_usage.py +++ b/tests/test_readme_usage.py @@ -13,6 +13,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest +from coreason_identity.models import UserContext from coreason_optimizer.core.client import OpenAIClient from coreason_optimizer.core.config import OptimizerConfig @@ -31,7 +32,7 @@ class MockAgent: outputs = ["answer"] -def test_readme_library_usage_flow(tmp_path: Path) -> None: +def test_readme_library_usage_flow(tmp_path: Path, mock_context: UserContext) -> None: """ Verify the code snippet in 'Library Usage' section of README.md works. """ @@ -82,8 +83,8 @@ def test_readme_library_usage_flow(tmp_path: Path) -> None: optimizer = MiproOptimizer(client, metric, config) # 3. Load Data - train_set = Dataset.from_csv(train_csv, input_cols=["question"], reference_col="answer") - val_set = Dataset.from_csv(val_csv, input_cols=["question"], reference_col="answer") + train_set = Dataset.from_csv(train_csv, input_cols=["question"], reference_col="answer", context=mock_context) + val_set = Dataset.from_csv(val_csv, input_cols=["question"], reference_col="answer", context=mock_context) # 4. Compile agent = MockAgent()