From df59e6cea69b48bd7b85075550f2516cac6d6f2c Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 16:57:42 +0200 Subject: [PATCH 01/18] fix dependencies uv.lock --- pyproject.toml | 1 + uv.lock | 309 +++++++++++++++++++++++++------------------------ 2 files changed, 160 insertions(+), 150 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2e90e39..d039c6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ dependencies = [ # API di social media "praw", # Reddit ] + [tool.pytest.ini_options] pythonpath = ["src"] testpaths = ["tests"] diff --git a/uv.lock b/uv.lock index 9c977c3..d8114d6 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = "==3.12.*" [[package]] name = "agno" -version = "2.0.5" +version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "docstring-parser" }, @@ -20,9 +20,9 @@ dependencies = [ { name = "typer" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/30/53264fe9543fcb1d8b860ca3a3b705da5032e73f93818273d3169c3edc1b/agno-2.0.5.tar.gz", hash = "sha256:37d72fb98eb97c3abdb15be186f795b39f8ff668d97d9f9382e5971329e075d1", size = 855430, upload-time = "2025-09-17T02:45:10.348Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/c4/7a8915453846a4dea643b303d8e7448955c2221034309b10f7b2af1c9c3b/agno-2.1.0.tar.gz", hash = "sha256:0a840528fbc69bead7ff0dc6f28e59864af86f138db22a7b65bc9de71a141391", size = 906137, upload-time = "2025-10-01T11:32:21.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/f7/c216f0a218ef99958b77194613250131834f08d3a48b7a33bde6c638c5be/agno-2.0.5-py3-none-any.whl", hash = "sha256:9050bdb63feca2f421c0e3e73172ac269e82ed462e54b7318868a65107a89bd3", size = 1077050, upload-time = "2025-09-17T02:45:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/25/ce/3ab4eabb9c135177956a353ead7b025c2b8e563a6be7213eb001a5bab446/agno-2.1.0-py3-none-any.whl", hash = "sha256:c0e0554ffbfddbc222b08d58d9e3225325e81880910ef1221a27227b7a522e27", size = 1134550, upload-time = "2025-10-01T11:32:19.102Z" }, ] [[package]] @@ -45,7 +45,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.16" +version = "3.12.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -56,24 +56,25 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/1c4721d143e14af753f2bf5e3b681883e1f24b592c0482df6fa6e33597fa/aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8", size = 7676826, upload-time = "2025-04-02T02:17:44.74Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/38/100d01cbc60553743baf0fba658cb125f8ad674a8a771f765cdc155a890d/aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27", size = 704881, upload-time = "2025-04-02T02:16:09.26Z" }, - { url = "https://files.pythonhosted.org/packages/21/ed/b4102bb6245e36591209e29f03fe87e7956e54cb604ee12e20f7eb47f994/aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713", size = 464564, upload-time = "2025-04-02T02:16:10.781Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e1/a9ab6c47b62ecee080eeb33acd5352b40ecad08fb2d0779bcc6739271745/aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb", size = 456548, upload-time = "2025-04-02T02:16:12.764Z" }, - { url = "https://files.pythonhosted.org/packages/80/ad/216c6f71bdff2becce6c8776f0aa32cb0fa5d83008d13b49c3208d2e4016/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321", size = 1691749, upload-time = "2025-04-02T02:16:14.304Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ea/7df7bcd3f4e734301605f686ffc87993f2d51b7acb6bcc9b980af223f297/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e", size = 1736874, upload-time = "2025-04-02T02:16:16.538Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/c7724b9c87a29b7cfd1202ec6446bae8524a751473d25e2ff438bc9a02bf/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c", size = 1786885, upload-time = "2025-04-02T02:16:18.268Z" }, - { url = "https://files.pythonhosted.org/packages/86/b3/f61f8492fa6569fa87927ad35a40c159408862f7e8e70deaaead349e2fba/aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce", size = 1698059, upload-time = "2025-04-02T02:16:20.234Z" }, - { url = "https://files.pythonhosted.org/packages/ce/be/7097cf860a9ce8bbb0e8960704e12869e111abcd3fbd245153373079ccec/aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e", size = 1626527, upload-time = "2025-04-02T02:16:22.092Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1d/aaa841c340e8c143a8d53a1f644c2a2961c58cfa26e7b398d6bf75cf5d23/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b", size = 1644036, upload-time = "2025-04-02T02:16:23.707Z" }, - { url = "https://files.pythonhosted.org/packages/2c/88/59d870f76e9345e2b149f158074e78db457985c2b4da713038d9da3020a8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540", size = 1685270, upload-time = "2025-04-02T02:16:25.874Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b1/c6686948d4c79c3745595efc469a9f8a43cab3c7efc0b5991be65d9e8cb8/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b", size = 1650852, upload-time = "2025-04-02T02:16:27.556Z" }, - { url = "https://files.pythonhosted.org/packages/fe/94/3e42a6916fd3441721941e0f1b8438e1ce2a4c49af0e28e0d3c950c9b3c9/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e", size = 1704481, upload-time = "2025-04-02T02:16:29.573Z" }, - { url = "https://files.pythonhosted.org/packages/b1/6d/6ab5854ff59b27075c7a8c610597d2b6c38945f9a1284ee8758bc3720ff6/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c", size = 1735370, upload-time = "2025-04-02T02:16:31.191Z" }, - { url = "https://files.pythonhosted.org/packages/73/2a/08a68eec3c99a6659067d271d7553e4d490a0828d588e1daa3970dc2b771/aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71", size = 1697619, upload-time = "2025-04-02T02:16:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/61/d5/fea8dbbfb0cd68fbb56f0ae913270a79422d9a41da442a624febf72d2aaf/aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2", size = 411710, upload-time = "2025-04-02T02:16:34.525Z" }, - { url = "https://files.pythonhosted.org/packages/33/fb/41cde15fbe51365024550bf77b95a4fc84ef41365705c946da0421f0e1e0/aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682", size = 438012, upload-time = "2025-04-02T02:16:36.103Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, ] [[package]] @@ -100,16 +101,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.10.0" +version = "4.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, ] [[package]] @@ -188,11 +189,11 @@ wheels = [ [[package]] name = "cachetools" -version = "5.5.2" +version = "6.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/61/e4fad8155db4a04bfb4734c7c8ff0882f078f24294d42798b3568eb63bff/cachetools-6.2.0.tar.gz", hash = "sha256:38b328c0889450f05f5e120f56ab68c8abaf424e1275522b138ffc93253f7e32", size = 30988, upload-time = "2025-08-25T18:57:30.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, + { url = "https://files.pythonhosted.org/packages/6c/56/3124f61d37a7a4e7cc96afc5492c78ba0cb551151e530b54669ddd1436ef/cachetools-6.2.0-py3-none-any.whl", hash = "sha256:1c76a8960c0041fcc21097e357f882197c79da0dbff766e7317890a65d7d8ba6", size = 11276, upload-time = "2025-08-25T18:57:29.684Z" }, ] [[package]] @@ -249,14 +250,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.1" +version = "8.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, ] [[package]] @@ -286,43 +287,43 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.1" +version = "46.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/62/e3664e6ffd7743e1694b244dde70b43a394f6f7fbcacf7014a8ff5197c73/cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7", size = 749198, upload-time = "2025-09-17T00:10:35.797Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/9b/e301418629f7bfdf72db9e80ad6ed9d1b83c487c471803eaa6464c511a01/cryptography-46.0.2.tar.gz", hash = "sha256:21b6fc8c71a3f9a604f028a329e5560009cc4a3a828bfea5fcba8eb7647d88fe", size = 749293, upload-time = "2025-10-01T00:29:11.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/8c/44ee01267ec01e26e43ebfdae3f120ec2312aa72fa4c0507ebe41a26739f/cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475", size = 7285044, upload-time = "2025-09-17T00:08:36.807Z" }, - { url = "https://files.pythonhosted.org/packages/22/59/9ae689a25047e0601adfcb159ec4f83c0b4149fdb5c3030cc94cd218141d/cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080", size = 4308182, upload-time = "2025-09-17T00:08:39.388Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/ca6cc9df7118f2fcd142c76b1da0f14340d77518c05b1ebfbbabca6b9e7d/cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e", size = 4572393, upload-time = "2025-09-17T00:08:41.663Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a3/0f5296f63815d8e985922b05c31f77ce44787b3127a67c0b7f70f115c45f/cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6", size = 4308400, upload-time = "2025-09-17T00:08:43.559Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8c/74fcda3e4e01be1d32775d5b4dd841acaac3c1b8fa4d0774c7ac8d52463d/cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8", size = 4015786, upload-time = "2025-09-17T00:08:45.758Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b8/85d23287baeef273b0834481a3dd55bbed3a53587e3b8d9f0898235b8f91/cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28", size = 4982606, upload-time = "2025-09-17T00:08:47.602Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/de61ad5b52433b389afca0bc70f02a7a1f074651221f599ce368da0fe437/cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9", size = 4604234, upload-time = "2025-09-17T00:08:49.879Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1f/dbd4d6570d84748439237a7478d124ee0134bf166ad129267b7ed8ea6d22/cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736", size = 4307669, upload-time = "2025-09-17T00:08:52.321Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fd/ca0a14ce7f0bfe92fa727aacaf2217eb25eb7e4ed513b14d8e03b26e63ed/cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b", size = 4947579, upload-time = "2025-09-17T00:08:54.697Z" }, - { url = "https://files.pythonhosted.org/packages/89/6b/09c30543bb93401f6f88fce556b3bdbb21e55ae14912c04b7bf355f5f96c/cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab", size = 4603669, upload-time = "2025-09-17T00:08:57.16Z" }, - { url = "https://files.pythonhosted.org/packages/23/9a/38cb01cb09ce0adceda9fc627c9cf98eb890fc8d50cacbe79b011df20f8a/cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75", size = 4435828, upload-time = "2025-09-17T00:08:59.606Z" }, - { url = "https://files.pythonhosted.org/packages/0f/53/435b5c36a78d06ae0bef96d666209b0ecd8f8181bfe4dda46536705df59e/cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5", size = 4709553, upload-time = "2025-09-17T00:09:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/f5/c4/0da6e55595d9b9cd3b6eb5dc22f3a07ded7f116a3ea72629cab595abb804/cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0", size = 3058327, upload-time = "2025-09-17T00:09:03.726Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/cd29a35e0d6e78a0ee61793564c8cff0929c38391cb0de27627bdc7525aa/cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7", size = 3523893, upload-time = "2025-09-17T00:09:06.272Z" }, - { url = "https://files.pythonhosted.org/packages/f2/dd/eea390f3e78432bc3d2f53952375f8b37cb4d37783e626faa6a51e751719/cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0", size = 2932145, upload-time = "2025-09-17T00:09:08.568Z" }, - { url = "https://files.pythonhosted.org/packages/98/e5/fbd632385542a3311915976f88e0dfcf09e62a3fc0aff86fb6762162a24d/cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b", size = 7255677, upload-time = "2025-09-17T00:09:42.407Z" }, - { url = "https://files.pythonhosted.org/packages/56/3e/13ce6eab9ad6eba1b15a7bd476f005a4c1b3f299f4c2f32b22408b0edccf/cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8", size = 4301110, upload-time = "2025-09-17T00:09:45.614Z" }, - { url = "https://files.pythonhosted.org/packages/a2/67/65dc233c1ddd688073cf7b136b06ff4b84bf517ba5529607c9d79720fc67/cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead", size = 4562369, upload-time = "2025-09-17T00:09:47.601Z" }, - { url = "https://files.pythonhosted.org/packages/17/db/d64ae4c6f4e98c3dac5bf35dd4d103f4c7c345703e43560113e5e8e31b2b/cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2", size = 4302126, upload-time = "2025-09-17T00:09:49.335Z" }, - { url = "https://files.pythonhosted.org/packages/3d/19/5f1eea17d4805ebdc2e685b7b02800c4f63f3dd46cfa8d4c18373fea46c8/cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32", size = 4009431, upload-time = "2025-09-17T00:09:51.239Z" }, - { url = "https://files.pythonhosted.org/packages/81/b5/229ba6088fe7abccbfe4c5edb96c7a5ad547fac5fdd0d40aa6ea540b2985/cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef", size = 4980739, upload-time = "2025-09-17T00:09:54.181Z" }, - { url = "https://files.pythonhosted.org/packages/3a/9c/50aa38907b201e74bc43c572f9603fa82b58e831bd13c245613a23cff736/cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0", size = 4592289, upload-time = "2025-09-17T00:09:56.731Z" }, - { url = "https://files.pythonhosted.org/packages/5a/33/229858f8a5bb22f82468bb285e9f4c44a31978d5f5830bb4ea1cf8a4e454/cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128", size = 4301815, upload-time = "2025-09-17T00:09:58.548Z" }, - { url = "https://files.pythonhosted.org/packages/52/cb/b76b2c87fbd6ed4a231884bea3ce073406ba8e2dae9defad910d33cbf408/cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca", size = 4943251, upload-time = "2025-09-17T00:10:00.475Z" }, - { url = "https://files.pythonhosted.org/packages/94/0f/f66125ecf88e4cb5b8017ff43f3a87ede2d064cb54a1c5893f9da9d65093/cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc", size = 4591247, upload-time = "2025-09-17T00:10:02.874Z" }, - { url = "https://files.pythonhosted.org/packages/f6/22/9f3134ae436b63b463cfdf0ff506a0570da6873adb4bf8c19b8a5b4bac64/cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7", size = 4428534, upload-time = "2025-09-17T00:10:04.994Z" }, - { url = "https://files.pythonhosted.org/packages/89/39/e6042bcb2638650b0005c752c38ea830cbfbcbb1830e4d64d530000aa8dc/cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a", size = 4699541, upload-time = "2025-09-17T00:10:06.925Z" }, - { url = "https://files.pythonhosted.org/packages/68/46/753d457492d15458c7b5a653fc9a84a1c9c7a83af6ebdc94c3fc373ca6e8/cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1", size = 3043779, upload-time = "2025-09-17T00:10:08.951Z" }, - { url = "https://files.pythonhosted.org/packages/2f/50/b6f3b540c2f6ee712feeb5fa780bb11fad76634e71334718568e7695cb55/cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3", size = 3517226, upload-time = "2025-09-17T00:10:10.769Z" }, - { url = "https://files.pythonhosted.org/packages/ff/e8/77d17d00981cdd27cc493e81e1749a0b8bbfb843780dbd841e30d7f50743/cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9", size = 2923149, upload-time = "2025-09-17T00:10:13.236Z" }, + { url = "https://files.pythonhosted.org/packages/e0/98/7a8df8c19a335c8028414738490fc3955c0cecbfdd37fcc1b9c3d04bd561/cryptography-46.0.2-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:f3e32ab7dd1b1ef67b9232c4cf5e2ee4cd517d4316ea910acaaa9c5712a1c663", size = 7261255, upload-time = "2025-10-01T00:27:22.947Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/b2adb2aa1baa6706adc3eb746691edd6f90a656a9a65c3509e274d15a2b8/cryptography-46.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1fd1a69086926b623ef8126b4c33d5399ce9e2f3fac07c9c734c2a4ec38b6d02", size = 4297596, upload-time = "2025-10-01T00:27:25.258Z" }, + { url = "https://files.pythonhosted.org/packages/e4/27/0f190ada240003119488ae66c897b5e97149292988f556aef4a6a2a57595/cryptography-46.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb7fb9cd44c2582aa5990cf61a4183e6f54eea3172e54963787ba47287edd135", size = 4450899, upload-time = "2025-10-01T00:27:27.458Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/e4744105ab02fdf6bb58ba9a816e23b7a633255987310b4187d6745533db/cryptography-46.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9066cfd7f146f291869a9898b01df1c9b0e314bfa182cef432043f13fc462c92", size = 4300382, upload-time = "2025-10-01T00:27:29.091Z" }, + { url = "https://files.pythonhosted.org/packages/33/fb/bf9571065c18c04818cb07de90c43fc042c7977c68e5de6876049559c72f/cryptography-46.0.2-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:97e83bf4f2f2c084d8dd792d13841d0a9b241643151686010866bbd076b19659", size = 4017347, upload-time = "2025-10-01T00:27:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/35/72/fc51856b9b16155ca071080e1a3ad0c3a8e86616daf7eb018d9565b99baa/cryptography-46.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:4a766d2a5d8127364fd936572c6e6757682fc5dfcbdba1632d4554943199f2fa", size = 4983500, upload-time = "2025-10-01T00:27:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/c1/53/0f51e926799025e31746d454ab2e36f8c3f0d41592bc65cb9840368d3275/cryptography-46.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fab8f805e9675e61ed8538f192aad70500fa6afb33a8803932999b1049363a08", size = 4482591, upload-time = "2025-10-01T00:27:34.869Z" }, + { url = "https://files.pythonhosted.org/packages/86/96/4302af40b23ab8aa360862251fb8fc450b2a06ff24bc5e261c2007f27014/cryptography-46.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1e3b6428a3d56043bff0bb85b41c535734204e599c1c0977e1d0f261b02f3ad5", size = 4300019, upload-time = "2025-10-01T00:27:37.029Z" }, + { url = "https://files.pythonhosted.org/packages/9b/59/0be12c7fcc4c5e34fe2b665a75bc20958473047a30d095a7657c218fa9e8/cryptography-46.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:1a88634851d9b8de8bb53726f4300ab191d3b2f42595e2581a54b26aba71b7cc", size = 4950006, upload-time = "2025-10-01T00:27:40.272Z" }, + { url = "https://files.pythonhosted.org/packages/55/1d/42fda47b0111834b49e31590ae14fd020594d5e4dadd639bce89ad790fba/cryptography-46.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:be939b99d4e091eec9a2bcf41aaf8f351f312cd19ff74b5c83480f08a8a43e0b", size = 4482088, upload-time = "2025-10-01T00:27:42.668Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/60f583f69aa1602c2bdc7022dae86a0d2b837276182f8c1ec825feb9b874/cryptography-46.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f13b040649bc18e7eb37936009b24fd31ca095a5c647be8bb6aaf1761142bd1", size = 4425599, upload-time = "2025-10-01T00:27:44.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/57/d8d4134cd27e6e94cf44adb3f3489f935bde85f3a5508e1b5b43095b917d/cryptography-46.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9bdc25e4e01b261a8fda4e98618f1c9515febcecebc9566ddf4a70c63967043b", size = 4697458, upload-time = "2025-10-01T00:27:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/d1/2b/531e37408573e1da33adfb4c58875013ee8ac7d548d1548967d94a0ae5c4/cryptography-46.0.2-cp311-abi3-win32.whl", hash = "sha256:8b9bf67b11ef9e28f4d78ff88b04ed0929fcd0e4f70bb0f704cfc32a5c6311ee", size = 3056077, upload-time = "2025-10-01T00:27:48.424Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cd/2f83cafd47ed2dc5a3a9c783ff5d764e9e70d3a160e0df9a9dcd639414ce/cryptography-46.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:758cfc7f4c38c5c5274b55a57ef1910107436f4ae842478c4989abbd24bd5acb", size = 3512585, upload-time = "2025-10-01T00:27:50.521Z" }, + { url = "https://files.pythonhosted.org/packages/00/36/676f94e10bfaa5c5b86c469ff46d3e0663c5dc89542f7afbadac241a3ee4/cryptography-46.0.2-cp311-abi3-win_arm64.whl", hash = "sha256:218abd64a2e72f8472c2102febb596793347a3e65fafbb4ad50519969da44470", size = 2927474, upload-time = "2025-10-01T00:27:52.91Z" }, + { url = "https://files.pythonhosted.org/packages/d5/bb/fa95abcf147a1b0bb94d95f53fbb09da77b24c776c5d87d36f3d94521d2c/cryptography-46.0.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a08e7401a94c002e79dc3bc5231b6558cd4b2280ee525c4673f650a37e2c7685", size = 7248090, upload-time = "2025-10-01T00:28:22.846Z" }, + { url = "https://files.pythonhosted.org/packages/b7/66/f42071ce0e3ffbfa80a88feadb209c779fda92a23fbc1e14f74ebf72ef6b/cryptography-46.0.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d30bc11d35743bf4ddf76674a0a369ec8a21f87aaa09b0661b04c5f6c46e8d7b", size = 4293123, upload-time = "2025-10-01T00:28:25.072Z" }, + { url = "https://files.pythonhosted.org/packages/a8/5d/1fdbd2e5c1ba822828d250e5a966622ef00185e476d1cd2726b6dd135e53/cryptography-46.0.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bca3f0ce67e5a2a2cf524e86f44697c4323a86e0fd7ba857de1c30d52c11ede1", size = 4439524, upload-time = "2025-10-01T00:28:26.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/5e4989a7d102d4306053770d60f978c7b6b1ea2ff8c06e0265e305b23516/cryptography-46.0.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ff798ad7a957a5021dcbab78dfff681f0cf15744d0e6af62bd6746984d9c9e9c", size = 4297264, upload-time = "2025-10-01T00:28:29.327Z" }, + { url = "https://files.pythonhosted.org/packages/28/78/b56f847d220cb1d6d6aef5a390e116ad603ce13a0945a3386a33abc80385/cryptography-46.0.2-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cb5e8daac840e8879407acbe689a174f5ebaf344a062f8918e526824eb5d97af", size = 4011872, upload-time = "2025-10-01T00:28:31.479Z" }, + { url = "https://files.pythonhosted.org/packages/e1/80/2971f214b066b888944f7b57761bf709ee3f2cf805619a18b18cab9b263c/cryptography-46.0.2-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:3f37aa12b2d91e157827d90ce78f6180f0c02319468a0aea86ab5a9566da644b", size = 4978458, upload-time = "2025-10-01T00:28:33.267Z" }, + { url = "https://files.pythonhosted.org/packages/a5/84/0cb0a2beaa4f1cbe63ebec4e97cd7e0e9f835d0ba5ee143ed2523a1e0016/cryptography-46.0.2-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e38f203160a48b93010b07493c15f2babb4e0f2319bbd001885adb3f3696d21", size = 4472195, upload-time = "2025-10-01T00:28:36.039Z" }, + { url = "https://files.pythonhosted.org/packages/30/8b/2b542ddbf78835c7cd67b6fa79e95560023481213a060b92352a61a10efe/cryptography-46.0.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d19f5f48883752b5ab34cff9e2f7e4a7f216296f33714e77d1beb03d108632b6", size = 4296791, upload-time = "2025-10-01T00:28:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/78/12/9065b40201b4f4876e93b9b94d91feb18de9150d60bd842a16a21565007f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:04911b149eae142ccd8c9a68892a70c21613864afb47aba92d8c7ed9cc001023", size = 4939629, upload-time = "2025-10-01T00:28:39.654Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9e/6507dc048c1b1530d372c483dfd34e7709fc542765015425f0442b08547f/cryptography-46.0.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:8b16c1ede6a937c291d41176934268e4ccac2c6521c69d3f5961c5a1e11e039e", size = 4471988, upload-time = "2025-10-01T00:28:41.822Z" }, + { url = "https://files.pythonhosted.org/packages/b1/86/d025584a5f7d5c5ec8d3633dbcdce83a0cd579f1141ceada7817a4c26934/cryptography-46.0.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:747b6f4a4a23d5a215aadd1d0b12233b4119c4313df83ab4137631d43672cc90", size = 4422989, upload-time = "2025-10-01T00:28:43.608Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/536370418b38a15a61bbe413006b79dfc3d2b4b0eafceb5581983f973c15/cryptography-46.0.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6b275e398ab3a7905e168c036aad54b5969d63d3d9099a0a66cc147a3cc983be", size = 4685578, upload-time = "2025-10-01T00:28:45.361Z" }, + { url = "https://files.pythonhosted.org/packages/15/52/ea7e2b1910f547baed566c866fbb86de2402e501a89ecb4871ea7f169a81/cryptography-46.0.2-cp38-abi3-win32.whl", hash = "sha256:0b507c8e033307e37af61cb9f7159b416173bdf5b41d11c4df2e499a1d8e007c", size = 3036711, upload-time = "2025-10-01T00:28:47.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/9e/171f40f9c70a873e73c2efcdbe91e1d4b1777a03398fa1c4af3c56a2477a/cryptography-46.0.2-cp38-abi3-win_amd64.whl", hash = "sha256:f9b2dc7668418fb6f221e4bf701f716e05e8eadb4f1988a2487b11aedf8abe62", size = 3500007, upload-time = "2025-10-01T00:28:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/3e/7c/15ad426257615f9be8caf7f97990cf3dcbb5b8dd7ed7e0db581a1c4759dd/cryptography-46.0.2-cp38-abi3-win_arm64.whl", hash = "sha256:91447f2b17e83c9e0c89f133119d83f94ce6e0fb55dd47da0a959316e6e9cfa1", size = 2918153, upload-time = "2025-10-01T00:28:51.003Z" }, ] [[package]] @@ -407,16 +408,16 @@ wheels = [ [[package]] name = "fastapi" -version = "0.116.2" +version = "0.118.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/01/64/1296f46d6b9e3b23fb22e5d01af3f104ef411425531376212f1eefa2794d/fastapi-0.116.2.tar.gz", hash = "sha256:231a6af2fe21cfa2c32730170ad8514985fc250bec16c9b242d3b94c835ef529", size = 298595, upload-time = "2025-09-16T18:29:23.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/28/3c/2b9345a6504e4055eaa490e0b41c10e338ad61d9aeaae41d97807873cdf2/fastapi-0.118.0.tar.gz", hash = "sha256:5e81654d98c4d2f53790a7d32d25a7353b30c81441be7d0958a26b5d761fa1c8", size = 310536, upload-time = "2025-09-29T03:37:23.126Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/e4/c543271a8018874b7f682bf6156863c416e1334b8ed3e51a69495c5d4360/fastapi-0.116.2-py3-none-any.whl", hash = "sha256:c3a7a8fb830b05f7e087d920e0d786ca1fc9892eb4e9a84b227be4c1bc7569db", size = 95670, upload-time = "2025-09-16T18:29:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/54/20/54e2bdaad22ca91a59455251998d43094d5c3d3567c52c7c04774b3f43f2/fastapi-0.118.0-py3-none-any.whl", hash = "sha256:705137a61e2ef71019d2445b123aa8845bd97273c395b744d5a7dfe559056855", size = 97694, upload-time = "2025-09-29T03:37:21.338Z" }, ] [[package]] @@ -535,21 +536,21 @@ wheels = [ [[package]] name = "google-auth" -version = "2.40.3" +version = "2.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cachetools" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/af/5129ce5b2f9688d2fa49b463e544972a7c82b0fdb50980dafee92e121d9f/google_auth-2.41.1.tar.gz", hash = "sha256:b76b7b1f9e61f0cb7e88870d14f6a94aeef248959ef6992670efee37709cbfd2", size = 292284, upload-time = "2025-09-30T22:51:26.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/be/a4/7319a2a8add4cc352be9e3efeff5e2aacee917c85ca2fa1647e29089983c/google_auth-2.41.1-py2.py3-none-any.whl", hash = "sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d", size = 221302, upload-time = "2025-09-30T22:51:24.212Z" }, ] [[package]] name = "google-genai" -version = "1.38.0" +version = "1.39.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -561,14 +562,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/11/108ddd3aca8af6a9e2369e59b9646a3a4c64aefb39d154f6467ab8d79f34/google_genai-1.38.0.tar.gz", hash = "sha256:363272fc4f677d0be6a1aed7ebabe8adf45e1626a7011a7886a587e9464ca9ec", size = 244903, upload-time = "2025-09-16T23:25:42.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/3e/25b88bda07ca237043f1be45d13c49ffbc73f9edf45d3232345802f67197/google_genai-1.39.1.tar.gz", hash = "sha256:4721704b43d170fc3f1b1cb5494bee1a7f7aae20de3a5383cdf6a129139df80b", size = 244631, upload-time = "2025-09-26T20:56:19.5Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/53/6c/1de711bab3c118284904c3bedf870519e8c63a7a8e0905ac3833f1db9cbc/google_genai-1.38.0-py3-none-any.whl", hash = "sha256:95407425132d42b3fa11bc92b3f5cf61a0fbd8d9add1f0e89aac52c46fbba090", size = 245558, upload-time = "2025-09-16T23:25:41.141Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/12c1f386184d2fcd694b73adeabc3714a5ed65c01cc183b4e3727a26b9d1/google_genai-1.39.1-py3-none-any.whl", hash = "sha256:6ca36c7e40db6fcba7049dfdd102c86da326804f34403bd7d90fa613a45e5a78", size = 244681, upload-time = "2025-09-26T20:56:17.527Z" }, ] [[package]] name = "gradio" -version = "5.46.0" +version = "5.47.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiofiles" }, @@ -600,14 +601,14 @@ dependencies = [ { name = "typing-extensions" }, { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/4b/733c3d9eb57936a004434647ab245e2c55e0262164dcd8aa891e718adf36/gradio-5.46.0.tar.gz", hash = "sha256:04ffe0bf79e81d63c16560fb483db06d39d96f2f36b7a672fa595dd6ddc69784", size = 72189142, upload-time = "2025-09-16T20:45:06.044Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/df/b792699b386c91aac38f5f844f92703a9fdd37aa4d2193c37de2cd4fa007/gradio-5.47.2.tar.gz", hash = "sha256:2e1cc00421da159ed9e9e2c8760e792ca2d8fa9bc610f3da0ec5cfa3fa6ca0be", size = 72289342, upload-time = "2025-09-26T19:51:10.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/a4/fc769047a7e17d6d9b63bf96385e799114c63c5f883b4accbd6914eb2649/gradio-5.46.0-py3-none-any.whl", hash = "sha256:e088fd68a3a04365caf3c7d6846c6e1fff1aca3b4e5b49ec5003f18bdbe343d5", size = 60273957, upload-time = "2025-09-16T20:44:53.732Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/7fed1186a9c289dad190011c1d86be761aeef968e856d653efa2f1d48dc9/gradio-5.47.2-py3-none-any.whl", hash = "sha256:e5cdf106b27bdb321284f327537682f3060ef0c62d9c70236eeaa8b1917a6803", size = 60369896, upload-time = "2025-09-26T19:51:05.636Z" }, ] [[package]] name = "gradio-client" -version = "1.13.0" +version = "1.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "fsspec" }, @@ -617,9 +618,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/16/70/c2989a14bfb3975ca4923463b2e01eb917a79f8842aac48cb472a133cf26/gradio_client-1.13.0.tar.gz", hash = "sha256:07de7e8e58553335d56e0c7db6af60f1205fd1f167bf07f3e0f587888695a8f3", size = 322963, upload-time = "2025-09-10T17:06:32.23Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/a9/a3beb0ece8c05c33e6376b790fa42e0dd157abca8220cf639b249a597467/gradio_client-1.13.3.tar.gz", hash = "sha256:869b3e67e0f7a0f40df8c48c94de99183265cf4b7b1d9bd4623e336d219ffbe7", size = 323253, upload-time = "2025-09-26T19:51:21.7Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/aa/8ad1cc8be082867aaa941aae30a38d68db9fbaf4306a51143307acd79b7a/gradio_client-1.13.0-py3-none-any.whl", hash = "sha256:4489ebd07ae40c6cc7a6a02cf60a53e9e3345aa5342a3814c356775bbad64bbc", size = 325012, upload-time = "2025-09-10T17:06:30.721Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0b/337b74504681b5dde39f20d803bb09757f9973ecdc65fd4e819d4b11faf7/gradio_client-1.13.3-py3-none-any.whl", hash = "sha256:3f63e4d33a2899c1a12b10fe3cf77b82a6919ff1a1fb6391f6aa225811aa390c", size = 325350, upload-time = "2025-09-26T19:51:20.288Z" }, ] [[package]] @@ -719,7 +720,7 @@ socks = [ [[package]] name = "huggingface-hub" -version = "0.35.0" +version = "0.35.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -731,9 +732,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/79/d71d40efa058e8c4a075158f8855bc2998037b5ff1c84f249f34435c1df7/huggingface_hub-0.35.0.tar.gz", hash = "sha256:ccadd2a78eef75effff184ad89401413629fabc52cefd76f6bbacb9b1c0676ac", size = 461486, upload-time = "2025-09-16T13:49:33.282Z" } +sdist = { url = "https://files.pythonhosted.org/packages/10/7e/a0a97de7c73671863ca6b3f61fa12518caf35db37825e43d63a70956738c/huggingface_hub-0.35.3.tar.gz", hash = "sha256:350932eaa5cc6a4747efae85126ee220e4ef1b54e29d31c3b45c5612ddf0b32a", size = 461798, upload-time = "2025-09-29T14:29:58.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/85/a18508becfa01f1e4351b5e18651b06d210dbd96debccd48a452acccb901/huggingface_hub-0.35.0-py3-none-any.whl", hash = "sha256:f2e2f693bca9a26530b1c0b9bcd4c1495644dad698e6a0060f90e22e772c31e9", size = 563436, upload-time = "2025-09-16T13:49:30.627Z" }, + { url = "https://files.pythonhosted.org/packages/31/a0/651f93d154cb72323358bf2bbae3e642bdb5d2f1bfc874d096f7cb159fa0/huggingface_hub-0.35.3-py3-none-any.whl", hash = "sha256:0e3a01829c19d86d03793e4577816fe3bdfc1602ac62c7fb220d593d351224ba", size = 564262, upload-time = "2025-09-29T14:29:55.813Z" }, ] [[package]] @@ -815,20 +816,21 @@ wheels = [ [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] [[package]] @@ -867,6 +869,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] +[[package]] +name = "multitasking" +version = "0.0.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/0d/74f0293dfd7dcc3837746d0138cbedd60b31701ecc75caec7d3f281feba0/multitasking-0.0.12.tar.gz", hash = "sha256:2fba2fa8ed8c4b85e227c5dd7dc41c7d658de3b6f247927316175a57349b84d1", size = 19984, upload-time = "2025-07-20T21:27:51.636Z" } + [[package]] name = "newsapi-python" version = "0.2.7" @@ -900,15 +908,15 @@ wheels = [ [[package]] name = "ollama" -version = "0.5.4" +version = "0.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/62/a36be4555e4218d6c8b35e72e0dfe0823845400097275cd81c9aec4ddf39/ollama-0.5.4.tar.gz", hash = "sha256:75857505a5d42e5e58114a1b78cc8c24596d8866863359d8a2329946a9b6d6f3", size = 45233, upload-time = "2025-09-16T00:25:25.785Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/47/f9ee32467fe92744474a8c72e138113f3b529fc266eea76abfdec9a33f3b/ollama-0.6.0.tar.gz", hash = "sha256:da2b2d846b5944cfbcee1ca1e6ee0585f6c9d45a2fe9467cbcd096a37383da2f", size = 50811, upload-time = "2025-09-24T22:46:02.417Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/af/d0a23c8fdec4c8ddb771191d9b36a57fbce6741835a78f1b18ab6d15ae7d/ollama-0.5.4-py3-none-any.whl", hash = "sha256:6374c9bb4f2a371b3583c09786112ba85b006516745689c172a7e28af4d4d1a2", size = 13548, upload-time = "2025-09-16T00:25:24.186Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c1/edc9f41b425ca40b26b7c104c5f6841a4537bb2552bfa6ca66e81405bb95/ollama-0.6.0-py3-none-any.whl", hash = "sha256:534511b3ccea2dff419ae06c3b58d7f217c55be7897c8ce5868dfb6b219cf7a0", size = 14130, upload-time = "2025-09-24T22:46:01.19Z" }, ] [[package]] @@ -945,7 +953,7 @@ wheels = [ [[package]] name = "pandas" -version = "2.3.2" +version = "2.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, @@ -953,15 +961,15 @@ dependencies = [ { name = "pytz" }, { name = "tzdata" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/db/614c20fb7a85a14828edd23f1c02db58a30abf3ce76f38806155d160313c/pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9", size = 11587652, upload-time = "2025-08-21T10:27:15.888Z" }, - { url = "https://files.pythonhosted.org/packages/99/b0/756e52f6582cade5e746f19bad0517ff27ba9c73404607c0306585c201b3/pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b", size = 10717686, upload-time = "2025-08-21T10:27:18.486Z" }, - { url = "https://files.pythonhosted.org/packages/37/4c/dd5ccc1e357abfeee8353123282de17997f90ff67855f86154e5a13b81e5/pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175", size = 11278722, upload-time = "2025-08-21T10:27:21.149Z" }, - { url = "https://files.pythonhosted.org/packages/d3/a4/f7edcfa47e0a88cda0be8b068a5bae710bf264f867edfdf7b71584ace362/pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9", size = 11987803, upload-time = "2025-08-21T10:27:23.767Z" }, - { url = "https://files.pythonhosted.org/packages/f6/61/1bce4129f93ab66f1c68b7ed1c12bac6a70b1b56c5dab359c6bbcd480b52/pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4", size = 12766345, upload-time = "2025-08-21T10:27:26.6Z" }, - { url = "https://files.pythonhosted.org/packages/8e/46/80d53de70fee835531da3a1dae827a1e76e77a43ad22a8cd0f8142b61587/pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811", size = 13439314, upload-time = "2025-08-21T10:27:29.213Z" }, - { url = "https://files.pythonhosted.org/packages/28/30/8114832daff7489f179971dbc1d854109b7f4365a546e3ea75b6516cea95/pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", size = 10983326, upload-time = "2025-08-21T10:27:31.901Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, ] [[package]] @@ -1179,16 +1187,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.10.1" +version = "2.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, + { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" }, ] [[package]] @@ -1292,19 +1300,20 @@ wheels = [ [[package]] name = "pyyaml" -version = "6.0.2" +version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, ] [[package]] @@ -1371,28 +1380,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.13.0" +version = "0.13.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/1a/1f4b722862840295bcaba8c9e5261572347509548faaa99b2d57ee7bfe6a/ruff-0.13.0.tar.gz", hash = "sha256:5b4b1ee7eb35afae128ab94459b13b2baaed282b1fb0f472a73c82c996c8ae60", size = 5372863, upload-time = "2025-09-10T16:25:37.917Z" } +sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/fe/6f87b419dbe166fd30a991390221f14c5b68946f389ea07913e1719741e0/ruff-0.13.0-py3-none-linux_armv6l.whl", hash = "sha256:137f3d65d58ee828ae136a12d1dc33d992773d8f7644bc6b82714570f31b2004", size = 12187826, upload-time = "2025-09-10T16:24:39.5Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/c92296b1fc36d2499e12b74a3fdb230f77af7bdf048fad7b0a62e94ed56a/ruff-0.13.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:21ae48151b66e71fd111b7d79f9ad358814ed58c339631450c66a4be33cc28b9", size = 12933428, upload-time = "2025-09-10T16:24:43.866Z" }, - { url = "https://files.pythonhosted.org/packages/44/cf/40bc7221a949470307d9c35b4ef5810c294e6cfa3caafb57d882731a9f42/ruff-0.13.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:64de45f4ca5441209e41742d527944635a05a6e7c05798904f39c85bafa819e3", size = 12095543, upload-time = "2025-09-10T16:24:46.638Z" }, - { url = "https://files.pythonhosted.org/packages/f1/03/8b5ff2a211efb68c63a1d03d157e924997ada87d01bebffbd13a0f3fcdeb/ruff-0.13.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b2c653ae9b9d46e0ef62fc6fbf5b979bda20a0b1d2b22f8f7eb0cde9f4963b8", size = 12312489, upload-time = "2025-09-10T16:24:49.556Z" }, - { url = "https://files.pythonhosted.org/packages/37/fc/2336ef6d5e9c8d8ea8305c5f91e767d795cd4fc171a6d97ef38a5302dadc/ruff-0.13.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4cec632534332062bc9eb5884a267b689085a1afea9801bf94e3ba7498a2d207", size = 11991631, upload-time = "2025-09-10T16:24:53.439Z" }, - { url = "https://files.pythonhosted.org/packages/39/7f/f6d574d100fca83d32637d7f5541bea2f5e473c40020bbc7fc4a4d5b7294/ruff-0.13.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd628101d9f7d122e120ac7c17e0a0f468b19bc925501dbe03c1cb7f5415b24", size = 13720602, upload-time = "2025-09-10T16:24:56.392Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c8/a8a5b81d8729b5d1f663348d11e2a9d65a7a9bd3c399763b1a51c72be1ce/ruff-0.13.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:afe37db8e1466acb173bb2a39ca92df00570e0fd7c94c72d87b51b21bb63efea", size = 14697751, upload-time = "2025-09-10T16:24:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/57/f5/183ec292272ce7ec5e882aea74937f7288e88ecb500198b832c24debc6d3/ruff-0.13.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f96a8d90bb258d7d3358b372905fe7333aaacf6c39e2408b9f8ba181f4b6ef2", size = 14095317, upload-time = "2025-09-10T16:25:03.025Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8d/7f9771c971724701af7926c14dab31754e7b303d127b0d3f01116faef456/ruff-0.13.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b5e3d883e4f924c5298e3f2ee0f3085819c14f68d1e5b6715597681433f153", size = 13144418, upload-time = "2025-09-10T16:25:06.272Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a6/7985ad1778e60922d4bef546688cd8a25822c58873e9ff30189cfe5dc4ab/ruff-0.13.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03447f3d18479df3d24917a92d768a89f873a7181a064858ea90a804a7538991", size = 13370843, upload-time = "2025-09-10T16:25:09.965Z" }, - { url = "https://files.pythonhosted.org/packages/64/1c/bafdd5a7a05a50cc51d9f5711da704942d8dd62df3d8c70c311e98ce9f8a/ruff-0.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fbc6b1934eb1c0033da427c805e27d164bb713f8e273a024a7e86176d7f462cf", size = 13321891, upload-time = "2025-09-10T16:25:12.969Z" }, - { url = "https://files.pythonhosted.org/packages/bc/3e/7817f989cb9725ef7e8d2cee74186bf90555279e119de50c750c4b7a72fe/ruff-0.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a8ab6a3e03665d39d4a25ee199d207a488724f022db0e1fe4002968abdb8001b", size = 12119119, upload-time = "2025-09-10T16:25:16.621Z" }, - { url = "https://files.pythonhosted.org/packages/58/07/9df080742e8d1080e60c426dce6e96a8faf9a371e2ce22eef662e3839c95/ruff-0.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2a5c62f8ccc6dd2fe259917482de7275cecc86141ee10432727c4816235bc41", size = 11961594, upload-time = "2025-09-10T16:25:19.49Z" }, - { url = "https://files.pythonhosted.org/packages/6a/f4/ae1185349197d26a2316840cb4d6c3fba61d4ac36ed728bf0228b222d71f/ruff-0.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b7b85ca27aeeb1ab421bc787009831cffe6048faae08ad80867edab9f2760945", size = 12933377, upload-time = "2025-09-10T16:25:22.371Z" }, - { url = "https://files.pythonhosted.org/packages/b6/39/e776c10a3b349fc8209a905bfb327831d7516f6058339a613a8d2aaecacd/ruff-0.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:79ea0c44a3032af768cabfd9616e44c24303af49d633b43e3a5096e009ebe823", size = 13418555, upload-time = "2025-09-10T16:25:25.681Z" }, - { url = "https://files.pythonhosted.org/packages/46/09/dca8df3d48e8b3f4202bf20b1658898e74b6442ac835bfe2c1816d926697/ruff-0.13.0-py3-none-win32.whl", hash = "sha256:4e473e8f0e6a04e4113f2e1de12a5039579892329ecc49958424e5568ef4f768", size = 12141613, upload-time = "2025-09-10T16:25:28.664Z" }, - { url = "https://files.pythonhosted.org/packages/61/21/0647eb71ed99b888ad50e44d8ec65d7148babc0e242d531a499a0bbcda5f/ruff-0.13.0-py3-none-win_amd64.whl", hash = "sha256:48e5c25c7a3713eea9ce755995767f4dcd1b0b9599b638b12946e892123d1efb", size = 13258250, upload-time = "2025-09-10T16:25:31.773Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a3/03216a6a86c706df54422612981fb0f9041dbb452c3401501d4a22b942c9/ruff-0.13.0-py3-none-win_arm64.whl", hash = "sha256:ab80525317b1e1d38614addec8ac954f1b3e662de9d59114ecbf771d00cf613e", size = 12312357, upload-time = "2025-09-10T16:25:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, + { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, + { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, + { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, ] [[package]] @@ -1521,7 +1530,7 @@ wheels = [ [[package]] name = "typer" -version = "0.17.4" +version = "0.19.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1529,9 +1538,9 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, ] [[package]] @@ -1545,14 +1554,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]] @@ -1627,24 +1636,24 @@ requires-dist = [ [[package]] name = "urllib3" -version = "2.3.0" +version = "2.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, ] [[package]] name = "uvicorn" -version = "0.35.0" +version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, ] [[package]] -- 2.49.1 From 603dc7edeb9f3b2d7eed73180fbbc08996af831b Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 17:01:33 +0200 Subject: [PATCH 02/18] refactor test markers for clarity --- tests/api/test_news_api.py | 2 +- tests/api/test_reddit.py | 2 ++ tests/conftest.py | 15 +++++++++------ tests/tools/test_market_tool.py | 3 ++- tests/tools/test_news_tool.py | 1 - 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/tests/api/test_news_api.py b/tests/api/test_news_api.py index 4b6b192..839941c 100644 --- a/tests/api/test_news_api.py +++ b/tests/api/test_news_api.py @@ -5,7 +5,7 @@ from app.news import NewsApiWrapper @pytest.mark.news @pytest.mark.api -@pytest.mark.skipif(not os.getenv("NEWS_API_KEY"), reason="NEWS_API_KEY not set") +@pytest.mark.skipif(not os.getenv("NEWS_API_KEY"), reason="NEWS_API_KEY not set in environment variables") class TestNewsAPI: def test_news_api_initialization(self): diff --git a/tests/api/test_reddit.py b/tests/api/test_reddit.py index 81ab8ca..c82f56e 100644 --- a/tests/api/test_reddit.py +++ b/tests/api/test_reddit.py @@ -1,9 +1,11 @@ +import os import pytest from praw import Reddit from app.social.reddit import MAX_COMMENTS, RedditWrapper @pytest.mark.social @pytest.mark.api +@pytest.mark.skipif(not(os.getenv("REDDIT_CLIENT_ID")) or not(os.getenv("REDDIT_API_CLIENT_ID")) or not os.getenv("REDDIT_API_CLIENT_SECRET"), reason="REDDIT_CLIENT_ID and REDDIT_API_CLIENT_SECRET not set in environment variables") class TestRedditWrapper: def test_initialization(self): wrapper = RedditWrapper() diff --git a/tests/conftest.py b/tests/conftest.py index 2b7cf90..290fbf2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,17 +14,20 @@ def pytest_configure(config:pytest.Config): markers = [ ("slow", "marks tests as slow (deselect with '-m \"not slow\"')"), + ("limited", "marks tests that have limited execution due to API constraints"), + ("api", "marks tests that require API access"), ("market", "marks tests that use market data"), + ("news", "marks tests that use news"), + ("social", "marks tests that use social media"), + ("wrapper", "marks tests for wrapper handler"), + + ("tools", "marks tests for tools"), + ("aggregator", "marks tests for market data aggregator"), + ("gemini", "marks tests that use Gemini model"), ("ollama_gpt", "marks tests that use Ollama GPT model"), ("ollama_qwen", "marks tests that use Ollama Qwen model"), - ("news", "marks tests that use news"), - ("social", "marks tests that use social media"), - ("limited", "marks tests that have limited execution due to API constraints"), - ("wrapper", "marks tests for wrapper handler"), - ("tools", "marks tests for tools"), - ("aggregator", "marks tests for market data aggregator"), ] for marker in markers: line = f"{marker[0]}: {marker[1]}" diff --git a/tests/tools/test_market_tool.py b/tests/tools/test_market_tool.py index 0d6d1a1..e4eff8a 100644 --- a/tests/tools/test_market_tool.py +++ b/tests/tools/test_market_tool.py @@ -3,8 +3,9 @@ import pytest from app.agents.market_agent import MarketToolkit from app.markets import MarketAPIsTool -@pytest.mark.limited # usa molte api calls e non voglio esaurire le chiavi api + @pytest.mark.tools +@pytest.mark.market @pytest.mark.api class TestMarketAPIsTool: def test_wrapper_initialization(self): diff --git a/tests/tools/test_news_tool.py b/tests/tools/test_news_tool.py index 14d142f..cb820ba 100644 --- a/tests/tools/test_news_tool.py +++ b/tests/tools/test_news_tool.py @@ -2,7 +2,6 @@ import pytest from app.news import NewsAPIsTool -@pytest.mark.limited @pytest.mark.tools @pytest.mark.news @pytest.mark.api -- 2.49.1 From daedf6cbba13f7a8f529468b990a68fed4ba6dbf Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 17:08:19 +0200 Subject: [PATCH 03/18] refactor: clean up imports and remove unused files --- .env.example | 3 -- src/app/markets/__init__.py | 7 ++- src/app/markets/yfinance.py | 62 ++++++++++++------------ src/app/toolkits/__init__.py | 0 src/app/toolkits/market_toolkit.py | 29 ----------- src/app/utils/aggregated_models.py | 60 +++++++++++------------ src/app/utils/market_data_aggregator.py | 64 ++++++++++++------------- src/app/utils/wrapper_handler.py | 1 + 8 files changed, 97 insertions(+), 129 deletions(-) delete mode 100644 src/app/toolkits/__init__.py delete mode 100644 src/app/toolkits/market_toolkit.py diff --git a/.env.example b/.env.example index 06a53cb..4cfc34a 100644 --- a/.env.example +++ b/.env.example @@ -6,9 +6,6 @@ # Vedi https://docs.agno.com/examples/models per vedere tutti i modelli supportati GOOGLE_API_KEY= -# Inserire il percorso di installazione di ollama (es. /usr/share/ollama/.ollama) -# attenzione che fra Linux nativo e WSL il percorso è diverso -OLLAMA_MODELS_PATH= ############################################################################### # Configurazioni per gli agenti di mercato ############################################################################### diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index eefc442..aea3504 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -1,13 +1,12 @@ +from typing import List, Optional +from agno.tools import Toolkit +from app.utils.wrapper_handler import WrapperHandler from .base import BaseWrapper, ProductInfo, Price from .coinbase import CoinBaseWrapper from .binance import BinanceWrapper from .cryptocompare import CryptoCompareWrapper from .yfinance import YFinanceWrapper from .binance_public import PublicBinanceAgent -from app.utils.wrapper_handler import WrapperHandler -from typing import List, Optional -from agno.tools import Toolkit - __all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "PublicBinanceAgent" ] diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py index f0e5d6d..82d88fe 100644 --- a/src/app/markets/yfinance.py +++ b/src/app/markets/yfinance.py @@ -8,11 +8,11 @@ def create_product_info(symbol: str, stock_data: dict) -> ProductInfo: Converte i dati di YFinanceTools in ProductInfo. """ product = ProductInfo() - + # ID univoco per yfinance product.id = f"yfinance_{symbol}" product.symbol = symbol - + # Estrai il prezzo corrente - gestisci diversi formati if 'currentPrice' in stock_data: product.price = float(stock_data['currentPrice']) @@ -27,7 +27,7 @@ def create_product_info(symbol: str, stock_data: dict) -> ProductInfo: product.price = 0.0 else: product.price = 0.0 - + # Volume 24h if 'volume' in stock_data: product.volume_24h = float(stock_data['volume']) @@ -35,13 +35,13 @@ def create_product_info(symbol: str, stock_data: dict) -> ProductInfo: product.volume_24h = float(stock_data['regularMarketVolume']) else: product.volume_24h = 0.0 - + # Status basato sulla disponibilità dei dati product.status = "trading" if product.price > 0 else "offline" - + # Valuta (default USD) product.quote_currency = stock_data.get('currency', 'USD') or 'USD' - + return product @@ -50,7 +50,7 @@ def create_price_from_history(hist_data: dict, timestamp: str) -> Price: Converte i dati storici di YFinanceTools in Price. """ price = Price() - + if timestamp in hist_data: day_data = hist_data[timestamp] price.high = float(day_data.get('High', 0.0)) @@ -59,7 +59,7 @@ def create_price_from_history(hist_data: dict, timestamp: str) -> Price: price.close = float(day_data.get('Close', 0.0)) price.volume = float(day_data.get('Volume', 0.0)) price.time = timestamp - + return price @@ -69,41 +69,41 @@ class YFinanceWrapper(BaseWrapper): Implementa l'interfaccia BaseWrapper per compatibilità con il sistema esistente. Usa YFinanceTools dalla libreria agno per coerenza con altri wrapper. """ - + def __init__(self, currency: str = "USD"): self.currency = currency # Inizializza YFinanceTools - non richiede parametri specifici self.tool = YFinanceTools() - + def _format_symbol(self, asset_id: str) -> str: """ Formatta il simbolo per yfinance. Per crypto, aggiunge '-USD' se non presente. """ asset_id = asset_id.upper() - + # Se è già nel formato corretto (es: BTC-USD), usa così if '-' in asset_id: return asset_id - + # Per crypto singole (BTC, ETH), aggiungi -USD if asset_id in ['BTC', 'ETH', 'ADA', 'SOL', 'DOT', 'LINK', 'UNI', 'AAVE']: return f"{asset_id}-USD" - + # Per azioni, usa il simbolo così com'è return asset_id - + def get_product(self, asset_id: str) -> ProductInfo: """ Recupera le informazioni di un singolo prodotto. """ symbol = self._format_symbol(asset_id) - + # Usa YFinanceTools per ottenere i dati try: # Ottieni le informazioni base dello stock stock_info = self.tool.get_company_info(symbol) - + # Se il risultato è una stringa JSON, parsala if isinstance(stock_info, str): try: @@ -118,9 +118,9 @@ class YFinanceWrapper(BaseWrapper): raise Exception("Dati non validi") else: stock_data = stock_info - + return create_product_info(symbol, stock_data) - + except Exception as e: # Fallback: prova a ottenere solo il prezzo try: @@ -140,13 +140,13 @@ class YFinanceWrapper(BaseWrapper): product.symbol = symbol product.status = "offline" return product - + def get_products(self, asset_ids: list[str]) -> list[ProductInfo]: """ Recupera le informazioni di multiple assets. """ products = [] - + for asset_id in asset_ids: try: product = self.get_product(asset_id) @@ -154,9 +154,9 @@ class YFinanceWrapper(BaseWrapper): except Exception as e: # Se un asset non è disponibile, continua con gli altri continue - + return products - + def get_all_products(self) -> list[ProductInfo]: """ Recupera tutti i prodotti disponibili. @@ -168,15 +168,15 @@ class YFinanceWrapper(BaseWrapper): 'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', 'SPY', 'QQQ', 'VTI', 'GLD', 'VIX' ] - + return self.get_products(popular_assets) - + def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: """ Recupera i dati storici di prezzo per un asset. """ symbol = self._format_symbol(asset_id) - + try: # Determina il periodo appropriato in base al limite if limit <= 7: @@ -191,24 +191,24 @@ class YFinanceWrapper(BaseWrapper): else: period = "3mo" interval = "1d" - + # Ottieni i dati storici hist_data = self.tool.get_historical_stock_prices(symbol, period=period, interval=interval) - + if isinstance(hist_data, str): hist_data = json.loads(hist_data) - + # Il formato dei dati è {timestamp: {Open: x, High: y, Low: z, Close: w, Volume: v}} prices = [] timestamps = sorted(hist_data.keys())[-limit:] # Prendi gli ultimi 'limit' timestamp - + for timestamp in timestamps: price = create_price_from_history(hist_data, timestamp) if price.close > 0: # Solo se ci sono dati validi prices.append(price) - + return prices - + except Exception as e: # Se fallisce, restituisci lista vuota return [] \ No newline at end of file diff --git a/src/app/toolkits/__init__.py b/src/app/toolkits/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/toolkits/market_toolkit.py b/src/app/toolkits/market_toolkit.py deleted file mode 100644 index 7267b96..0000000 --- a/src/app/toolkits/market_toolkit.py +++ /dev/null @@ -1,29 +0,0 @@ -from agno.tools import Toolkit -from app.markets import MarketAPIsTool - - -# TODO (?) in futuro fare in modo che la LLM faccia da sé per il mercato -# Non so se può essere utile, per ora lo lascio qui -# per ora mettiamo tutto statico e poi, se abbiamo API-Key senza limiti -# possiamo fare in modo di far scegliere alla LLM quale crypto proporre -# in base alle sue proprie chiamate API -class MarketToolkit(Toolkit): - def __init__(self): - self.market_api = MarketAPIs() - - super().__init__( - name="Market Toolkit", - tools=[ - self.market_api.get_historical_prices, - self.market_api.get_product, - ], - ) - -def instructions(): - return """ - Utilizza questo strumento per ottenere dati di mercato storici e attuali per criptovalute specifiche. - Puoi richiedere i prezzi storici o il prezzo attuale di una criptovaluta specifica. - Esempio di utilizzo: - - get_historical_prices("BTC", limit=10) # ottieni gli ultimi 10 prezzi storici di Bitcoin - - get_product("ETH") - """ diff --git a/src/app/utils/aggregated_models.py b/src/app/utils/aggregated_models.py index ee9f3ef..e4a5ff4 100644 --- a/src/app/utils/aggregated_models.py +++ b/src/app/utils/aggregated_models.py @@ -9,7 +9,7 @@ class AggregationMetadata(BaseModel): sources_ignored: Set[str] = Field(default_factory=set, description="Exchange ignorati (errori)") aggregation_timestamp: str = Field(default="", description="Timestamp dell'aggregazione") confidence_score: float = Field(default=0.0, description="Score 0-1 sulla qualità dei dati") - + class Config: # Nasconde questi campi dalla serializzazione di default extra = "forbid" @@ -19,15 +19,15 @@ class AggregatedProductInfo(ProductInfo): Versione aggregata di ProductInfo che mantiene la trasparenza per l'utente finale mentre fornisce metadati di debugging opzionali. """ - + # Override dei campi con logica di aggregazione id: str = Field(description="ID aggregato basato sul simbolo standardizzato") status: str = Field(description="Status aggregato (majority vote o conservative)") - + # Campi privati per debugging (non visibili di default) _metadata: Optional[AggregationMetadata] = PrivateAttr(default=None) _source_data: Optional[Dict[str, ProductInfo]] = PrivateAttr(default=None) - + @classmethod def from_multiple_sources(cls, products: List[ProductInfo]) -> 'AggregatedProductInfo': """ @@ -36,37 +36,37 @@ class AggregatedProductInfo(ProductInfo): """ if not products: raise ValueError("Nessun prodotto da aggregare") - + # Raggruppa per symbol (la chiave vera per l'aggregazione) symbol_groups = {} for product in products: if product.symbol not in symbol_groups: symbol_groups[product.symbol] = [] symbol_groups[product.symbol].append(product) - + # Per ora gestiamo un symbol alla volta if len(symbol_groups) > 1: raise ValueError(f"Simboli multipli non supportati: {list(symbol_groups.keys())}") - + symbol_products = list(symbol_groups.values())[0] - + # Estrai tutte le fonti sources = [] for product in symbol_products: # Determina la fonte dall'ID o da altri metadati se disponibili source = cls._detect_source(product) sources.append(source) - + # Aggrega i dati aggregated_data = cls._aggregate_products(symbol_products, sources) - + # Crea l'istanza e assegna gli attributi privati instance = cls(**aggregated_data) instance._metadata = aggregated_data.get("_metadata") instance._source_data = aggregated_data.get("_source_data") - + return instance - + @staticmethod def _detect_source(product: ProductInfo) -> str: """Rileva la fonte da un ProductInfo""" @@ -81,7 +81,7 @@ class AggregatedProductInfo(ProductInfo): return "yfinance" else: return "unknown" - + @classmethod def _aggregate_products(cls, products: List[ProductInfo], sources: List[str]) -> dict: """ @@ -90,11 +90,11 @@ class AggregatedProductInfo(ProductInfo): """ import statistics from datetime import datetime - + # ID: usa il symbol come chiave standardizzata symbol = products[0].symbol aggregated_id = f"{symbol}_AGG" - + # Status: strategia "conservativa" - il più restrittivo vince # Ordine: trading_only < limit_only < auction < maintenance < offline status_priority = { @@ -105,41 +105,41 @@ class AggregatedProductInfo(ProductInfo): "offline": 5, "": 0 # Default se non specificato } - + statuses = [p.status for p in products if p.status] if statuses: # Prendi lo status con priorità più alta (più restrittivo) aggregated_status = max(statuses, key=lambda s: status_priority.get(s, 0)) else: aggregated_status = "trading" # Default ottimistico - + # Prezzo: media semplice (uso diretto del campo price come float) prices = [p.price for p in products if p.price > 0] aggregated_price = statistics.mean(prices) if prices else 0.0 - + # Volume: somma (assumendo che i volumi siano esclusivi per exchange) volumes = [p.volume_24h for p in products if p.volume_24h > 0] total_volume = sum(volumes) aggregated_volume = sum(price_i * volume_i for price_i, volume_i in zip((p.price for p in products), (volume for volume in volumes))) / total_volume aggregated_volume = round(aggregated_volume, 5) # aggregated_volume = sum(volumes) if volumes else 0.0 # NOTE old implementation - + # Valuta: prendi la prima (dovrebbero essere tutte uguali) quote_currency = next((p.quote_currency for p in products if p.quote_currency), "USD") - + # Calcola confidence score confidence = cls._calculate_confidence(products, sources) - + # Crea metadati per debugging metadata = AggregationMetadata( sources_used=set(sources), aggregation_timestamp=datetime.now().isoformat(), confidence_score=confidence ) - + # Salva dati sorgente per debugging source_data = dict(zip(sources, products)) - + return { "symbol": symbol, "price": aggregated_price, @@ -150,33 +150,33 @@ class AggregatedProductInfo(ProductInfo): "_metadata": metadata, "_source_data": source_data } - + @staticmethod def _calculate_confidence(products: List[ProductInfo], sources: List[str]) -> float: """Calcola un punteggio di confidenza 0-1""" if not products: return 0.0 - + score = 1.0 - + # Riduci score se pochi dati if len(products) < 2: score *= 0.7 - + # Riduci score se prezzi troppo diversi prices = [p.price for p in products if p.price > 0] if len(prices) > 1: price_std = (max(prices) - min(prices)) / statistics.mean(prices) if price_std > 0.05: # >5% variazione score *= 0.8 - + # Riduci score se fonti sconosciute unknown_sources = sum(1 for s in sources if s == "unknown") if unknown_sources > 0: score *= (1 - unknown_sources / len(sources)) - + return max(0.0, min(1.0, score)) - + def get_debug_info(self) -> dict: """Metodo opzionale per ottenere informazioni di debug""" return { diff --git a/src/app/utils/market_data_aggregator.py b/src/app/utils/market_data_aggregator.py index ea2d7c0..9c6206d 100644 --- a/src/app/utils/market_data_aggregator.py +++ b/src/app/utils/market_data_aggregator.py @@ -5,17 +5,17 @@ from app.utils.aggregated_models import AggregatedProductInfo class MarketDataAggregator: """ Aggregatore di dati di mercato che mantiene la trasparenza per l'utente. - + Compone MarketAPIs per fornire gli stessi metodi, ma restituisce dati aggregati da tutte le fonti disponibili. L'utente finale non vede la complessità. """ - + def __init__(self, currency: str = "USD"): # Import lazy per evitare circular import from app.markets import MarketAPIsTool self._market_apis = MarketAPIsTool(currency) self._aggregation_enabled = True - + def get_product(self, asset_id: str) -> ProductInfo: """ Override che aggrega dati da tutte le fonti disponibili. @@ -23,13 +23,13 @@ class MarketDataAggregator: """ if not self._aggregation_enabled: return self._market_apis.get_product(asset_id) - + # Raccogli dati da tutte le fonti try: raw_results = self.wrappers.try_call_all( lambda wrapper: wrapper.get_product(asset_id) ) - + # Converti in ProductInfo se necessario products = [] for wrapper_class, result in raw_results.items(): @@ -38,29 +38,29 @@ class MarketDataAggregator: elif isinstance(result, dict): # Converti dizionario in ProductInfo products.append(ProductInfo(**result)) - + if not products: raise Exception("Nessun dato disponibile") - + # Aggrega i risultati aggregated = AggregatedProductInfo.from_multiple_sources(products) - + # Restituisci come ProductInfo normale (nascondi la complessità) return ProductInfo(**aggregated.dict(exclude={"_metadata", "_source_data"})) - + except Exception as e: # Fallback: usa il comportamento normale se l'aggregazione fallisce return self._market_apis.get_product(asset_id) - + def get_products(self, asset_ids: List[str]) -> List[ProductInfo]: """ Aggrega dati per multiple asset. """ if not self._aggregation_enabled: return self._market_apis.get_products(asset_ids) - + aggregated_products = [] - + for asset_id in asset_ids: try: product = self.get_product(asset_id) @@ -68,36 +68,36 @@ class MarketDataAggregator: except Exception as e: # Salta asset che non riescono ad aggregare continue - + return aggregated_products - + def get_all_products(self) -> List[ProductInfo]: """ Aggrega tutti i prodotti disponibili. """ if not self._aggregation_enabled: return self._market_apis.get_all_products() - + # Raccogli tutti i prodotti da tutte le fonti try: all_products_by_source = self.wrappers.try_call_all( lambda wrapper: wrapper.get_all_products() ) - + # Raggruppa per symbol per aggregare symbol_groups = {} for wrapper_class, products in all_products_by_source.items(): if not isinstance(products, list): continue - + for product in products: if isinstance(product, dict): product = ProductInfo(**product) - + if product.symbol not in symbol_groups: symbol_groups[product.symbol] = [] symbol_groups[product.symbol].append(product) - + # Aggrega ogni gruppo aggregated_products = [] for symbol, products in symbol_groups.items(): @@ -111,13 +111,13 @@ class MarketDataAggregator: # Se l'aggregazione fallisce, usa il primo disponibile if products: aggregated_products.append(products[0]) - + return aggregated_products - + except Exception as e: # Fallback: usa il comportamento normale return self._market_apis.get_all_products() - + def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> List[Price]: """ Per i dati storici, usa una strategia diversa: @@ -125,7 +125,7 @@ class MarketDataAggregator: """ if not self._aggregation_enabled: return self._market_apis.get_historical_prices(asset_id, limit) - + # Per dati storici, usa il primo wrapper che funziona # (l'aggregazione di dati storici è più complessa) try: @@ -135,21 +135,21 @@ class MarketDataAggregator: except Exception as e: # Fallback: usa il comportamento normale return self._market_apis.get_historical_prices(asset_id, limit) - + def enable_aggregation(self, enabled: bool = True): """Abilita o disabilita l'aggregazione""" self._aggregation_enabled = enabled - + def is_aggregation_enabled(self) -> bool: """Controlla se l'aggregazione è abilitata""" return self._aggregation_enabled - + # Metodi proxy per completare l'interfaccia BaseWrapper @property def wrappers(self): """Accesso al wrapper handler per compatibilità""" return self._market_apis.wrappers - + def get_aggregated_product_with_debug(self, asset_id: str) -> Dict[str, Any]: """ Metodo speciale per debugging: restituisce dati aggregati con metadati. @@ -159,24 +159,24 @@ class MarketDataAggregator: raw_results = self.wrappers.try_call_all( lambda wrapper: wrapper.get_product(asset_id) ) - + products = [] for wrapper_class, result in raw_results.items(): if isinstance(result, ProductInfo): products.append(result) elif isinstance(result, dict): products.append(ProductInfo(**result)) - + if not products: raise Exception("Nessun dato disponibile") - + aggregated = AggregatedProductInfo.from_multiple_sources(products) - + return { "product": aggregated.dict(exclude={"_metadata", "_source_data"}), "debug": aggregated.get_debug_info() } - + except Exception as e: return { "error": str(e), diff --git a/src/app/utils/wrapper_handler.py b/src/app/utils/wrapper_handler.py index 4f22a8e..19181ea 100644 --- a/src/app/utils/wrapper_handler.py +++ b/src/app/utils/wrapper_handler.py @@ -94,6 +94,7 @@ class WrapperHandler(Generic[W]): def __check(wrappers: list[W]) -> bool: return all(w.__class__ is type for w in wrappers) + @staticmethod def __concise_error(e: Exception) -> str: last_frame = traceback.extract_tb(e.__traceback__)[-1] return f"{e} [\"{last_frame.filename}\", line {last_frame.lineno}]" -- 2.49.1 From ca67eca4c4bae6118641a16ed533bdc06c50a383 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 17:22:45 +0200 Subject: [PATCH 04/18] refactor: remove unused agent files and clean up market API instructions --- src/app/agents/__init__.py | 0 src/app/agents/market_agent.py | 90 ---------------------------------- src/app/agents/news_agent.py | 35 ------------- src/app/agents/social_agent.py | 36 -------------- src/app/markets/__init__.py | 27 +++++----- src/app/pipeline.py | 46 ++++++++++++----- 6 files changed, 51 insertions(+), 183 deletions(-) delete mode 100644 src/app/agents/__init__.py delete mode 100644 src/app/agents/market_agent.py delete mode 100644 src/app/agents/news_agent.py delete mode 100644 src/app/agents/social_agent.py diff --git a/src/app/agents/__init__.py b/src/app/agents/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/app/agents/market_agent.py b/src/app/agents/market_agent.py deleted file mode 100644 index 12f9eab..0000000 --- a/src/app/agents/market_agent.py +++ /dev/null @@ -1,90 +0,0 @@ -from typing import Union, List, Dict, Optional, Any, Iterator, Sequence -from agno.agent import Agent -from agno.models.message import Message -from agno.run.agent import RunOutput, RunOutputEvent -from pydantic import BaseModel - -from app.toolkits.market_toolkit import MarketToolkit -from app.markets.base import ProductInfo # modello dati già definito nel tuo progetto - - -class MarketAgent(Agent): - """ - Wrapper che trasforma MarketToolkit in un Agent compatibile con Team. - Produce sia output leggibile (content) che dati strutturati (metadata). - """ - - def __init__(self, currency: str = "USD"): - super().__init__() - self.toolkit = MarketToolkit() - self.currency = currency - self.name = "MarketAgent" - - def run( - self, - input: Union[str, List, Dict, Message, BaseModel, List[Message]], - *, - stream: Optional[bool] = None, - stream_intermediate_steps: Optional[bool] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - session_state: Optional[Dict[str, Any]] = None, - audio: Optional[Sequence[Any]] = None, - images: Optional[Sequence[Any]] = None, - videos: Optional[Sequence[Any]] = None, - files: Optional[Sequence[Any]] = None, - retries: Optional[int] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - add_history_to_context: Optional[bool] = None, - add_dependencies_to_context: Optional[bool] = None, - add_session_state_to_context: Optional[bool] = None, - dependencies: Optional[Dict[str, Any]] = None, - metadata: Optional[Dict[str, Any]] = None, - yield_run_response: bool = False, - debug_mode: Optional[bool] = None, - **kwargs: Any, - ) -> Union[RunOutput, Iterator[Union[RunOutputEvent, RunOutput]]]: - # 1. Estraggo la query dal parametro "input" - if isinstance(input, str): - query = input - elif isinstance(input, dict) and "query" in input: - query = input["query"] - elif isinstance(input, Message): - query = input.content - elif isinstance(input, BaseModel): - query = str(input) - elif isinstance(input, list) and input and isinstance(input[0], Message): - query = input[0].content - else: - query = str(input) - - # 2. Individuo i simboli da analizzare - symbols = [] - for token in query.upper().split(): - if token in ("BTC", "ETH", "XRP", "LTC", "BCH"): # TODO: estendere dinamicamente - symbols.append(token) - - if not symbols: - symbols = ["BTC", "ETH"] # default - - # 3. Recupero i dati dal toolkit - results = [] - products: List[ProductInfo] = [] - - try: - products.extend(self.toolkit.get_current_prices(symbols)) # supponiamo ritorni un ProductInfo o simile - # Usa list comprehension per iterare symbols e products insieme - results.extend([ - f"{symbol}: ${product.price:.2f}" if hasattr(product, 'price') and product.price else f"{symbol}: N/A" - for symbol, product in zip(symbols, products) - ]) - except Exception as e: - results.extend(f"Errore: Impossibile recuperare i dati di mercato\n{str(e)}") - - # 4. Preparo output leggibile + metadati strutturati - output_text = "📊 Dati di mercato:\n" + "\n".join(results) - - return RunOutput( - content=output_text, - metadata={"products": products} - ) diff --git a/src/app/agents/news_agent.py b/src/app/agents/news_agent.py deleted file mode 100644 index d6de5e5..0000000 --- a/src/app/agents/news_agent.py +++ /dev/null @@ -1,35 +0,0 @@ -from agno.agent import Agent - -class NewsAgent(Agent): - """ - Gli agenti devono esporre un metodo run con questa firma. - - def run( - self, - input: Union[str, List, Dict, Message, BaseModel, List[Message]], - *, - stream: Optional[bool] = None, - stream_intermediate_steps: Optional[bool] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - session_state: Optional[Dict[str, Any]] = None, - audio: Optional[Sequence[Any]] = None, - images: Optional[Sequence[Any]] = None, - videos: Optional[Sequence[Any]] = None, - files: Optional[Sequence[Any]] = None, - retries: Optional[int] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - add_history_to_context: Optional[bool] = None, - add_dependencies_to_context: Optional[bool] = None, - add_session_state_to_context: Optional[bool] = None, - dependencies: Optional[Dict[str, Any]] = None, - metadata: Optional[Dict[str, Any]] = None, - yield_run_response: bool = False, - debug_mode: Optional[bool] = None, - **kwargs: Any, - ) -> Union[RunOutput, Iterator[Union[RunOutputEvent, RunOutput]]]: - """ - @staticmethod - def analyze(query: str) -> str: - # Mock analisi news - return "📰 Sentiment news: ottimismo sul mercato crypto grazie all'adozione istituzionale." diff --git a/src/app/agents/social_agent.py b/src/app/agents/social_agent.py deleted file mode 100644 index cefa7ef..0000000 --- a/src/app/agents/social_agent.py +++ /dev/null @@ -1,36 +0,0 @@ -from agno.agent import Agent - - -class SocialAgent(Agent): - """ - Gli agenti devono esporre un metodo run con questa firma. - - def run( - self, - input: Union[str, List, Dict, Message, BaseModel, List[Message]], - *, - stream: Optional[bool] = None, - stream_intermediate_steps: Optional[bool] = None, - user_id: Optional[str] = None, - session_id: Optional[str] = None, - session_state: Optional[Dict[str, Any]] = None, - audio: Optional[Sequence[Any]] = None, - images: Optional[Sequence[Any]] = None, - videos: Optional[Sequence[Any]] = None, - files: Optional[Sequence[Any]] = None, - retries: Optional[int] = None, - knowledge_filters: Optional[Dict[str, Any]] = None, - add_history_to_context: Optional[bool] = None, - add_dependencies_to_context: Optional[bool] = None, - add_session_state_to_context: Optional[bool] = None, - dependencies: Optional[Dict[str, Any]] = None, - metadata: Optional[Dict[str, Any]] = None, - yield_run_response: bool = False, - debug_mode: Optional[bool] = None, - **kwargs: Any, - ) -> Union[RunOutput, Iterator[Union[RunOutputEvent, RunOutput]]]: - """ - @staticmethod - def analyze(query: str) -> str: - # Mock analisi social - return "💬 Sentiment social: forte interesse retail su nuove altcoin emergenti." diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index aea3504..30a24fb 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -14,11 +14,11 @@ __all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWra class MarketAPIsTool(BaseWrapper, Toolkit): """ Classe per gestire le API di mercato disponibili. - + Supporta due modalità: 1. **Modalità standard** (default): usa il primo wrapper disponibile 2. **Modalità aggregazione**: aggrega dati da tutte le fonti disponibili - + L'aggregazione può essere abilitata/disabilitata dinamicamente. """ @@ -26,11 +26,11 @@ class MarketAPIsTool(BaseWrapper, Toolkit): self.currency = currency wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ] self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers) - + # Inizializza l'aggregatore solo se richiesto (lazy initialization) self._aggregator = None self._aggregation_enabled = enable_aggregation - + Toolkit.__init__( self, name="Market APIs Toolkit", @@ -41,7 +41,7 @@ class MarketAPIsTool(BaseWrapper, Toolkit): self.get_historical_prices, ], ) - + def _get_aggregator(self): """Lazy initialization dell'aggregatore""" if self._aggregator is None: @@ -55,36 +55,36 @@ class MarketAPIsTool(BaseWrapper, Toolkit): if self._aggregation_enabled: return self._get_aggregator().get_product(asset_id) return self.wrappers.try_call(lambda w: w.get_product(asset_id)) - + def get_products(self, asset_ids: List[str]) -> List[ProductInfo]: """Ottieni informazioni su multiple prodotti""" if self._aggregation_enabled: return self._get_aggregator().get_products(asset_ids) return self.wrappers.try_call(lambda w: w.get_products(asset_ids)) - + def get_all_products(self) -> List[ProductInfo]: """Ottieni tutti i prodotti disponibili""" if self._aggregation_enabled: return self._get_aggregator().get_all_products() return self.wrappers.try_call(lambda w: w.get_all_products()) - + def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> List[Price]: """Ottieni dati storici dei prezzi""" if self._aggregation_enabled: return self._get_aggregator().get_historical_prices(asset_id, limit) return self.wrappers.try_call(lambda w: w.get_historical_prices(asset_id, limit)) - + # Metodi per controllare l'aggregazione def enable_aggregation(self, enabled: bool = True): """Abilita/disabilita la modalità aggregazione""" self._aggregation_enabled = enabled if self._aggregator: self._aggregator.enable_aggregation(enabled) - + def is_aggregation_enabled(self) -> bool: """Verifica se l'aggregazione è abilitata""" return self._aggregation_enabled - + # Metodo speciale per debugging (opzionale) def get_aggregated_product_with_debug(self, asset_id: str) -> dict: """ @@ -94,3 +94,8 @@ class MarketAPIsTool(BaseWrapper, Toolkit): if not self._aggregation_enabled: raise RuntimeError("L'aggregazione deve essere abilitata per usare questo metodo") return self._get_aggregator().get_aggregated_product_with_debug(asset_id) + +# TODO definire istruzioni per gli agenti di mercato +MARKET_INSTRUCTIONS = """ + +""" \ No newline at end of file diff --git a/src/app/pipeline.py b/src/app/pipeline.py index 7a440de..10dddab 100644 --- a/src/app/pipeline.py +++ b/src/app/pipeline.py @@ -1,11 +1,9 @@ -from typing import List - from agno.team import Team from agno.utils.log import log_info -from app.agents.market_agent import MarketAgent -from app.agents.news_agent import NewsAgent -from app.agents.social_agent import SocialAgent +from app.news import NewsAPIsTool, NEWS_INSTRUCTIONS +from app.social import SocialAPIsTool, SOCIAL_INSTRUCTIONS +from app.markets import MarketAPIsTool, MARKET_INSTRUCTIONS from app.models import AppModels from app.predictor import PredictorStyle, PredictorInput, PredictorOutput, PREDICTOR_INSTRUCTIONS @@ -17,12 +15,38 @@ class Pipeline: def __init__(self): # Inizializza gli agenti - self.market_agent = MarketAgent() - self.news_agent = NewsAgent() - self.social_agent = SocialAgent() + market_agent = AppModels.OLLAMA_QWEN_1B.get_agent( + instructions=MARKET_INSTRUCTIONS, + name="MarketAgent", + tools=[MarketAPIsTool()] + ) + news_agent = AppModels.OLLAMA_QWEN_1B.get_agent( + instructions=NEWS_INSTRUCTIONS, + name="NewsAgent", + tools=[NewsAPIsTool()] + ) + social_agent = AppModels.OLLAMA_QWEN_1B.get_agent( + instructions=SOCIAL_INSTRUCTIONS, + name="SocialAgent", + tools=[SocialAPIsTool()] + ) # Crea il Team - self.team = Team(name="CryptoAnalysisTeam", members=[self.market_agent, self.news_agent, self.social_agent]) + prompt = """ + You are the coordinator of a team of analysts specialized in cryptocurrency market analysis. + Your role is to gather insights from various sources, including market data, news articles, and social media trends. + Based on the information provided by your team members, you will synthesize a comprehensive sentiment analysis for each cryptocurrency discussed. + Your analysis should consider the following aspects: + 1. Market Trends: Evaluate the current market trends and price movements. + 2. News Impact: Assess the impact of recent news articles on market sentiment. + 3. Social Media Buzz: Analyze social media discussions and trends related to the cryptocurrencies. + Your final output should be a well-rounded sentiment analysis that can guide investment decisions. + """ # TODO migliorare il prompt + self.team = Team( + model = AppModels.OLLAMA_QWEN_1B.get_model(prompt), + name="CryptoAnalysisTeam", + members=[market_agent, news_agent, social_agent] + ) # Modelli disponibili e Predictor self.available_models = AppModels.availables() @@ -76,8 +100,8 @@ class Pipeline: return output - def list_providers(self) -> List[str]: + def list_providers(self) -> list[str]: return [m.name for m in self.available_models] - def list_styles(self) -> List[str]: + def list_styles(self) -> list[str]: return [s.value for s in self.styles] -- 2.49.1 From 1e7e10ab4466169b154f32527238ab00383aecaa Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 18:03:42 +0200 Subject: [PATCH 05/18] refactor: enhance wrapper initialization with keyword arguments and clean up tests --- src/app/markets/__init__.py | 4 ++-- src/app/utils/wrapper_handler.py | 5 +++-- tests/tools/test_market_tool.py | 29 ----------------------------- 3 files changed, 5 insertions(+), 33 deletions(-) diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index 30a24fb..a2efa16 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -23,9 +23,9 @@ class MarketAPIsTool(BaseWrapper, Toolkit): """ def __init__(self, currency: str = "USD", enable_aggregation: bool = False): - self.currency = currency + kwargs = {"currency": currency or "USD"} wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ] - self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers) + self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs) # Inizializza l'aggregatore solo se richiesto (lazy initialization) self._aggregator = None diff --git a/src/app/utils/wrapper_handler.py b/src/app/utils/wrapper_handler.py index 19181ea..3429c13 100644 --- a/src/app/utils/wrapper_handler.py +++ b/src/app/utils/wrapper_handler.py @@ -100,7 +100,7 @@ class WrapperHandler(Generic[W]): return f"{e} [\"{last_frame.filename}\", line {last_frame.lineno}]" @staticmethod - def build_wrappers(constructors: Iterable[Type[W]], try_per_wrapper: int = 3, retry_delay: int = 2) -> 'WrapperHandler[W]': + def build_wrappers(constructors: Iterable[Type[W]], try_per_wrapper: int = 3, retry_delay: int = 2, kwargs: dict | None = None) -> 'WrapperHandler[W]': """ Builds a WrapperHandler instance with the given wrapper constructors. It attempts to initialize each wrapper and logs a warning if any cannot be initialized. @@ -109,6 +109,7 @@ class WrapperHandler(Generic[W]): constructors (Iterable[Type[W]]): An iterable of wrapper classes to instantiate. e.g. [WrapperA, WrapperB] try_per_wrapper (int): Number of retries per wrapper before switching to the next. retry_delay (int): Delay in seconds between retries. + kwargs (dict | None): Optional dictionary with keyword arguments common to all wrappers. Returns: WrapperHandler[W]: An instance of WrapperHandler with the initialized wrappers. Raises: @@ -119,7 +120,7 @@ class WrapperHandler(Generic[W]): result = [] for wrapper_class in constructors: try: - wrapper = wrapper_class() + wrapper = wrapper_class(**(kwargs or {})) result.append(wrapper) except Exception as e: log_warning(f"{wrapper_class} cannot be initialized: {e}") diff --git a/tests/tools/test_market_tool.py b/tests/tools/test_market_tool.py index e4eff8a..7513585 100644 --- a/tests/tools/test_market_tool.py +++ b/tests/tools/test_market_tool.py @@ -1,6 +1,4 @@ -import os import pytest -from app.agents.market_agent import MarketToolkit from app.markets import MarketAPIsTool @@ -35,27 +33,6 @@ class TestMarketAPIsTool: assert hasattr(btc_product, 'price') assert btc_product.price > 0 - def test_market_toolkit_integration(self): - try: - toolkit = MarketToolkit() - assert toolkit is not None - assert hasattr(toolkit, 'market_agent') - assert toolkit.market_api is not None - - tools = toolkit.tools - assert len(tools) > 0 - - except Exception as e: - print(f"MarketToolkit test failed: {e}") - # Non fail completamente - il toolkit potrebbe avere dipendenze specifiche - - def test_provider_selection_mechanism(self): - potential_providers = 0 - if os.getenv('CDP_API_KEY_NAME') and os.getenv('CDP_API_PRIVATE_KEY'): - potential_providers += 1 - if os.getenv('CRYPTOCOMPARE_API_KEY'): - potential_providers += 1 - def test_error_handling(self): try: market_wrapper = MarketAPIsTool("USD") @@ -63,9 +40,3 @@ class TestMarketAPIsTool: assert fake_product is None or fake_product.price == 0 except Exception as e: pass - - def test_wrapper_currency_support(self): - market_wrapper = MarketAPIsTool("USD") - assert hasattr(market_wrapper, 'currency') - assert isinstance(market_wrapper.currency, str) - assert len(market_wrapper.currency) >= 3 # USD, EUR, etc. -- 2.49.1 From ff1c70153690f77d0ebdd7d2592d534f1e2e4a90 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 18:17:16 +0200 Subject: [PATCH 06/18] refactor: remove PublicBinanceAgent --- src/app/markets/binance_public.py | 218 ------------------------------ 1 file changed, 218 deletions(-) delete mode 100644 src/app/markets/binance_public.py diff --git a/src/app/markets/binance_public.py b/src/app/markets/binance_public.py deleted file mode 100644 index c1d9896..0000000 --- a/src/app/markets/binance_public.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -Versione pubblica di Binance per accesso ai dati pubblici senza autenticazione. - -Questa implementazione estende BaseWrapper per mantenere coerenza -con l'architettura del modulo markets. -""" - -from typing import Optional, Dict, Any -from datetime import datetime, timedelta -from binance.client import Client -from .base import BaseWrapper, ProductInfo, Price - - -class PublicBinanceAgent(BaseWrapper): - """ - Agent per l'accesso ai dati pubblici di Binance. - - Utilizza l'API pubblica di Binance per ottenere informazioni - sui prezzi e sui mercati senza richiedere autenticazione. - """ - - def __init__(self): - """Inizializza il client pubblico senza credenziali.""" - self.client = Client() - - def __format_symbol(self, asset_id: str) -> str: - """ - Formatta l'asset_id per Binance (es. BTC -> BTCUSDT). - - Args: - asset_id: ID dell'asset (es. "BTC", "ETH") - - Returns: - Simbolo formattato per Binance - """ - if asset_id.endswith("USDT") or asset_id.endswith("BUSD"): - return asset_id - return f"{asset_id}USDT" - - def get_product(self, asset_id: str) -> ProductInfo: - """ - Ottiene informazioni su un singolo prodotto. - - Args: - asset_id: ID dell'asset (es. "BTC") - - Returns: - Oggetto ProductInfo con le informazioni del prodotto - """ - symbol = self.__format_symbol(asset_id) - try: - ticker = self.client.get_symbol_ticker(symbol=symbol) - ticker_24h = self.client.get_ticker(symbol=symbol) - return ProductInfo.from_binance(ticker, ticker_24h) - except Exception as e: - print(f"Errore nel recupero del prodotto {asset_id}: {e}") - return ProductInfo(id=asset_id, symbol=asset_id) - - def get_products(self, asset_ids: list[str]) -> list[ProductInfo]: - """ - Ottiene informazioni su più prodotti. - - Args: - asset_ids: Lista di ID degli asset - - Returns: - Lista di oggetti ProductInfo - """ - products = [] - for asset_id in asset_ids: - product = self.get_product(asset_id) - products.append(product) - return products - - def get_all_products(self) -> list[ProductInfo]: - """ - Ottiene informazioni su tutti i prodotti disponibili. - - Returns: - Lista di oggetti ProductInfo per i principali asset - """ - # Per la versione pubblica, restituiamo solo i principali asset - major_assets = ["BTC", "ETH", "BNB", "ADA", "DOT", "LINK", "LTC", "XRP"] - return self.get_products(major_assets) - - def get_historical_prices(self, asset_id: str = "BTC") -> list[Price]: - """ - Ottiene i prezzi storici per un asset. - - Args: - asset_id: ID dell'asset (default: "BTC") - - Returns: - Lista di oggetti Price con i dati storici - """ - symbol = self.__format_symbol(asset_id) - try: - # Ottieni candele degli ultimi 30 giorni - end_time = datetime.now() - start_time = end_time - timedelta(days=30) - - klines = self.client.get_historical_klines( - symbol, - Client.KLINE_INTERVAL_1DAY, - start_time.strftime("%d %b %Y %H:%M:%S"), - end_time.strftime("%d %b %Y %H:%M:%S") - ) - - prices = [] - for kline in klines: - price = Price( - open=float(kline[1]), - high=float(kline[2]), - low=float(kline[3]), - close=float(kline[4]), - volume=float(kline[5]), - time=str(datetime.fromtimestamp(kline[0] / 1000)) - ) - prices.append(price) - - return prices - except Exception as e: - print(f"Errore nel recupero dei prezzi storici per {asset_id}: {e}") - return [] - - def get_public_prices(self, symbols: Optional[list[str]] = None) -> Optional[Dict[str, Any]]: - """ - Ottiene i prezzi pubblici per i simboli specificati. - - Args: - symbols: Lista di simboli da recuperare (es. ["BTCUSDT", "ETHUSDT"]). - Se None, recupera BTC e ETH di default. - - Returns: - Dizionario con i prezzi e informazioni sulla fonte, o None in caso di errore. - """ - if symbols is None: - symbols = ["BTCUSDT", "ETHUSDT"] - - try: - prices = {} - for symbol in symbols: - ticker = self.client.get_symbol_ticker(symbol=symbol) - # Converte BTCUSDT -> BTC_USD per consistenza - clean_symbol = symbol.replace("USDT", "_USD").replace("BUSD", "_USD") - prices[clean_symbol] = float(ticker['price']) - - return { - **prices, - 'source': 'binance_public', - 'timestamp': self.client.get_server_time()['serverTime'] - } - except Exception as e: - print(f"Errore nel recupero dei prezzi pubblici: {e}") - return None - - def get_24hr_ticker(self, symbol: str) -> Optional[Dict[str, Any]]: - """ - Ottiene le statistiche 24h per un simbolo specifico. - - Args: - symbol: Simbolo del trading pair (es. "BTCUSDT") - - Returns: - Dizionario con le statistiche 24h o None in caso di errore. - """ - try: - ticker = self.client.get_ticker(symbol=symbol) - return { - 'symbol': ticker['symbol'], - 'price': float(ticker['lastPrice']), - 'price_change': float(ticker['priceChange']), - 'price_change_percent': float(ticker['priceChangePercent']), - 'high_24h': float(ticker['highPrice']), - 'low_24h': float(ticker['lowPrice']), - 'volume_24h': float(ticker['volume']), - 'source': 'binance_public' - } - except Exception as e: - print(f"Errore nel recupero del ticker 24h per {symbol}: {e}") - return None - - def get_exchange_info(self) -> Optional[Dict[str, Any]]: - """ - Ottiene informazioni generali sull'exchange. - - Returns: - Dizionario con informazioni sull'exchange o None in caso di errore. - """ - try: - info = self.client.get_exchange_info() - return { - 'timezone': info['timezone'], - 'server_time': info['serverTime'], - 'symbols_count': len(info['symbols']), - 'source': 'binance_public' - } - except Exception as e: - print(f"Errore nel recupero delle informazioni exchange: {e}") - return None - - -# Esempio di utilizzo -if __name__ == "__main__": - # Uso senza credenziali - public_agent = PublicBinanceAgent() - - # Ottieni prezzi di default (BTC e ETH) - public_prices = public_agent.get_public_prices() - print("Prezzi pubblici:", public_prices) - - # Ottieni statistiche 24h per BTC - btc_stats = public_agent.get_24hr_ticker("BTCUSDT") - print("Statistiche BTC 24h:", btc_stats) - - # Ottieni informazioni exchange - exchange_info = public_agent.get_exchange_info() - print("Info exchange:", exchange_info) \ No newline at end of file -- 2.49.1 From 020b22a75689dce3f85951f3bb70fc6b138ebc84 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 18:39:07 +0200 Subject: [PATCH 07/18] refactor: aggregator - simplified MarketDataAggregator and related models to functions --- src/app/markets/__init__.py | 91 ++++------ src/app/markets/base.py | 1 - src/app/utils/aggregated_models.py | 186 --------------------- src/app/utils/market_aggregation.py | 82 +++++++++ src/app/utils/market_data_aggregator.py | 184 -------------------- tests/utils/test_market_data_aggregator.py | 168 ++++++++++--------- 6 files changed, 207 insertions(+), 505 deletions(-) delete mode 100644 src/app/utils/aggregated_models.py create mode 100644 src/app/utils/market_aggregation.py delete mode 100644 src/app/utils/market_data_aggregator.py diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index a2efa16..d0eb6ad 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -1,36 +1,27 @@ -from typing import List, Optional from agno.tools import Toolkit from app.utils.wrapper_handler import WrapperHandler +from app.utils.market_aggregation import aggregate_product_info, aggregate_history_prices from .base import BaseWrapper, ProductInfo, Price from .coinbase import CoinBaseWrapper from .binance import BinanceWrapper from .cryptocompare import CryptoCompareWrapper from .yfinance import YFinanceWrapper -from .binance_public import PublicBinanceAgent -__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper", "PublicBinanceAgent" ] +__all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWrapper", "YFinanceWrapper" ] class MarketAPIsTool(BaseWrapper, Toolkit): """ - Classe per gestire le API di mercato disponibili. - - Supporta due modalità: - 1. **Modalità standard** (default): usa il primo wrapper disponibile - 2. **Modalità aggregazione**: aggrega dati da tutte le fonti disponibili - - L'aggregazione può essere abilitata/disabilitata dinamicamente. + Classe per comporre più MarketAPI con gestione degli errori e aggregazione dei dati. + Usa WrapperHandler per gestire più API con logica di retry e failover. + Si può scegliere se aggregare i dati da tutte le fonti o usare una singola fonte tramite delle chiamate apposta. """ - def __init__(self, currency: str = "USD", enable_aggregation: bool = False): + def __init__(self, currency: str = "USD"): kwargs = {"currency": currency or "USD"} wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ] self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs) - # Inizializza l'aggregatore solo se richiesto (lazy initialization) - self._aggregator = None - self._aggregation_enabled = enable_aggregation - Toolkit.__init__( self, name="Market APIs Toolkit", @@ -39,61 +30,45 @@ class MarketAPIsTool(BaseWrapper, Toolkit): self.get_products, self.get_all_products, self.get_historical_prices, + self.get_products_aggregated, + self.get_historical_prices_aggregated, ], ) - def _get_aggregator(self): - """Lazy initialization dell'aggregatore""" - if self._aggregator is None: - from app.utils.market_data_aggregator import MarketDataAggregator - self._aggregator = MarketDataAggregator(self.currency) - self._aggregator.enable_aggregation(self._aggregation_enabled) - return self._aggregator - - def get_product(self, asset_id: str) -> Optional[ProductInfo]: - """Ottieni informazioni su un prodotto specifico""" - if self._aggregation_enabled: - return self._get_aggregator().get_product(asset_id) + def get_product(self, asset_id: str) -> ProductInfo: return self.wrappers.try_call(lambda w: w.get_product(asset_id)) - - def get_products(self, asset_ids: List[str]) -> List[ProductInfo]: - """Ottieni informazioni su multiple prodotti""" - if self._aggregation_enabled: - return self._get_aggregator().get_products(asset_ids) + def get_products(self, asset_ids: list[str]) -> list[ProductInfo]: return self.wrappers.try_call(lambda w: w.get_products(asset_ids)) - - def get_all_products(self) -> List[ProductInfo]: - """Ottieni tutti i prodotti disponibili""" - if self._aggregation_enabled: - return self._get_aggregator().get_all_products() + def get_all_products(self) -> list[ProductInfo]: return self.wrappers.try_call(lambda w: w.get_all_products()) - - def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> List[Price]: - """Ottieni dati storici dei prezzi""" - if self._aggregation_enabled: - return self._get_aggregator().get_historical_prices(asset_id, limit) + def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: return self.wrappers.try_call(lambda w: w.get_historical_prices(asset_id, limit)) - # Metodi per controllare l'aggregazione - def enable_aggregation(self, enabled: bool = True): - """Abilita/disabilita la modalità aggregazione""" - self._aggregation_enabled = enabled - if self._aggregator: - self._aggregator.enable_aggregation(enabled) - def is_aggregation_enabled(self) -> bool: - """Verifica se l'aggregazione è abilitata""" - return self._aggregation_enabled + def get_products_aggregated(self, asset_ids: list[str]) -> list[ProductInfo]: + """ + Restituisce i dati aggregati per una lista di asset_id.\n + Attenzione che si usano tutte le fonti, quindi potrebbe usare molte chiamate API (che potrebbero essere a pagamento). + Args: + asset_ids (list[str]): Lista di asset_id da cercare. + Returns: + list[ProductInfo]: Lista di ProductInfo aggregati. + """ + all_products = self.wrappers.try_call_all(lambda w: w.get_products(asset_ids)) + return aggregate_product_info(all_products) - # Metodo speciale per debugging (opzionale) - def get_aggregated_product_with_debug(self, asset_id: str) -> dict: + def get_historical_prices_aggregated(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: """ - Metodo speciale per ottenere dati aggregati con informazioni di debug. - Disponibile solo quando l'aggregazione è abilitata. + Restituisce i dati storici aggregati per un asset_id. Usa i dati di tutte le fonti disponibili e li aggrega.\n + Attenzione che si usano tutte le fonti, quindi potrebbe usare molte chiamate API (che potrebbero essere a pagamento). + Args: + asset_id (str): Asset ID da cercare. + limit (int): Numero massimo di dati storici da restituire. + Returns: + list[Price]: Lista di Price aggregati. """ - if not self._aggregation_enabled: - raise RuntimeError("L'aggregazione deve essere abilitata per usare questo metodo") - return self._get_aggregator().get_aggregated_product_with_debug(asset_id) + all_prices = self.wrappers.try_call_all(lambda w: w.get_historical_prices(asset_id, limit)) + return aggregate_history_prices(all_prices) # TODO definire istruzioni per gli agenti di mercato MARKET_INSTRUCTIONS = """ diff --git a/src/app/markets/base.py b/src/app/markets/base.py index 117c174..5761675 100644 --- a/src/app/markets/base.py +++ b/src/app/markets/base.py @@ -1,4 +1,3 @@ - from pydantic import BaseModel class BaseWrapper: diff --git a/src/app/utils/aggregated_models.py b/src/app/utils/aggregated_models.py deleted file mode 100644 index e4a5ff4..0000000 --- a/src/app/utils/aggregated_models.py +++ /dev/null @@ -1,186 +0,0 @@ -import statistics -from typing import Dict, List, Optional, Set -from pydantic import BaseModel, Field, PrivateAttr -from app.markets.base import ProductInfo - -class AggregationMetadata(BaseModel): - """Metadati nascosti per debugging e audit trail""" - sources_used: Set[str] = Field(default_factory=set, description="Exchange usati nell'aggregazione") - sources_ignored: Set[str] = Field(default_factory=set, description="Exchange ignorati (errori)") - aggregation_timestamp: str = Field(default="", description="Timestamp dell'aggregazione") - confidence_score: float = Field(default=0.0, description="Score 0-1 sulla qualità dei dati") - - class Config: - # Nasconde questi campi dalla serializzazione di default - extra = "forbid" - -class AggregatedProductInfo(ProductInfo): - """ - Versione aggregata di ProductInfo che mantiene la trasparenza per l'utente finale - mentre fornisce metadati di debugging opzionali. - """ - - # Override dei campi con logica di aggregazione - id: str = Field(description="ID aggregato basato sul simbolo standardizzato") - status: str = Field(description="Status aggregato (majority vote o conservative)") - - # Campi privati per debugging (non visibili di default) - _metadata: Optional[AggregationMetadata] = PrivateAttr(default=None) - _source_data: Optional[Dict[str, ProductInfo]] = PrivateAttr(default=None) - - @classmethod - def from_multiple_sources(cls, products: List[ProductInfo]) -> 'AggregatedProductInfo': - """ - Crea un AggregatedProductInfo da una lista di ProductInfo. - Usa strategie intelligenti per gestire ID e status. - """ - if not products: - raise ValueError("Nessun prodotto da aggregare") - - # Raggruppa per symbol (la chiave vera per l'aggregazione) - symbol_groups = {} - for product in products: - if product.symbol not in symbol_groups: - symbol_groups[product.symbol] = [] - symbol_groups[product.symbol].append(product) - - # Per ora gestiamo un symbol alla volta - if len(symbol_groups) > 1: - raise ValueError(f"Simboli multipli non supportati: {list(symbol_groups.keys())}") - - symbol_products = list(symbol_groups.values())[0] - - # Estrai tutte le fonti - sources = [] - for product in symbol_products: - # Determina la fonte dall'ID o da altri metadati se disponibili - source = cls._detect_source(product) - sources.append(source) - - # Aggrega i dati - aggregated_data = cls._aggregate_products(symbol_products, sources) - - # Crea l'istanza e assegna gli attributi privati - instance = cls(**aggregated_data) - instance._metadata = aggregated_data.get("_metadata") - instance._source_data = aggregated_data.get("_source_data") - - return instance - - @staticmethod - def _detect_source(product: ProductInfo) -> str: - """Rileva la fonte da un ProductInfo""" - # Strategia semplice: usa pattern negli ID - if "coinbase" in product.id.lower() or "cb" in product.id.lower(): - return "coinbase" - elif "binance" in product.id.lower() or "bn" in product.id.lower(): - return "binance" - elif "crypto" in product.id.lower() or "cc" in product.id.lower(): - return "cryptocompare" - elif "yfinance" in product.id.lower() or "yf" in product.id.lower(): - return "yfinance" - else: - return "unknown" - - @classmethod - def _aggregate_products(cls, products: List[ProductInfo], sources: List[str]) -> dict: - """ - Logica di aggregazione principale. - Gestisce ID, status e altri campi numerici. - """ - import statistics - from datetime import datetime - - # ID: usa il symbol come chiave standardizzata - symbol = products[0].symbol - aggregated_id = f"{symbol}_AGG" - - # Status: strategia "conservativa" - il più restrittivo vince - # Ordine: trading_only < limit_only < auction < maintenance < offline - status_priority = { - "trading": 1, - "limit_only": 2, - "auction": 3, - "maintenance": 4, - "offline": 5, - "": 0 # Default se non specificato - } - - statuses = [p.status for p in products if p.status] - if statuses: - # Prendi lo status con priorità più alta (più restrittivo) - aggregated_status = max(statuses, key=lambda s: status_priority.get(s, 0)) - else: - aggregated_status = "trading" # Default ottimistico - - # Prezzo: media semplice (uso diretto del campo price come float) - prices = [p.price for p in products if p.price > 0] - aggregated_price = statistics.mean(prices) if prices else 0.0 - - # Volume: somma (assumendo che i volumi siano esclusivi per exchange) - volumes = [p.volume_24h for p in products if p.volume_24h > 0] - total_volume = sum(volumes) - aggregated_volume = sum(price_i * volume_i for price_i, volume_i in zip((p.price for p in products), (volume for volume in volumes))) / total_volume - aggregated_volume = round(aggregated_volume, 5) - # aggregated_volume = sum(volumes) if volumes else 0.0 # NOTE old implementation - - # Valuta: prendi la prima (dovrebbero essere tutte uguali) - quote_currency = next((p.quote_currency for p in products if p.quote_currency), "USD") - - # Calcola confidence score - confidence = cls._calculate_confidence(products, sources) - - # Crea metadati per debugging - metadata = AggregationMetadata( - sources_used=set(sources), - aggregation_timestamp=datetime.now().isoformat(), - confidence_score=confidence - ) - - # Salva dati sorgente per debugging - source_data = dict(zip(sources, products)) - - return { - "symbol": symbol, - "price": aggregated_price, - "volume_24h": aggregated_volume, - "quote_currency": quote_currency, - "id": aggregated_id, - "status": aggregated_status, - "_metadata": metadata, - "_source_data": source_data - } - - @staticmethod - def _calculate_confidence(products: List[ProductInfo], sources: List[str]) -> float: - """Calcola un punteggio di confidenza 0-1""" - if not products: - return 0.0 - - score = 1.0 - - # Riduci score se pochi dati - if len(products) < 2: - score *= 0.7 - - # Riduci score se prezzi troppo diversi - prices = [p.price for p in products if p.price > 0] - if len(prices) > 1: - price_std = (max(prices) - min(prices)) / statistics.mean(prices) - if price_std > 0.05: # >5% variazione - score *= 0.8 - - # Riduci score se fonti sconosciute - unknown_sources = sum(1 for s in sources if s == "unknown") - if unknown_sources > 0: - score *= (1 - unknown_sources / len(sources)) - - return max(0.0, min(1.0, score)) - - def get_debug_info(self) -> dict: - """Metodo opzionale per ottenere informazioni di debug""" - return { - "aggregated_product": self.dict(), - "metadata": self._metadata.dict() if self._metadata else None, - "sources": list(self._source_data.keys()) if self._source_data else [] - } \ No newline at end of file diff --git a/src/app/utils/market_aggregation.py b/src/app/utils/market_aggregation.py new file mode 100644 index 0000000..3d7b6b8 --- /dev/null +++ b/src/app/utils/market_aggregation.py @@ -0,0 +1,82 @@ +import statistics +from app.markets.base import ProductInfo, Price + + +def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[float]: + """Aggrega i prezzi storici per symbol calcolando la media""" + raise NotImplementedError("Funzione non ancora implementata per problemi di timestamp he deve essere uniformato prima di usare questa funzione.") + # TODO implementare l'aggregazione dopo aver modificato la classe Price in modo che abbia un timestamp integer + # aggregated_prices = [] + # for timestamp in range(len(next(iter(prices.values())))): + # timestamp_prices = [ + # price_list[timestamp].price + # for price_list in prices.values() + # if len(price_list) > timestamp and price_list[timestamp].price is not None + # ] + # if timestamp_prices: + # aggregated_prices.append(statistics.mean(timestamp_prices)) + # else: + # aggregated_prices.append(None) + # return aggregated_prices + +def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[ProductInfo]: + """ + Aggrega una lista di ProductInfo per symbol. + """ + + # Costruzione mappa symbol -> lista di ProductInfo + symbols_infos: dict[str, list[ProductInfo]] = {} + for _, product_list in products.items(): + for product in product_list: + symbols_infos.setdefault(product.symbol, []).append(product) + + # Aggregazione per ogni symbol + sources = list(products.keys()) + aggregated_products = [] + for symbol, product_list in symbols_infos.items(): + product = ProductInfo() + + product.id = f"{symbol}_AGG" + product.symbol = symbol + product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency) + + statuses = {} + for p in product_list: + statuses[p.status] = statuses.get(p.status, 0) + 1 + product.status = max(statuses, key=statuses.get) if statuses else "" + + prices = [p.price for p in product_list] + product.price = statistics.mean(prices) + + volumes = [p.volume_24h for p in product_list] + product.volume_24h = sum([p * v for p, v in zip(prices, volumes)]) / sum(volumes) + aggregated_products.append(product) + + confidence = _calculate_confidence(product_list, sources) # TODO necessary? + + return aggregated_products + +def _calculate_confidence(products: list[ProductInfo], sources: list[str]) -> float: + """Calcola un punteggio di confidenza 0-1""" + if not products: + return 0.0 + + score = 1.0 + + # Riduci score se pochi dati + if len(products) < 2: + score *= 0.7 + + # Riduci score se prezzi troppo diversi + prices = [p.price for p in products if p.price > 0] + if len(prices) > 1: + price_std = (max(prices) - min(prices)) / statistics.mean(prices) + if price_std > 0.05: # >5% variazione + score *= 0.8 + + # Riduci score se fonti sconosciute + unknown_sources = sum(1 for s in sources if s == "unknown") + if unknown_sources > 0: + score *= (1 - unknown_sources / len(sources)) + + return max(0.0, min(1.0, score)) diff --git a/src/app/utils/market_data_aggregator.py b/src/app/utils/market_data_aggregator.py deleted file mode 100644 index 9c6206d..0000000 --- a/src/app/utils/market_data_aggregator.py +++ /dev/null @@ -1,184 +0,0 @@ -from typing import List, Optional, Dict, Any -from app.markets.base import ProductInfo, Price -from app.utils.aggregated_models import AggregatedProductInfo - -class MarketDataAggregator: - """ - Aggregatore di dati di mercato che mantiene la trasparenza per l'utente. - - Compone MarketAPIs per fornire gli stessi metodi, ma restituisce dati aggregati - da tutte le fonti disponibili. L'utente finale non vede la complessità. - """ - - def __init__(self, currency: str = "USD"): - # Import lazy per evitare circular import - from app.markets import MarketAPIsTool - self._market_apis = MarketAPIsTool(currency) - self._aggregation_enabled = True - - def get_product(self, asset_id: str) -> ProductInfo: - """ - Override che aggrega dati da tutte le fonti disponibili. - Per l'utente sembra un normale ProductInfo. - """ - if not self._aggregation_enabled: - return self._market_apis.get_product(asset_id) - - # Raccogli dati da tutte le fonti - try: - raw_results = self.wrappers.try_call_all( - lambda wrapper: wrapper.get_product(asset_id) - ) - - # Converti in ProductInfo se necessario - products = [] - for wrapper_class, result in raw_results.items(): - if isinstance(result, ProductInfo): - products.append(result) - elif isinstance(result, dict): - # Converti dizionario in ProductInfo - products.append(ProductInfo(**result)) - - if not products: - raise Exception("Nessun dato disponibile") - - # Aggrega i risultati - aggregated = AggregatedProductInfo.from_multiple_sources(products) - - # Restituisci come ProductInfo normale (nascondi la complessità) - return ProductInfo(**aggregated.dict(exclude={"_metadata", "_source_data"})) - - except Exception as e: - # Fallback: usa il comportamento normale se l'aggregazione fallisce - return self._market_apis.get_product(asset_id) - - def get_products(self, asset_ids: List[str]) -> List[ProductInfo]: - """ - Aggrega dati per multiple asset. - """ - if not self._aggregation_enabled: - return self._market_apis.get_products(asset_ids) - - aggregated_products = [] - - for asset_id in asset_ids: - try: - product = self.get_product(asset_id) - aggregated_products.append(product) - except Exception as e: - # Salta asset che non riescono ad aggregare - continue - - return aggregated_products - - def get_all_products(self) -> List[ProductInfo]: - """ - Aggrega tutti i prodotti disponibili. - """ - if not self._aggregation_enabled: - return self._market_apis.get_all_products() - - # Raccogli tutti i prodotti da tutte le fonti - try: - all_products_by_source = self.wrappers.try_call_all( - lambda wrapper: wrapper.get_all_products() - ) - - # Raggruppa per symbol per aggregare - symbol_groups = {} - for wrapper_class, products in all_products_by_source.items(): - if not isinstance(products, list): - continue - - for product in products: - if isinstance(product, dict): - product = ProductInfo(**product) - - if product.symbol not in symbol_groups: - symbol_groups[product.symbol] = [] - symbol_groups[product.symbol].append(product) - - # Aggrega ogni gruppo - aggregated_products = [] - for symbol, products in symbol_groups.items(): - try: - aggregated = AggregatedProductInfo.from_multiple_sources(products) - # Restituisci come ProductInfo normale - aggregated_products.append( - ProductInfo(**aggregated.dict(exclude={"_metadata", "_source_data"})) - ) - except Exception: - # Se l'aggregazione fallisce, usa il primo disponibile - if products: - aggregated_products.append(products[0]) - - return aggregated_products - - except Exception as e: - # Fallback: usa il comportamento normale - return self._market_apis.get_all_products() - - def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> List[Price]: - """ - Per i dati storici, usa una strategia diversa: - prendi i dati dalla fonte più affidabile o aggrega se possibile. - """ - if not self._aggregation_enabled: - return self._market_apis.get_historical_prices(asset_id, limit) - - # Per dati storici, usa il primo wrapper che funziona - # (l'aggregazione di dati storici è più complessa) - try: - return self.wrappers.try_call( - lambda wrapper: wrapper.get_historical_prices(asset_id, limit) - ) - except Exception as e: - # Fallback: usa il comportamento normale - return self._market_apis.get_historical_prices(asset_id, limit) - - def enable_aggregation(self, enabled: bool = True): - """Abilita o disabilita l'aggregazione""" - self._aggregation_enabled = enabled - - def is_aggregation_enabled(self) -> bool: - """Controlla se l'aggregazione è abilitata""" - return self._aggregation_enabled - - # Metodi proxy per completare l'interfaccia BaseWrapper - @property - def wrappers(self): - """Accesso al wrapper handler per compatibilità""" - return self._market_apis.wrappers - - def get_aggregated_product_with_debug(self, asset_id: str) -> Dict[str, Any]: - """ - Metodo speciale per debugging: restituisce dati aggregati con metadati. - Usato solo per testing e monitoraggio. - """ - try: - raw_results = self.wrappers.try_call_all( - lambda wrapper: wrapper.get_product(asset_id) - ) - - products = [] - for wrapper_class, result in raw_results.items(): - if isinstance(result, ProductInfo): - products.append(result) - elif isinstance(result, dict): - products.append(ProductInfo(**result)) - - if not products: - raise Exception("Nessun dato disponibile") - - aggregated = AggregatedProductInfo.from_multiple_sources(products) - - return { - "product": aggregated.dict(exclude={"_metadata", "_source_data"}), - "debug": aggregated.get_debug_info() - } - - except Exception as e: - return { - "error": str(e), - "debug": {"error": str(e)} - } \ No newline at end of file diff --git a/tests/utils/test_market_data_aggregator.py b/tests/utils/test_market_data_aggregator.py index e8d1a6f..57ef4a1 100644 --- a/tests/utils/test_market_data_aggregator.py +++ b/tests/utils/test_market_data_aggregator.py @@ -1,88 +1,104 @@ import pytest -from app.utils.market_data_aggregator import MarketDataAggregator -from app.utils.aggregated_models import AggregatedProductInfo from app.markets.base import ProductInfo, Price +from app.utils.market_aggregation import aggregate_history_prices, aggregate_product_info + @pytest.mark.aggregator -@pytest.mark.limited @pytest.mark.market -@pytest.mark.api class TestMarketDataAggregator: - - def test_initialization(self): - """Test che il MarketDataAggregator si inizializzi correttamente""" - aggregator = MarketDataAggregator() - assert aggregator is not None - assert aggregator.is_aggregation_enabled() == True - - def test_aggregation_toggle(self): - """Test del toggle dell'aggregazione""" - aggregator = MarketDataAggregator() - - # Disabilita aggregazione - aggregator.enable_aggregation(False) - assert aggregator.is_aggregation_enabled() == False - - # Riabilita aggregazione - aggregator.enable_aggregation(True) - assert aggregator.is_aggregation_enabled() == True - - def test_aggregated_product_info_creation(self): - """Test creazione AggregatedProductInfo da fonti multiple""" - - # Crea dati di esempio - product1 = ProductInfo( - id="BTC-USD", - symbol="BTC-USD", + + def __product(self, symbol: str, price: float, volume: float, status: str, currency: str) -> ProductInfo: + prod = ProductInfo() + prod.id=f"{symbol}-{currency}" + prod.symbol=symbol + prod.price=price + prod.volume_24h=volume + prod.status=status + prod.quote_currency=currency + return prod + + def test_aggregate_product_info(self): + products: dict[str, list[ProductInfo]] = { + "Provider1": [self.__product("BTC", 50000.0, 1000.0, "active", "USD")], + "Provider2": [self.__product("BTC", 50100.0, 1100.0, "active", "USD")], + "Provider3": [self.__product("BTC", 49900.0, 900.0, "inactive", "USD")], + } + + aggregated = aggregate_product_info(products) + print(aggregated) + assert len(aggregated) == 1 + + info = aggregated[0] + assert info is not None + assert info.symbol == "BTC" + assert info.price == pytest.approx(50000.0, rel=1e-3) + + avg_weighted_volume = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0) + assert info.volume_24h == pytest.approx(avg_weighted_volume, rel=1e-3) + assert info.status == "active" + assert info.quote_currency == "USD" + + def test_aggregate_product_info_multiple_symbols(self): + products = { + "Provider1": [ + self.__product("BTC", 50000.0, 1000.0, "active", "USD"), + self.__product("ETH", 4000.0, 2000.0, "active", "USD"), + ], + "Provider2": [ + self.__product("BTC", 50100.0, 1100.0, "active", "USD"), + self.__product("ETH", 4050.0, 2100.0, "active", "USD"), + ], + } + + aggregated = aggregate_product_info(products) + assert len(aggregated) == 2 + + btc_info = next((p for p in aggregated if p.symbol == "BTC"), None) + eth_info = next((p for p in aggregated if p.symbol == "ETH"), None) + + assert btc_info is not None + assert btc_info.price == pytest.approx(50050.0, rel=1e-3) + avg_weighted_volume_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0) + assert btc_info.volume_24h == pytest.approx(avg_weighted_volume_btc, rel=1e-3) + assert btc_info.status == "active" + assert btc_info.quote_currency == "USD" + + assert eth_info is not None + assert eth_info.price == pytest.approx(4025.0, rel=1e-3) + avg_weighted_volume_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0) + assert eth_info.volume_24h == pytest.approx(avg_weighted_volume_eth, rel=1e-3) + assert eth_info.status == "active" + assert eth_info.quote_currency == "USD" + + def test_aggregate_history_prices(self): + """Test aggregazione di prezzi storici usando aggregate_history_prices""" + + price1 = Price( + timestamp="2024-06-01T00:00:00Z", price=50000.0, - volume_24h=1000.0, - status="active", - quote_currency="USD" + source="exchange1" ) - - product2 = ProductInfo( - id="BTC-USD", - symbol="BTC-USD", + price2 = Price( + timestamp="2024-06-01T00:00:00Z", price=50100.0, - volume_24h=1100.0, - status="active", - quote_currency="USD" + source="exchange2" ) - - # Aggrega i prodotti - aggregated = AggregatedProductInfo.from_multiple_sources([product1, product2]) - - assert aggregated.symbol == "BTC-USD" - assert aggregated.price == pytest.approx(50050.0, rel=1e-3) # media tra 50000 e 50100 - assert aggregated.volume_24h == 50052.38095 # somma dei volumi - assert aggregated.status == "active" # majority vote - assert aggregated.id == "BTC-USD_AGG" # mapping_id con suffisso aggregazione - - def test_confidence_calculation(self): - """Test del calcolo della confidence""" - - product1 = ProductInfo( - id="BTC-USD", - symbol="BTC-USD", - price=50000.0, - volume_24h=1000.0, - status="active", - quote_currency="USD" + price3 = Price( + timestamp="2024-06-01T01:00:00Z", + price=50200.0, + source="exchange1" ) - - product2 = ProductInfo( - id="BTC-USD", - symbol="BTC-USD", - price=50100.0, - volume_24h=1100.0, - status="active", - quote_currency="USD" + price4 = Price( + timestamp="2024-06-01T01:00:00Z", + price=50300.0, + source="exchange2" ) - - aggregated = AggregatedProductInfo.from_multiple_sources([product1, product2]) - - # Verifica che ci siano metadati - assert aggregated._metadata is not None - assert len(aggregated._metadata.sources_used) > 0 - assert aggregated._metadata.aggregation_timestamp != "" - # La confidence può essere 0.0 se ci sono fonti "unknown" + + prices = [price1, price2, price3, price4] + aggregated_prices = aggregate_history_prices(prices) + + assert len(aggregated_prices) == 2 + assert aggregated_prices[0].timestamp == "2024-06-01T00:00:00Z" + assert aggregated_prices[0].price == pytest.approx(50050.0, rel=1e-3) + assert aggregated_prices[1].timestamp == "2024-06-01T01:00:00Z" + assert aggregated_prices[1].price == pytest.approx(50250.0, rel=1e-3) -- 2.49.1 From 6cbef28d6be99c89aff1a8676b29468b6048f93f Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 19:39:12 +0200 Subject: [PATCH 08/18] refactor: update README and .env.example to reflect the latest changes to the project --- .env.example | 3 +- README.md | 120 +++++++++++++++++++++++---------------------------- 2 files changed, 55 insertions(+), 68 deletions(-) diff --git a/.env.example b/.env.example index 4cfc34a..fd9a427 100644 --- a/.env.example +++ b/.env.example @@ -2,8 +2,7 @@ # Configurazioni per i modelli di linguaggio ############################################################################### -# Alcune API sono a pagamento, altre hanno un piano gratuito con limiti di utilizzo -# Vedi https://docs.agno.com/examples/models per vedere tutti i modelli supportati +# https://makersuite.google.com/app/apikey GOOGLE_API_KEY= ############################################################################### diff --git a/README.md b/README.md index 4e02240..95981f3 100644 --- a/README.md +++ b/README.md @@ -9,53 +9,67 @@ L'obiettivo è quello di creare un sistema di consulenza finanziaria basato su L # **Indice** - [Installazione](#installazione) - - [Ollama (Modelli Locali)](#ollama-modelli-locali) - - [Variabili d'Ambiente](#variabili-dambiente) - - [Installazione in locale con UV](#installazione-in-locale-con-uv) - - [Installazione con Docker](#installazione-con-docker) + - [1. Variabili d'Ambiente](#1-variabili-dambiente) + - [2. Ollama](#2-ollama) + - [3. Docker](#3-docker) + - [4. UV (solo per sviluppo locale)](#4-uv-solo-per-sviluppo-locale) - [Applicazione](#applicazione) - [Ultimo Aggiornamento](#ultimo-aggiornamento) - [Tests](#tests) # **Installazione** -Per l'installazione di questo progetto si consiglia di utilizzare **Docker**. Con questo approccio si evita di dover installare manualmente tutte le dipendenze e si può eseguire il progetto in un ambiente isolato. -Per lo sviluppo locale si può utilizzare **uv** che si occupa di creare un ambiente virtuale e installare tutte le dipendenze. +L'installazione di questo progetto richiede 3 passaggi totali (+1 se si vuole sviluppare in locale) che devono essere eseguiti in sequenza. Se questi passaggi sono eseguiti correttamente, l'applicazione dovrebbe partire senza problemi. Altrimenti è molto probabile che si verifichino errori di vario tipo (moduli mancanti, chiavi API non trovate, ecc.). -In ogni caso, ***prima*** di avviare l'applicazione è però necessario configurare correttamente le **API keys** e installare Ollama per l'utilizzo dei modelli locali, altrimenti il progetto, anche se installato correttamente, non riuscirà a partire. +1. Configurare le variabili d'ambiente +2. Installare Ollama e i modelli locali +3. Far partire il progetto con Docker (consigliato) +4. (Solo per sviluppo locale) Installare uv e creare l'ambiente virtuale -### Ollama (Modelli Locali) -Per utilizzare modelli AI localmente, è necessario installare Ollama: +> [!IMPORTANT]\ +> Prima di iniziare, assicurarsi di avere clonato il repository e di essere nella cartella principale del progetto. -**1. Installazione Ollama**: -- **Linux**: `curl -fsSL https://ollama.com/install.sh | sh` -- **macOS/Windows**: Scarica l'installer da [https://ollama.com/download/windows](https://ollama.com/download/windows) +### **1. Variabili d'Ambiente** -**2. GPU Support (Raccomandato)**: -Per utilizzare la GPU con Ollama, assicurati di avere NVIDIA CUDA Toolkit installato: -- **Download**: [NVIDIA CUDA Downloads](https://developer.nvidia.com/cuda-downloads?target_os=Windows&target_arch=x86_64&target_version=11&target_type=exe_local) -- **Documentazione WSL**: [CUDA WSL User Guide](https://docs.nvidia.com/cuda/wsl-user-guide/index.html) - -**3. Installazione Modelli**: -Si possono avere più modelli installati contemporaneamente. Per questo progetto si consiglia di utilizzare il modello open source `gpt-oss` poiché prestante e compatibile con tante funzionalità. Per il download: `ollama pull gpt-oss:latest` - -### Variabili d'Ambiente - -**1. Copia il file di esempio**: +Copia il file `.env.example` in `.env` e modificalo con le tue API keys: ```sh cp .env.example .env ``` -**2. Modifica il file .env** creato con le tue API keys e il path dei modelli Ollama, inserendoli nelle variabili opportune dopo l'uguale e ***senza*** spazi. +Le API Keys devono essere inserite nelle variabili opportune dopo l'uguale e ***senza*** spazi. Esse si possono ottenere tramite i loro providers (alcune sono gratuite, altre a pagamento).\ +Nel file [.env.example](.env.example) sono presenti tutte le variabili da compilare con anche il link per recuperare le chiavi, quindi, dopo aver copiato il file, basta seguire le istruzioni al suo interno. -Le API Keys puoi ottenerle tramite i seguenti servizi (alcune sono gratuite, altre a pagamento): -- **Google AI**: [Google AI Studio](https://makersuite.google.com/app/apikey) (gratuito con limiti) -- **Anthropic**: [Anthropic Console](https://console.anthropic.com/) -- **DeepSeek**: [DeepSeek Platform](https://platform.deepseek.com/) -- **OpenAI**: [OpenAI Platform](https://platform.openai.com/api-keys) +### **2. Ollama** +Per utilizzare modelli AI localmente, è necessario installare Ollama, un gestore di modelli LLM che consente di eseguire modelli direttamente sul proprio hardware. Si consiglia di utilizzare Ollama con il supporto GPU per prestazioni ottimali, ma è possibile eseguirlo anche solo con la CPU. + +Per l'installazione scaricare Ollama dal loro [sito ufficiale](https://ollama.com/download/linux). + +Dopo l'installazione, si possono iniziare a scaricare i modelli desiderati tramite il comando `ollama pull :`. + +I modelli usati dall'applicazione sono visibili in [src/app/models.py](src/app/models.py). Di seguito metto lo stesso una lista di modelli, ma potrebbe non essere aggiornata: +- `gpt-oss:latest` +- `qwen3:latest` +- `qwen3:4b` +- `qwen3:1.7b` + +### **3. Docker** +Se si vuole solamente avviare il progetto, si consiglia di utilizzare [Docker](https://www.docker.com), dato che sono stati creati i files [Dockerfile](Dockerfile) e [docker-compose.yaml](docker-compose.yaml) per creare il container con tutti i file necessari e già in esecuzione. + +```sh +# Configura le variabili d'ambiente +cp .env.example .env +nano .env # Modifica il file + +# Avvia il container +docker compose up --build -d +``` + +Se si sono seguiti i passaggi precedenti per la configurazione delle variabili d'ambiente, l'applicazione dovrebbe partire correttamente, dato che il file `.env` verrà automaticamente caricato nel container grazie alla configurazione in `docker-compose.yaml`. + +### **4. UV (solo per sviluppo locale)** + +Per prima cosa installa uv se non è già presente sul sistema -## **Installazione in locale con UV** -**1. Installazione uv**: Per prima cosa installa uv se non è già presente sul sistema: ```sh # Windows (PowerShell) powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" @@ -64,54 +78,28 @@ powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | ie curl -LsSf https://astral.sh/uv/install.sh | sh ``` -**2. Ambiente e dipendenze**: uv installerà python e creerà automaticamente l'ambiente virtuale con le dipendenze corrette: +UV installerà python e creerà automaticamente l'ambiente virtuale con le dipendenze corrette (nota che questo passaggio è opzionale, dato che uv, ogni volta che si esegue un comando, controlla se l'ambiente è attivo e se le dipendenze sono installate): + ```sh uv sync --frozen --no-cache ``` -**3. Run**: Successivamente si può far partire il progetto tramite il comando: +A questo punto si può far partire il progetto tramite il comando: + ```sh uv run python src/app.py ``` -## **Installazione con Docker** -Alternativamente, se si ha installato [Docker](https://www.docker.com), si può utilizzare il [Dockerfile](Dockerfile) e il [docker-compose.yaml](docker-compose.yaml) per creare il container con tutti i file necessari e già in esecuzione: - -**IMPORTANTE**: Assicurati di aver configurato il file `.env` come descritto sopra prima di avviare Docker. - -```sh -docker compose up --build -d -``` - -Il file `.env` verrà automaticamente caricato nel container grazie alla configurazione in `docker-compose.yaml`. - # **Applicazione** ***L'applicazione è attualmente in fase di sviluppo.*** Usando la libreria ``gradio`` è stata creata un'interfaccia web semplice per interagire con l'agente principale. Gli agenti secondari si trovano nella cartella `src/app/agents` e sono: -- **Market Agent**: Agente unificato che supporta multiple fonti di dati (Coinbase + CryptoCompare) con auto-configurazione -- **News Agent**: Recupera le notizie finanziarie più recenti utilizzando. -- **Social Agent**: Analizza i sentimenti sui social media utilizzando. +- **Market Agent**: Agente unificato che supporta multiple fonti di dati con auto-retry e gestione degli errori. +- **News Agent**: Recupera le notizie finanziarie più recenti sul mercato delle criptovalute. +- **Social Agent**: Analizza i sentimenti sui social media riguardo alle criptovalute. - **Predictor Agent**: Utilizza i dati raccolti dagli altri agenti per fare previsioni. -## Ultimo Aggiornamento - -### Cose non funzionanti -- **Market Agent**: Non è un vero agente dato che non usa LLM per ragionare ma prende solo i dati -- **market_aggregator.py**: Non è usato per ora -- **News Agent**: Non funziona lo scraping online, per ora usa dati mock -- **Social Agent**: Non funziona lo scraping online, per ora usa dati mock -- **Demos**: Le demos nella cartella [demos](demos) non sono aggiornate e non funzionano per ora - -### ToDo -- [X] Per lo scraping online bisogna iscriversi e recuperare le chiavi API -- [X] **Market Agent**: [CryptoCompare](https://www.cryptocompare.com/cryptopian/api-keys) -- [X] **Market Agent**: [Coinbase](https://www.coinbase.com/cloud/discover/api-keys) -- [ ] **News Agent**: [CryptoPanic](https://cryptopanic.com/) -- [ ] **Social Agent**: [post più hot da r/CryptoCurrency (Reddit)](https://www.reddit.com/) -- [ ] Capire come `gpt-oss` parsifica la risposta e per questioni "estetiche" si può pensare di visualizzare lo stream dei token. Vedere il sorgente `src/ollama_demo.py` per risolvere il problema. - ## Tests Per eseguire i test, assicurati di aver configurato correttamente le variabili d'ambiente nel file `.env` come descritto sopra. Poi esegui il comando: @@ -119,8 +107,8 @@ Per eseguire i test, assicurati di aver configurato correttamente le variabili d uv run pytest -v # Oppure per test specifici -uv run pytest -v tests/agents/test_market.py -uv run pytest -v tests/agents/test_predictor.py +uv run pytest -v tests/api/test_binance.py +uv run pytest -v -k "test_news" # Oppure usando i markers uv run pytest -v -m api -- 2.49.1 From 9c471948ffce62786914425e4f9886dce4cae69e Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 21:01:10 +0200 Subject: [PATCH 09/18] refactor: simplify product info and price creation in YFinanceWrapper --- src/app/markets/yfinance.py | 196 ++++++------------------------------ tests/api/test_yfinance.py | 54 ++-------- 2 files changed, 41 insertions(+), 209 deletions(-) diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py index 82d88fe..9e7fcd2 100644 --- a/src/app/markets/yfinance.py +++ b/src/app/markets/yfinance.py @@ -3,63 +3,30 @@ from agno.tools.yfinance import YFinanceTools from .base import BaseWrapper, ProductInfo, Price -def create_product_info(symbol: str, stock_data: dict) -> ProductInfo: +def create_product_info(stock_data: dict[str, str]) -> ProductInfo: """ Converte i dati di YFinanceTools in ProductInfo. """ product = ProductInfo() - - # ID univoco per yfinance - product.id = f"yfinance_{symbol}" - product.symbol = symbol - - # Estrai il prezzo corrente - gestisci diversi formati - if 'currentPrice' in stock_data: - product.price = float(stock_data['currentPrice']) - elif 'regularMarketPrice' in stock_data: - product.price = float(stock_data['regularMarketPrice']) - elif 'Current Stock Price' in stock_data: - # Formato: "254.63 USD" - estrai solo il numero - price_str = stock_data['Current Stock Price'].split()[0] - try: - product.price = float(price_str) - except ValueError: - product.price = 0.0 - else: - product.price = 0.0 - - # Volume 24h - if 'volume' in stock_data: - product.volume_24h = float(stock_data['volume']) - elif 'regularMarketVolume' in stock_data: - product.volume_24h = float(stock_data['regularMarketVolume']) - else: - product.volume_24h = 0.0 - - # Status basato sulla disponibilità dei dati + product.id = stock_data.get('Symbol', '') + product.symbol = product.id.split('-')[0] # Rimuovi il suffisso della valuta per le crypto + product.price = float(stock_data.get('Current Stock Price', f"0.0 USD").split(" ")[0]) # prende solo il numero + product.volume_24h = 0.0 # YFinance non fornisce il volume 24h direttamente product.status = "trading" if product.price > 0 else "offline" - - # Valuta (default USD) - product.quote_currency = stock_data.get('currency', 'USD') or 'USD' - + product.quote_currency = product.id.split('-')[0] # La valuta è la parte dopo il '-' return product - -def create_price_from_history(hist_data: dict, timestamp: str) -> Price: +def create_price_from_history(hist_data: dict[str, str]) -> Price: """ Converte i dati storici di YFinanceTools in Price. """ price = Price() - - if timestamp in hist_data: - day_data = hist_data[timestamp] - price.high = float(day_data.get('High', 0.0)) - price.low = float(day_data.get('Low', 0.0)) - price.open = float(day_data.get('Open', 0.0)) - price.close = float(day_data.get('Close', 0.0)) - price.volume = float(day_data.get('Volume', 0.0)) - price.time = timestamp - + price.high = float(hist_data.get('High', 0.0)) + price.low = float(hist_data.get('Low', 0.0)) + price.open = float(hist_data.get('Open', 0.0)) + price.close = float(hist_data.get('Close', 0.0)) + price.volume = float(hist_data.get('Volume', 0.0)) + price.time = hist_data.get('Timestamp', '') return price @@ -72,143 +39,46 @@ class YFinanceWrapper(BaseWrapper): def __init__(self, currency: str = "USD"): self.currency = currency - # Inizializza YFinanceTools - non richiede parametri specifici self.tool = YFinanceTools() def _format_symbol(self, asset_id: str) -> str: """ Formatta il simbolo per yfinance. - Per crypto, aggiunge '-USD' se non presente. + Per crypto, aggiunge '-' e la valuta (es. BTC -> BTC-USD). """ asset_id = asset_id.upper() - - # Se è già nel formato corretto (es: BTC-USD), usa così - if '-' in asset_id: - return asset_id - - # Per crypto singole (BTC, ETH), aggiungi -USD - if asset_id in ['BTC', 'ETH', 'ADA', 'SOL', 'DOT', 'LINK', 'UNI', 'AAVE']: - return f"{asset_id}-USD" - - # Per azioni, usa il simbolo così com'è - return asset_id + return f"{asset_id}-{self.currency}" if '-' not in asset_id else asset_id def get_product(self, asset_id: str) -> ProductInfo: - """ - Recupera le informazioni di un singolo prodotto. - """ symbol = self._format_symbol(asset_id) - - # Usa YFinanceTools per ottenere i dati - try: - # Ottieni le informazioni base dello stock - stock_info = self.tool.get_company_info(symbol) - - # Se il risultato è una stringa JSON, parsala - if isinstance(stock_info, str): - try: - stock_data = json.loads(stock_info) - except json.JSONDecodeError: - # Se non è JSON valido, prova a ottenere solo il prezzo - price_data_str = self.tool.get_current_stock_price(symbol) - if price_data_str and price_data_str.replace('.', '').replace('-', '').isdigit(): - price = float(price_data_str) - stock_data = {'currentPrice': price, 'currency': 'USD'} - else: - raise Exception("Dati non validi") - else: - stock_data = stock_info - - return create_product_info(symbol, stock_data) - - except Exception as e: - # Fallback: prova a ottenere solo il prezzo - try: - price_data_str = self.tool.get_current_stock_price(symbol) - if price_data_str and price_data_str.replace('.', '').replace('-', '').isdigit(): - price = float(price_data_str) - minimal_data = { - 'currentPrice': price, - 'currency': 'USD' - } - return create_product_info(symbol, minimal_data) - else: - raise Exception("Prezzo non disponibile") - except Exception: - # Se tutto fallisce, restituisci un prodotto vuoto - product = ProductInfo() - product.symbol = symbol - product.status = "offline" - return product + stock_info = self.tool.get_company_info(symbol) + stock_info = json.loads(stock_info) + return create_product_info(stock_info) def get_products(self, asset_ids: list[str]) -> list[ProductInfo]: - """ - Recupera le informazioni di multiple assets. - """ products = [] - for asset_id in asset_ids: - try: - product = self.get_product(asset_id) - products.append(product) - except Exception as e: - # Se un asset non è disponibile, continua con gli altri - continue - + product = self.get_product(asset_id) + products.append(product) return products def get_all_products(self) -> list[ProductInfo]: - """ - Recupera tutti i prodotti disponibili. - Restituisce una lista predefinita di asset popolari. - """ - # Lista di asset popolari (azioni, ETF, crypto) - popular_assets = [ - 'BTC', 'ETH', 'ADA', 'SOL', 'DOT', - 'AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN', - 'SPY', 'QQQ', 'VTI', 'GLD', 'VIX' - ] - - return self.get_products(popular_assets) + raise NotImplementedError("YFinanceWrapper does not support get_all_products due to API limitations.") def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: - """ - Recupera i dati storici di prezzo per un asset. - """ symbol = self._format_symbol(asset_id) - try: - # Determina il periodo appropriato in base al limite - if limit <= 7: - period = "1d" - interval = "15m" - elif limit <= 30: - period = "5d" - interval = "1h" - elif limit <= 90: - period = "1mo" - interval = "1d" - else: - period = "3mo" - interval = "1d" + days = limit // 24 + 1 # Arrotonda per eccesso + hist_data = self.tool.get_historical_stock_prices(symbol, period=f"{days}d", interval="1h") + hist_data = json.loads(hist_data) - # Ottieni i dati storici - hist_data = self.tool.get_historical_stock_prices(symbol, period=period, interval=interval) + # Il formato dei dati è {timestamp: {Open: x, High: y, Low: z, Close: w, Volume: v}} + timestamps = sorted(hist_data.keys())[-limit:] - if isinstance(hist_data, str): - hist_data = json.loads(hist_data) - - # Il formato dei dati è {timestamp: {Open: x, High: y, Low: z, Close: w, Volume: v}} - prices = [] - timestamps = sorted(hist_data.keys())[-limit:] # Prendi gli ultimi 'limit' timestamp - - for timestamp in timestamps: - price = create_price_from_history(hist_data, timestamp) - if price.close > 0: # Solo se ci sono dati validi - prices.append(price) - - return prices - - except Exception as e: - # Se fallisce, restituisci lista vuota - return [] \ No newline at end of file + prices = [] + for timestamp in timestamps: + temp = hist_data[timestamp] + temp['Timestamp'] = timestamp + price = create_price_from_history(temp) + prices.append(price) + return prices diff --git a/tests/api/test_yfinance.py b/tests/api/test_yfinance.py index c0e9ba2..8c70dfd 100644 --- a/tests/api/test_yfinance.py +++ b/tests/api/test_yfinance.py @@ -1,4 +1,3 @@ -import os import pytest from app.markets import YFinanceWrapper @@ -14,17 +13,6 @@ class TestYFinance: assert hasattr(market, 'tool') assert market.tool is not None - def test_yfinance_get_product(self): - market = YFinanceWrapper() - product = market.get_product("AAPL") - assert product is not None - assert hasattr(product, 'symbol') - assert product.symbol == "AAPL" - assert hasattr(product, 'price') - assert product.price > 0 - assert hasattr(product, 'status') - assert product.status == "trading" - def test_yfinance_get_crypto_product(self): market = YFinanceWrapper() product = market.get_product("BTC") @@ -37,49 +25,21 @@ class TestYFinance: def test_yfinance_get_products(self): market = YFinanceWrapper() - products = market.get_products(["AAPL", "GOOGL"]) + products = market.get_products(["BTC", "ETH"]) assert products is not None assert isinstance(products, list) assert len(products) == 2 symbols = [p.symbol for p in products] - assert "AAPL" in symbols - assert "GOOGL" in symbols + assert "BTC" in symbols + assert "ETH" in symbols for product in products: assert hasattr(product, 'price') assert product.price > 0 - def test_yfinance_get_all_products(self): - market = YFinanceWrapper() - products = market.get_all_products() - assert products is not None - assert isinstance(products, list) - assert len(products) > 0 - # Dovrebbe contenere asset popolari - symbols = [p.symbol for p in products] - assert "AAPL" in symbols # Apple dovrebbe essere nella lista - for product in products: - assert hasattr(product, 'symbol') - assert hasattr(product, 'price') - def test_yfinance_invalid_product(self): market = YFinanceWrapper() - # Per YFinance, un prodotto invalido dovrebbe restituire un prodotto offline - product = market.get_product("INVALIDSYMBOL123") - assert product is not None - assert product.status == "offline" - - def test_yfinance_history(self): - market = YFinanceWrapper() - history = market.get_historical_prices("AAPL", limit=5) - assert history is not None - assert isinstance(history, list) - assert len(history) == 5 - for entry in history: - assert hasattr(entry, 'time') - assert hasattr(entry, 'close') - assert hasattr(entry, 'high') - assert entry.close > 0 - assert entry.high > 0 + with pytest.raises(Exception): + _ = market.get_product("INVALIDSYMBOL123") def test_yfinance_crypto_history(self): market = YFinanceWrapper() @@ -90,4 +50,6 @@ class TestYFinance: for entry in history: assert hasattr(entry, 'time') assert hasattr(entry, 'close') - assert entry.close > 0 \ No newline at end of file + assert entry.close > 0 + assert hasattr(entry, 'open') + assert entry.open > 0 \ No newline at end of file -- 2.49.1 From ebd427501797bb30b0dadcf69c396f2dd267dfd6 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 21:14:09 +0200 Subject: [PATCH 10/18] refactor: remove get_all_products method from market API wrappers and update documentation --- README.md | 2 ++ demos/market_providers_api_demo.py | 18 ------------- src/app/markets/__init__.py | 26 ++++++++++++++----- src/app/markets/base.py | 8 ------ src/app/markets/binance.py | 11 -------- src/app/markets/coinbase.py | 4 --- src/app/markets/cryptocompare.py | 4 --- src/app/markets/yfinance.py | 3 --- tests/tools/test_market_tool.py | 1 - ...ggregator.py => test_market_aggregator.py} | 0 10 files changed, 21 insertions(+), 56 deletions(-) rename tests/utils/{test_market_data_aggregator.py => test_market_aggregator.py} (100%) diff --git a/README.md b/README.md index 95981f3..a545c92 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ cp .env.example .env Le API Keys devono essere inserite nelle variabili opportune dopo l'uguale e ***senza*** spazi. Esse si possono ottenere tramite i loro providers (alcune sono gratuite, altre a pagamento).\ Nel file [.env.example](.env.example) sono presenti tutte le variabili da compilare con anche il link per recuperare le chiavi, quindi, dopo aver copiato il file, basta seguire le istruzioni al suo interno. +Le chiavi non sono necessarie per far partire l'applicazione, ma senza di esse alcune funzionalità non saranno disponibili o saranno limitate. Per esempio senza la chiave di NewsAPI non si potranno recuperare le ultime notizie sul mercato delle criptovalute. Ciononostante, l'applicazione usa anche degli strumenti che non richiedono chiavi API, come Yahoo Finance e GNews, che permettono di avere comunque un'analisi di base del mercato. + ### **2. Ollama** Per utilizzare modelli AI localmente, è necessario installare Ollama, un gestore di modelli LLM che consente di eseguire modelli direttamente sul proprio hardware. Si consiglia di utilizzare Ollama con il supporto GPU per prestazioni ottimali, ma è possibile eseguirlo anche solo con la CPU. diff --git a/demos/market_providers_api_demo.py b/demos/market_providers_api_demo.py index 8c368e8..393a478 100644 --- a/demos/market_providers_api_demo.py +++ b/demos/market_providers_api_demo.py @@ -186,24 +186,6 @@ class ProviderTester: results["tests"]["get_products"] = f"ERROR: {error_msg}" results["overall_status"] = "PARTIAL" - # Test get_all_products - timestamp = datetime.now() - try: - all_products = wrapper.get_all_products() - self.formatter.print_request_info( - provider_name, "get_all_products()", timestamp, "✅ SUCCESS" - ) - self.formatter.print_product_table(all_products, f"{provider_name} All Products") - results["tests"]["get_all_products"] = "SUCCESS" - - except Exception as e: - error_msg = str(e) - self.formatter.print_request_info( - provider_name, "get_all_products()", timestamp, "❌ ERROR", error_msg - ) - results["tests"]["get_all_products"] = f"ERROR: {error_msg}" - results["overall_status"] = "PARTIAL" - # Test get_historical_prices timestamp = datetime.now() try: diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index d0eb6ad..6f09b2f 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -12,14 +12,29 @@ __all__ = [ "MarketAPIs", "BinanceWrapper", "CoinBaseWrapper", "CryptoCompareWra class MarketAPIsTool(BaseWrapper, Toolkit): """ - Classe per comporre più MarketAPI con gestione degli errori e aggregazione dei dati. - Usa WrapperHandler per gestire più API con logica di retry e failover. - Si può scegliere se aggregare i dati da tutte le fonti o usare una singola fonte tramite delle chiamate apposta. + Class that aggregates multiple market API wrappers and manages them using WrapperHandler. + This class supports retrieving product information and historical prices. + This class can also aggregate data from multiple sources to provide a more comprehensive view of the market. + The following wrappers are included in this order: + - BinanceWrapper + - YFinanceWrapper + - CoinBaseWrapper + - CryptoCompareWrapper """ def __init__(self, currency: str = "USD"): + """ + Initialize the MarketAPIsTool with multiple market API wrappers. + The following wrappers are included in this order: + - BinanceWrapper + - YFinanceWrapper + - CoinBaseWrapper + - CryptoCompareWrapper + Args: + currency (str): Valuta in cui restituire i prezzi. Default è "USD". + """ kwargs = {"currency": currency or "USD"} - wrappers = [ BinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper, YFinanceWrapper ] + wrappers = [ BinanceWrapper, YFinanceWrapper, CoinBaseWrapper, CryptoCompareWrapper ] self.wrappers: WrapperHandler[BaseWrapper] = WrapperHandler.build_wrappers(wrappers, kwargs=kwargs) Toolkit.__init__( @@ -28,7 +43,6 @@ class MarketAPIsTool(BaseWrapper, Toolkit): tools=[ self.get_product, self.get_products, - self.get_all_products, self.get_historical_prices, self.get_products_aggregated, self.get_historical_prices_aggregated, @@ -39,8 +53,6 @@ class MarketAPIsTool(BaseWrapper, Toolkit): return self.wrappers.try_call(lambda w: w.get_product(asset_id)) def get_products(self, asset_ids: list[str]) -> list[ProductInfo]: return self.wrappers.try_call(lambda w: w.get_products(asset_ids)) - def get_all_products(self) -> list[ProductInfo]: - return self.wrappers.try_call(lambda w: w.get_all_products()) def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: return self.wrappers.try_call(lambda w: w.get_historical_prices(asset_id, limit)) diff --git a/src/app/markets/base.py b/src/app/markets/base.py index 5761675..12892bb 100644 --- a/src/app/markets/base.py +++ b/src/app/markets/base.py @@ -26,14 +26,6 @@ class BaseWrapper: """ raise NotImplementedError - def get_all_products(self) -> list['ProductInfo']: - """ - Get product information for all available assets. - Returns: - list[ProductInfo]: A list of objects containing product information. - """ - raise NotImplementedError - def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list['Price']: """ Get historical price data for a specific asset ID. diff --git a/src/app/markets/binance.py b/src/app/markets/binance.py index d5dfe10..2eb85f0 100644 --- a/src/app/markets/binance.py +++ b/src/app/markets/binance.py @@ -54,17 +54,6 @@ class BinanceWrapper(BaseWrapper): return [get_product(self.currency, ticker) for ticker in tickers] - def get_all_products(self) -> list[ProductInfo]: - all_tickers = self.client.get_ticker() - products = [] - - for ticker in all_tickers: - # Filtra solo i simboli che terminano con la valuta di default - if ticker['symbol'].endswith(self.currency): - product = get_product(self.currency, ticker) - products.append(product) - return products - def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: symbol = self.__format_symbol(asset_id) diff --git a/src/app/markets/coinbase.py b/src/app/markets/coinbase.py index 286ec6f..9e4c64d 100644 --- a/src/app/markets/coinbase.py +++ b/src/app/markets/coinbase.py @@ -73,10 +73,6 @@ class CoinBaseWrapper(BaseWrapper): assets = self.client.get_products(product_ids=all_asset_ids) return [get_product(asset) for asset in assets.products] - def get_all_products(self) -> list[ProductInfo]: - assets = self.client.get_products() - return [get_product(asset) for asset in assets.products] - def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: asset_id = self.__format(asset_id) end_time = datetime.now() diff --git a/src/app/markets/cryptocompare.py b/src/app/markets/cryptocompare.py index c81a3bb..98e5284 100644 --- a/src/app/markets/cryptocompare.py +++ b/src/app/markets/cryptocompare.py @@ -67,10 +67,6 @@ class CryptoCompareWrapper(BaseWrapper): assets.append(get_product(asset_data)) return assets - def get_all_products(self) -> list[ProductInfo]: - # TODO serve davvero il workaroud qui? Possiamo prendere i dati da un altro endpoint intanto - raise NotImplementedError("get_all_products is not supported by CryptoCompare API") - def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[dict]: response = self.__request("/data/v2/histohour", params = { "fsym": asset_id, diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py index 9e7fcd2..02af2f6 100644 --- a/src/app/markets/yfinance.py +++ b/src/app/markets/yfinance.py @@ -62,9 +62,6 @@ class YFinanceWrapper(BaseWrapper): products.append(product) return products - def get_all_products(self) -> list[ProductInfo]: - raise NotImplementedError("YFinanceWrapper does not support get_all_products due to API limitations.") - def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list[Price]: symbol = self._format_symbol(asset_id) diff --git a/tests/tools/test_market_tool.py b/tests/tools/test_market_tool.py index 7513585..c6da5a8 100644 --- a/tests/tools/test_market_tool.py +++ b/tests/tools/test_market_tool.py @@ -11,7 +11,6 @@ class TestMarketAPIsTool: assert market_wrapper is not None assert hasattr(market_wrapper, 'get_product') assert hasattr(market_wrapper, 'get_products') - assert hasattr(market_wrapper, 'get_all_products') assert hasattr(market_wrapper, 'get_historical_prices') def test_wrapper_capabilities(self): diff --git a/tests/utils/test_market_data_aggregator.py b/tests/utils/test_market_aggregator.py similarity index 100% rename from tests/utils/test_market_data_aggregator.py rename to tests/utils/test_market_aggregator.py -- 2.49.1 From 1c884b67ddd38479823cf183cd97e8f923341313 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 21:31:43 +0200 Subject: [PATCH 11/18] fix: environment variable assertions --- src/app.py | 2 +- src/app/markets/coinbase.py | 4 ++-- src/app/markets/cryptocompare.py | 2 +- src/app/news/news_api.py | 2 +- src/app/social/reddit.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app.py b/src/app.py index 8477149..ec6d712 100644 --- a/src/app.py +++ b/src/app.py @@ -42,6 +42,6 @@ if __name__ == "__main__": analyze_btn = gr.Button("🔎 Analizza") analyze_btn.click(fn=pipeline.interact, inputs=[user_input], outputs=output) - server, port = ("0.0.0.0", 8000) + server, port = ("127.0.0.1", 8000) log_info(f"Starting UPO AppAI on http://{server}:{port}") demo.launch(server_name=server, server_port=port, quiet=True) diff --git a/src/app/markets/coinbase.py b/src/app/markets/coinbase.py index 9e4c64d..ea944e4 100644 --- a/src/app/markets/coinbase.py +++ b/src/app/markets/coinbase.py @@ -49,10 +49,10 @@ class CoinBaseWrapper(BaseWrapper): def __init__(self, currency: str = "USD"): api_key = os.getenv("COINBASE_API_KEY") - assert api_key is not None, "API key is required" + assert api_key, "COINBASE_API_KEY environment variable not set" api_private_key = os.getenv("COINBASE_API_SECRET") - assert api_private_key is not None, "API private key is required" + assert api_private_key, "COINBASE_API_SECRET environment variable not set" self.currency = currency self.client: RESTClient = RESTClient( diff --git a/src/app/markets/cryptocompare.py b/src/app/markets/cryptocompare.py index 98e5284..2986b33 100644 --- a/src/app/markets/cryptocompare.py +++ b/src/app/markets/cryptocompare.py @@ -34,7 +34,7 @@ class CryptoCompareWrapper(BaseWrapper): """ def __init__(self, currency:str='USD'): api_key = os.getenv("CRYPTOCOMPARE_API_KEY") - assert api_key is not None, "API key is required" + assert api_key, "CRYPTOCOMPARE_API_KEY environment variable not set" self.api_key = api_key self.currency = currency diff --git a/src/app/news/news_api.py b/src/app/news/news_api.py index 415fdac..6f62ef6 100644 --- a/src/app/news/news_api.py +++ b/src/app/news/news_api.py @@ -19,7 +19,7 @@ class NewsApiWrapper(NewsWrapper): def __init__(self): api_key = os.getenv("NEWS_API_KEY") - assert api_key is not None, "NEWS_API_KEY environment variable not set" + assert api_key, "NEWS_API_KEY environment variable not set" self.client = newsapi.NewsApiClient(api_key=api_key) self.category = "business" # Cryptocurrency is under business diff --git a/src/app/social/reddit.py b/src/app/social/reddit.py index 6028010..e0b4928 100644 --- a/src/app/social/reddit.py +++ b/src/app/social/reddit.py @@ -51,10 +51,10 @@ class RedditWrapper(SocialWrapper): def __init__(self): client_id = os.getenv("REDDIT_API_CLIENT_ID") - assert client_id is not None, "REDDIT_API_CLIENT_ID environment variable is not set" + assert client_id, "REDDIT_API_CLIENT_ID environment variable is not set" client_secret = os.getenv("REDDIT_API_CLIENT_SECRET") - assert client_secret is not None, "REDDIT_API_CLIENT_SECRET environment variable is not set" + assert client_secret, "REDDIT_API_CLIENT_SECRET environment variable is not set" self.tool = Reddit( client_id=client_id, -- 2.49.1 From 59a38c6e329419bae5064338918994cd5adf854f Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 23:38:50 +0200 Subject: [PATCH 12/18] refactor: remove status attribute from ProductInfo and update related methods to use timestamp_ms --- demos/market_providers_api_demo.py | 2 +- src/app/markets/base.py | 3 +- src/app/markets/binance.py | 25 +++++---- src/app/markets/coinbase.py | 8 ++- src/app/markets/cryptocompare.py | 32 +++++------ src/app/markets/yfinance.py | 3 +- src/app/utils/market_aggregation.py | 7 +-- tests/api/test_binance.py | 3 +- tests/api/test_coinbase.py | 3 +- tests/api/test_cryptocompare.py | 3 +- tests/api/test_reddit.py | 2 +- tests/api/test_yfinance.py | 11 ++-- tests/utils/test_market_aggregator.py | 76 +++++++++++++-------------- 13 files changed, 84 insertions(+), 94 deletions(-) diff --git a/demos/market_providers_api_demo.py b/demos/market_providers_api_demo.py index 393a478..fc05c26 100644 --- a/demos/market_providers_api_demo.py +++ b/demos/market_providers_api_demo.py @@ -154,7 +154,7 @@ class ProviderTester: if product: print(f"📦 Product: {product.symbol} (ID: {product.id})") print(f" Price: ${product.price:.2f}, Quote: {product.quote_currency}") - print(f" Status: {product.status}, Volume 24h: {product.volume_24h:,.2f}") + print(f" Volume 24h: {product.volume_24h:,.2f}") else: print(f"📦 Product: Nessun prodotto trovato per {symbol}") diff --git a/src/app/markets/base.py b/src/app/markets/base.py index 12892bb..4224014 100644 --- a/src/app/markets/base.py +++ b/src/app/markets/base.py @@ -46,7 +46,6 @@ class ProductInfo(BaseModel): symbol: str = "" price: float = 0.0 volume_24h: float = 0.0 - status: str = "" quote_currency: str = "" class Price(BaseModel): @@ -59,4 +58,4 @@ class Price(BaseModel): open: float = 0.0 close: float = 0.0 volume: float = 0.0 - time: str = "" + timestamp_ms: int = 0 # Timestamp in milliseconds diff --git a/src/app/markets/binance.py b/src/app/markets/binance.py index 2eb85f0..8e941c8 100644 --- a/src/app/markets/binance.py +++ b/src/app/markets/binance.py @@ -3,16 +3,25 @@ from datetime import datetime from binance.client import Client from .base import ProductInfo, BaseWrapper, Price -def get_product(currency: str, ticker_data: dict[str, str]) -> 'ProductInfo': +def get_product(currency: str, ticker_data: dict[str, str]) -> ProductInfo: product = ProductInfo() product.id = ticker_data.get('symbol') product.symbol = ticker_data.get('symbol', '').replace(currency, '') product.price = float(ticker_data.get('price', 0)) product.volume_24h = float(ticker_data.get('volume', 0)) - product.status = "TRADING" # Binance non fornisce status esplicito product.quote_currency = currency return product +def get_price(kline_data: list) -> Price: + price = Price() + price.open = float(kline_data[1]) + price.high = float(kline_data[2]) + price.low = float(kline_data[3]) + price.close = float(kline_data[4]) + price.volume = float(kline_data[5]) + price.timestamp_ms = kline_data[0] + return price + class BinanceWrapper(BaseWrapper): """ Wrapper per le API autenticate di Binance.\n @@ -63,15 +72,5 @@ class BinanceWrapper(BaseWrapper): interval=Client.KLINE_INTERVAL_1HOUR, limit=limit, ) + return [get_price(kline) for kline in klines] - prices = [] - for kline in klines: - price = Price() - price.open = float(kline[1]) - price.high = float(kline[2]) - price.low = float(kline[3]) - price.close = float(kline[4]) - price.volume = float(kline[5]) - price.time = str(datetime.fromtimestamp(kline[0] / 1000)) - prices.append(price) - return prices diff --git a/src/app/markets/coinbase.py b/src/app/markets/coinbase.py index ea944e4..54409c1 100644 --- a/src/app/markets/coinbase.py +++ b/src/app/markets/coinbase.py @@ -6,24 +6,22 @@ from coinbase.rest.types.product_types import Candle, GetProductResponse, Produc from .base import ProductInfo, BaseWrapper, Price -def get_product(product_data: GetProductResponse | Product) -> 'ProductInfo': +def get_product(product_data: GetProductResponse | Product) -> ProductInfo: product = ProductInfo() product.id = product_data.product_id or "" product.symbol = product_data.base_currency_id or "" product.price = float(product_data.price) if product_data.price else 0.0 product.volume_24h = float(product_data.volume_24h) if product_data.volume_24h else 0.0 - # TODO Check what status means in Coinbase - product.status = product_data.status or "" return product -def get_price(candle_data: Candle) -> 'Price': +def get_price(candle_data: Candle) -> Price: price = Price() price.high = float(candle_data.high) if candle_data.high else 0.0 price.low = float(candle_data.low) if candle_data.low else 0.0 price.open = float(candle_data.open) if candle_data.open else 0.0 price.close = float(candle_data.close) if candle_data.close else 0.0 price.volume = float(candle_data.volume) if candle_data.volume else 0.0 - price.time = str(candle_data.start) if candle_data.start else "" + price.timestamp_ms = int(candle_data.start) * 1000 if candle_data.start else 0 return price diff --git a/src/app/markets/cryptocompare.py b/src/app/markets/cryptocompare.py index 2986b33..f4b96e9 100644 --- a/src/app/markets/cryptocompare.py +++ b/src/app/markets/cryptocompare.py @@ -1,26 +1,26 @@ import os import requests -from typing import Optional, Dict, Any from .base import ProductInfo, BaseWrapper, Price -def get_product(asset_data: dict) -> 'ProductInfo': +def get_product(asset_data: dict) -> ProductInfo: product = ProductInfo() - product.id = asset_data['FROMSYMBOL'] + '-' + asset_data['TOSYMBOL'] - product.symbol = asset_data['FROMSYMBOL'] - product.price = float(asset_data['PRICE']) - product.volume_24h = float(asset_data['VOLUME24HOUR']) - product.status = "" # Cryptocompare does not provide status + product.id = asset_data.get('FROMSYMBOL', '') + '-' + asset_data.get('TOSYMBOL', '') + product.symbol = asset_data.get('FROMSYMBOL', '') + product.price = float(asset_data.get('PRICE', 0)) + product.volume_24h = float(asset_data.get('VOLUME24HOUR', 0)) + assert product.price > 0, "Invalid price data received from CryptoCompare" return product -def get_price(price_data: dict) -> 'Price': +def get_price(price_data: dict) -> Price: price = Price() - price.high = float(price_data['high']) - price.low = float(price_data['low']) - price.open = float(price_data['open']) - price.close = float(price_data['close']) - price.volume = float(price_data['volumeto']) - price.time = str(price_data['time']) + price.high = float(price_data.get('high', 0)) + price.low = float(price_data.get('low', 0)) + price.open = float(price_data.get('open', 0)) + price.close = float(price_data.get('close', 0)) + price.volume = float(price_data.get('volumeto', 0)) + price.timestamp_ms = price_data.get('time', 0) * 1000 + assert price.timestamp_ms > 0, "Invalid timestamp data received from CryptoCompare" return price @@ -39,7 +39,7 @@ class CryptoCompareWrapper(BaseWrapper): self.api_key = api_key self.currency = currency - def __request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + def __request(self, endpoint: str, params: dict[str, str] | None = None) -> dict[str, str]: if params is None: params = {} params['api_key'] = self.api_key @@ -67,7 +67,7 @@ class CryptoCompareWrapper(BaseWrapper): assets.append(get_product(asset_data)) return assets - def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[dict]: + def get_historical_prices(self, asset_id: str, limit: int = 100) -> list[Price]: response = self.__request("/data/v2/histohour", params = { "fsym": asset_id, "tsym": self.currency, diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py index 02af2f6..1b54e0a 100644 --- a/src/app/markets/yfinance.py +++ b/src/app/markets/yfinance.py @@ -12,7 +12,6 @@ def create_product_info(stock_data: dict[str, str]) -> ProductInfo: product.symbol = product.id.split('-')[0] # Rimuovi il suffisso della valuta per le crypto product.price = float(stock_data.get('Current Stock Price', f"0.0 USD").split(" ")[0]) # prende solo il numero product.volume_24h = 0.0 # YFinance non fornisce il volume 24h direttamente - product.status = "trading" if product.price > 0 else "offline" product.quote_currency = product.id.split('-')[0] # La valuta è la parte dopo il '-' return product @@ -26,7 +25,7 @@ def create_price_from_history(hist_data: dict[str, str]) -> Price: price.open = float(hist_data.get('Open', 0.0)) price.close = float(hist_data.get('Close', 0.0)) price.volume = float(hist_data.get('Volume', 0.0)) - price.time = hist_data.get('Timestamp', '') + price.timestamp_ms = int(hist_data.get('Timestamp', '0')) return price diff --git a/src/app/utils/market_aggregation.py b/src/app/utils/market_aggregation.py index 3d7b6b8..fb4f00a 100644 --- a/src/app/utils/market_aggregation.py +++ b/src/app/utils/market_aggregation.py @@ -2,7 +2,7 @@ import statistics from app.markets.base import ProductInfo, Price -def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[float]: +def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]: """Aggrega i prezzi storici per symbol calcolando la media""" raise NotImplementedError("Funzione non ancora implementata per problemi di timestamp he deve essere uniformato prima di usare questa funzione.") # TODO implementare l'aggregazione dopo aver modificato la classe Price in modo che abbia un timestamp integer @@ -40,11 +40,6 @@ def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[Produ product.symbol = symbol product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency) - statuses = {} - for p in product_list: - statuses[p.status] = statuses.get(p.status, 0) + 1 - product.status = max(statuses, key=statuses.get) if statuses else "" - prices = [p.price for p in product_list] product.price = statistics.mean(prices) diff --git a/tests/api/test_binance.py b/tests/api/test_binance.py index e4e0c20..dc4bfcb 100644 --- a/tests/api/test_binance.py +++ b/tests/api/test_binance.py @@ -45,8 +45,9 @@ class TestBinance: assert isinstance(history, list) assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') assert hasattr(entry, 'high') assert entry.close > 0 assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/api/test_coinbase.py b/tests/api/test_coinbase.py index b5f92e8..3ab8d43 100644 --- a/tests/api/test_coinbase.py +++ b/tests/api/test_coinbase.py @@ -47,8 +47,9 @@ class TestCoinBase: assert isinstance(history, list) assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') assert hasattr(entry, 'high') assert entry.close > 0 assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/api/test_cryptocompare.py b/tests/api/test_cryptocompare.py index 52aef9a..3c9133a 100644 --- a/tests/api/test_cryptocompare.py +++ b/tests/api/test_cryptocompare.py @@ -49,8 +49,9 @@ class TestCryptoCompare: assert isinstance(history, list) assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') assert hasattr(entry, 'high') assert entry.close > 0 assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/api/test_reddit.py b/tests/api/test_reddit.py index c82f56e..59cd61f 100644 --- a/tests/api/test_reddit.py +++ b/tests/api/test_reddit.py @@ -5,7 +5,7 @@ from app.social.reddit import MAX_COMMENTS, RedditWrapper @pytest.mark.social @pytest.mark.api -@pytest.mark.skipif(not(os.getenv("REDDIT_CLIENT_ID")) or not(os.getenv("REDDIT_API_CLIENT_ID")) or not os.getenv("REDDIT_API_CLIENT_SECRET"), reason="REDDIT_CLIENT_ID and REDDIT_API_CLIENT_SECRET not set in environment variables") +@pytest.mark.skipif(not(os.getenv("REDDIT_API_CLIENT_ID")) or not os.getenv("REDDIT_API_CLIENT_SECRET"), reason="REDDIT_CLIENT_ID and REDDIT_API_CLIENT_SECRET not set in environment variables") class TestRedditWrapper: def test_initialization(self): wrapper = RedditWrapper() diff --git a/tests/api/test_yfinance.py b/tests/api/test_yfinance.py index 8c70dfd..4971ccd 100644 --- a/tests/api/test_yfinance.py +++ b/tests/api/test_yfinance.py @@ -43,13 +43,14 @@ class TestYFinance: def test_yfinance_crypto_history(self): market = YFinanceWrapper() - history = market.get_historical_prices("BTC", limit=3) + history = market.get_historical_prices("BTC", limit=5) assert history is not None assert isinstance(history, list) - assert len(history) == 3 + assert len(history) == 5 for entry in history: - assert hasattr(entry, 'time') + assert hasattr(entry, 'timestamp_ms') assert hasattr(entry, 'close') + assert hasattr(entry, 'high') assert entry.close > 0 - assert hasattr(entry, 'open') - assert entry.open > 0 \ No newline at end of file + assert entry.high > 0 + assert entry.timestamp_ms > 0 diff --git a/tests/utils/test_market_aggregator.py b/tests/utils/test_market_aggregator.py index 57ef4a1..8f075e3 100644 --- a/tests/utils/test_market_aggregator.py +++ b/tests/utils/test_market_aggregator.py @@ -7,21 +7,30 @@ from app.utils.market_aggregation import aggregate_history_prices, aggregate_pro @pytest.mark.market class TestMarketDataAggregator: - def __product(self, symbol: str, price: float, volume: float, status: str, currency: str) -> ProductInfo: + def __product(self, symbol: str, price: float, volume: float, currency: str) -> ProductInfo: prod = ProductInfo() prod.id=f"{symbol}-{currency}" prod.symbol=symbol prod.price=price prod.volume_24h=volume - prod.status=status prod.quote_currency=currency return prod + def __price(self, timestamp_ms: int, high: float, low: float, open: float, close: float, volume: float) -> Price: + price = Price() + price.timestamp_ms = timestamp_ms + price.high = high + price.low = low + price.open = open + price.close = close + price.volume = volume + return price + def test_aggregate_product_info(self): products: dict[str, list[ProductInfo]] = { - "Provider1": [self.__product("BTC", 50000.0, 1000.0, "active", "USD")], - "Provider2": [self.__product("BTC", 50100.0, 1100.0, "active", "USD")], - "Provider3": [self.__product("BTC", 49900.0, 900.0, "inactive", "USD")], + "Provider1": [self.__product("BTC", 50000.0, 1000.0, "USD")], + "Provider2": [self.__product("BTC", 50100.0, 1100.0, "USD")], + "Provider3": [self.__product("BTC", 49900.0, 900.0, "USD")], } aggregated = aggregate_product_info(products) @@ -35,18 +44,17 @@ class TestMarketDataAggregator: avg_weighted_volume = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0) assert info.volume_24h == pytest.approx(avg_weighted_volume, rel=1e-3) - assert info.status == "active" assert info.quote_currency == "USD" def test_aggregate_product_info_multiple_symbols(self): products = { "Provider1": [ - self.__product("BTC", 50000.0, 1000.0, "active", "USD"), - self.__product("ETH", 4000.0, 2000.0, "active", "USD"), + self.__product("BTC", 50000.0, 1000.0, "USD"), + self.__product("ETH", 4000.0, 2000.0, "USD"), ], "Provider2": [ - self.__product("BTC", 50100.0, 1100.0, "active", "USD"), - self.__product("ETH", 4050.0, 2100.0, "active", "USD"), + self.__product("BTC", 50100.0, 1100.0, "USD"), + self.__product("ETH", 4050.0, 2100.0, "USD"), ], } @@ -60,45 +68,33 @@ class TestMarketDataAggregator: assert btc_info.price == pytest.approx(50050.0, rel=1e-3) avg_weighted_volume_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0) assert btc_info.volume_24h == pytest.approx(avg_weighted_volume_btc, rel=1e-3) - assert btc_info.status == "active" assert btc_info.quote_currency == "USD" assert eth_info is not None assert eth_info.price == pytest.approx(4025.0, rel=1e-3) avg_weighted_volume_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0) assert eth_info.volume_24h == pytest.approx(avg_weighted_volume_eth, rel=1e-3) - assert eth_info.status == "active" assert eth_info.quote_currency == "USD" def test_aggregate_history_prices(self): """Test aggregazione di prezzi storici usando aggregate_history_prices""" - price1 = Price( - timestamp="2024-06-01T00:00:00Z", - price=50000.0, - source="exchange1" - ) - price2 = Price( - timestamp="2024-06-01T00:00:00Z", - price=50100.0, - source="exchange2" - ) - price3 = Price( - timestamp="2024-06-01T01:00:00Z", - price=50200.0, - source="exchange1" - ) - price4 = Price( - timestamp="2024-06-01T01:00:00Z", - price=50300.0, - source="exchange2" - ) + prices = { + "Provider1": [ + self.__price(1685577600000, 50000.0, 49500.0, 49600.0, 49900.0, 150.0), + self.__price(1685581200000, 50200.0, 49800.0, 50000.0, 50100.0, 200.0), + ], + "Provider2": [ + self.__price(1685577600000, 50100.0, 49600.0, 49700.0, 50000.0, 180.0), + self.__price(1685581200000, 50300.0, 49900.0, 50100.0, 50200.0, 220.0), + ], + } - prices = [price1, price2, price3, price4] - aggregated_prices = aggregate_history_prices(prices) - - assert len(aggregated_prices) == 2 - assert aggregated_prices[0].timestamp == "2024-06-01T00:00:00Z" - assert aggregated_prices[0].price == pytest.approx(50050.0, rel=1e-3) - assert aggregated_prices[1].timestamp == "2024-06-01T01:00:00Z" - assert aggregated_prices[1].price == pytest.approx(50250.0, rel=1e-3) + aggregated = aggregate_history_prices(prices) + assert len(aggregated) == 2 + assert aggregated[0].timestamp_ms == 1685577600000 + assert aggregated[0].high == pytest.approx(50050.0, rel=1e-3) + assert aggregated[0].low == pytest.approx(49500.0, rel=1e-3) + assert aggregated[1].timestamp_ms == 1685581200000 + assert aggregated[1].high == pytest.approx(50250.0, rel=1e-3) + assert aggregated[1].low == pytest.approx(49800.0, rel=1e-3) -- 2.49.1 From 3ede7ba3f0aca75a13093d8cc95a9ef698e8b5ce Mon Sep 17 00:00:00 2001 From: Berack96 Date: Wed, 1 Oct 2025 23:51:10 +0200 Subject: [PATCH 13/18] feat: implement aggregate_history_prices function to calculate hourly price averages --- src/app/utils/market_aggregation.py | 42 +++++++++++++++++---------- tests/utils/test_market_aggregator.py | 4 +-- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/app/utils/market_aggregation.py b/src/app/utils/market_aggregation.py index fb4f00a..bb6c6d4 100644 --- a/src/app/utils/market_aggregation.py +++ b/src/app/utils/market_aggregation.py @@ -3,21 +3,33 @@ from app.markets.base import ProductInfo, Price def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]: - """Aggrega i prezzi storici per symbol calcolando la media""" - raise NotImplementedError("Funzione non ancora implementata per problemi di timestamp he deve essere uniformato prima di usare questa funzione.") - # TODO implementare l'aggregazione dopo aver modificato la classe Price in modo che abbia un timestamp integer - # aggregated_prices = [] - # for timestamp in range(len(next(iter(prices.values())))): - # timestamp_prices = [ - # price_list[timestamp].price - # for price_list in prices.values() - # if len(price_list) > timestamp and price_list[timestamp].price is not None - # ] - # if timestamp_prices: - # aggregated_prices.append(statistics.mean(timestamp_prices)) - # else: - # aggregated_prices.append(None) - # return aggregated_prices + """ + Aggrega i prezzi storici per symbol calcolando la media + + """ + max_list_length = max(len(p) for p in prices.values()) + + # Costruiamo una mappa timestamp_h -> lista di Price + timestamped_prices: dict[int, list[Price]] = {} + for _, price_list in prices.items(): + for price in price_list: + time = price.timestamp_ms - (price.timestamp_ms % 3600000) # arrotonda all'ora (non dovrebbe essere necessario) + timestamped_prices.setdefault(time, []).append(price) + + # Ora aggregiamo i prezzi per ogni ora + aggregated_prices = [] + for time, price_list in timestamped_prices.items(): + price = Price() + price.timestamp_ms = time + price.high = statistics.mean([p.high for p in price_list]) + price.low = statistics.mean([p.low for p in price_list]) + price.open = statistics.mean([p.open for p in price_list]) + price.close = statistics.mean([p.close for p in price_list]) + price.volume = statistics.mean([p.volume for p in price_list]) + aggregated_prices.append(price) + + assert(len(aggregated_prices) <= max_list_length) + return aggregated_prices def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[ProductInfo]: """ diff --git a/tests/utils/test_market_aggregator.py b/tests/utils/test_market_aggregator.py index 8f075e3..f641ee5 100644 --- a/tests/utils/test_market_aggregator.py +++ b/tests/utils/test_market_aggregator.py @@ -94,7 +94,7 @@ class TestMarketDataAggregator: assert len(aggregated) == 2 assert aggregated[0].timestamp_ms == 1685577600000 assert aggregated[0].high == pytest.approx(50050.0, rel=1e-3) - assert aggregated[0].low == pytest.approx(49500.0, rel=1e-3) + assert aggregated[0].low == pytest.approx(49550.0, rel=1e-3) assert aggregated[1].timestamp_ms == 1685581200000 assert aggregated[1].high == pytest.approx(50250.0, rel=1e-3) - assert aggregated[1].low == pytest.approx(49800.0, rel=1e-3) + assert aggregated[1].low == pytest.approx(49850.0, rel=1e-3) -- 2.49.1 From 31f38efdf5249d36ef84fc8102f02f57e223f4d5 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 2 Oct 2025 00:27:37 +0200 Subject: [PATCH 14/18] refactor: update docker-compose and app.py for improved environment variable handling and compatibility --- docker-compose.yaml | 8 -------- src/app.py | 5 +++-- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 884d9fd..20f5616 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,12 +9,4 @@ services: env_file: - .env environment: - # Modelli supportati - OLLAMA_HOST=http://host.docker.internal:11434 - - GOOGLE_API_KEY=${GOOGLE_API_KEY} - # Chiavi per le crypto API - - CDP_API_KEY_NAME=${CDP_API_KEY_NAME} - - CDP_API_PRIVATE_KEY=${CDP_API_PRIVATE_KEY} - - CRYPTOCOMPARE_API_KEY=${CRYPTOCOMPARE_API_KEY} - - BINANCE_API_KEY=${BINANCE_API_KEY} - - BINANCE_API_SECRET=${BINANCE_API_SECRET} diff --git a/src/app.py b/src/app.py index ec6d712..cf09fd5 100644 --- a/src/app.py +++ b/src/app.py @@ -42,6 +42,7 @@ if __name__ == "__main__": analyze_btn = gr.Button("🔎 Analizza") analyze_btn.click(fn=pipeline.interact, inputs=[user_input], outputs=output) - server, port = ("127.0.0.1", 8000) - log_info(f"Starting UPO AppAI on http://{server}:{port}") + server, port = ("0.0.0.0", 8000) # 0.0.0.0 per docker compatibility + server_log = "localhost" if server == "0.0.0.0" else server + log_info(f"Starting UPO AppAI on http://{server_log}:{port}") demo.launch(server_name=server, server_port=port, quiet=True) -- 2.49.1 From 8b81cd5a7193ecde7b0e07dc756e0ea79118b7aa Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 2 Oct 2025 00:28:42 +0200 Subject: [PATCH 15/18] feat: add detailed market instructions and improve error handling in price aggregation methods --- src/app/markets/__init__.py | 20 +++++++++++++- src/app/markets/base.py | 6 ++--- src/app/utils/market_aggregation.py | 25 ++++++++++------- tests/utils/test_market_aggregator.py | 39 ++++++++++++++++++++------- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/src/app/markets/__init__.py b/src/app/markets/__init__.py index 6f09b2f..b782b8f 100644 --- a/src/app/markets/__init__.py +++ b/src/app/markets/__init__.py @@ -82,7 +82,25 @@ class MarketAPIsTool(BaseWrapper, Toolkit): all_prices = self.wrappers.try_call_all(lambda w: w.get_historical_prices(asset_id, limit)) return aggregate_history_prices(all_prices) -# TODO definire istruzioni per gli agenti di mercato MARKET_INSTRUCTIONS = """ +**TASK:** You are a specialized **Crypto Price Data Retrieval Agent**. Your primary goal is to fetch the most recent and/or historical price data for requested cryptocurrency assets (e.g., 'BTC', 'ETH', 'SOL'). You must provide the data in a clear and structured format. +**AVAILABLE TOOLS:** +1. `get_products(asset_ids: list[str])`: Get **current** product/price info for a list of assets. **(PREFERITA: usa questa per i prezzi live)** +2. `get_historical_prices(asset_id: str, limit: int)`: Get historical price data for one asset. Default limit is 100. **(PREFERITA: usa questa per i dati storici)** +3. `get_products_aggregated(asset_ids: list[str])`: Get **aggregated current** product/price info for a list of assets. **(USA SOLO SE richiesto 'aggregato' o se `get_products` fallisce)** +4. `get_historical_prices_aggregated(asset_id: str, limit: int)`: Get **aggregated historical** price data for one asset. **(USA SOLO SE richiesto 'aggregato' o se `get_historical_prices` fallisce)** + +**USAGE GUIDELINE:** +* **Asset ID:** Always convert common names (e.g., 'Bitcoin', 'Ethereum') into their official ticker/ID (e.g., 'BTC', 'ETH'). +* **Cost Management (Cruciale per LLM locale):** + * **Priorità Bassa per Aggregazione:** **Non** usare i metodi `*aggregated` a meno che l'utente non lo richieda esplicitamente o se i metodi non-aggregati falliscono. + * **Limitazione Storica:** Il limite predefinito per i dati storici deve essere **20** punti dati, a meno che l'utente non specifichi un limite diverso. +* **Fallimento Tool:** Se lo strumento non restituisce dati per un asset specifico, rispondi per quell'asset con: "Dati di prezzo non trovati per [Asset ID]." + +**REPORTING REQUIREMENT:** +1. **Format:** Output the results in a clear, easy-to-read list or table. +2. **Live Price Request:** If an asset's *current price* is requested, report the **Asset ID**, **Latest Price**, and **Time/Date of the price**. +3. **Historical Price Request:** If *historical data* is requested, report the **Asset ID**, the **Limit** of points returned, and the **First** and **Last** entries from the list of historical prices (Date, Price). Non stampare l'intera lista di dati storici. +4. **Output:** For all requests, fornire un **unico e conciso riepilogo** dei dati reperiti. """ \ No newline at end of file diff --git a/src/app/markets/base.py b/src/app/markets/base.py index 4224014..1ef247b 100644 --- a/src/app/markets/base.py +++ b/src/app/markets/base.py @@ -14,7 +14,7 @@ class BaseWrapper: Returns: ProductInfo: An object containing product information. """ - raise NotImplementedError + raise NotImplementedError("This method should be overridden by subclasses") def get_products(self, asset_ids: list[str]) -> list['ProductInfo']: """ @@ -24,7 +24,7 @@ class BaseWrapper: Returns: list[ProductInfo]: A list of objects containing product information. """ - raise NotImplementedError + raise NotImplementedError("This method should be overridden by subclasses") def get_historical_prices(self, asset_id: str = "BTC", limit: int = 100) -> list['Price']: """ @@ -35,7 +35,7 @@ class BaseWrapper: Returns: list[Price]: A list of Price objects. """ - raise NotImplementedError + raise NotImplementedError("This method should be overridden by subclasses") class ProductInfo(BaseModel): """ diff --git a/src/app/utils/market_aggregation.py b/src/app/utils/market_aggregation.py index bb6c6d4..d0cdfb3 100644 --- a/src/app/utils/market_aggregation.py +++ b/src/app/utils/market_aggregation.py @@ -4,10 +4,12 @@ from app.markets.base import ProductInfo, Price def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]: """ - Aggrega i prezzi storici per symbol calcolando la media - + Aggrega i prezzi storici per symbol calcolando la media oraria. + Args: + prices (dict[str, list[Price]]): Mappa provider -> lista di Price + Returns: + list[Price]: Lista di Price aggregati per ora """ - max_list_length = max(len(p) for p in prices.values()) # Costruiamo una mappa timestamp_h -> lista di Price timestamped_prices: dict[int, list[Price]] = {} @@ -27,13 +29,15 @@ def aggregate_history_prices(prices: dict[str, list[Price]]) -> list[Price]: price.close = statistics.mean([p.close for p in price_list]) price.volume = statistics.mean([p.volume for p in price_list]) aggregated_prices.append(price) - - assert(len(aggregated_prices) <= max_list_length) return aggregated_prices def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[ProductInfo]: """ Aggrega una lista di ProductInfo per symbol. + Args: + products (dict[str, list[ProductInfo]]): Mappa provider -> lista di ProductInfo + Returns: + list[ProductInfo]: Lista di ProductInfo aggregati per symbol """ # Costruzione mappa symbol -> lista di ProductInfo @@ -48,15 +52,16 @@ def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[Produ for symbol, product_list in symbols_infos.items(): product = ProductInfo() - product.id = f"{symbol}_AGG" + product.id = f"{symbol}_AGGREGATED" product.symbol = symbol product.quote_currency = next(p.quote_currency for p in product_list if p.quote_currency) - prices = [p.price for p in product_list] - product.price = statistics.mean(prices) + volume_sum = sum(p.volume_24h for p in product_list) + product.volume_24h = volume_sum / len(product_list) if product_list else 0.0 + + prices = sum(p.price * p.volume_24h for p in product_list) + product.price = (prices / volume_sum) if volume_sum > 0 else 0.0 - volumes = [p.volume_24h for p in product_list] - product.volume_24h = sum([p * v for p, v in zip(prices, volumes)]) / sum(volumes) aggregated_products.append(product) confidence = _calculate_confidence(product_list, sources) # TODO necessary? diff --git a/tests/utils/test_market_aggregator.py b/tests/utils/test_market_aggregator.py index f641ee5..4f5611c 100644 --- a/tests/utils/test_market_aggregator.py +++ b/tests/utils/test_market_aggregator.py @@ -40,10 +40,10 @@ class TestMarketDataAggregator: info = aggregated[0] assert info is not None assert info.symbol == "BTC" - assert info.price == pytest.approx(50000.0, rel=1e-3) - avg_weighted_volume = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0) - assert info.volume_24h == pytest.approx(avg_weighted_volume, rel=1e-3) + avg_weighted_price = (50000.0 * 1000.0 + 50100.0 * 1100.0 + 49900.0 * 900.0) / (1000.0 + 1100.0 + 900.0) + assert info.price == pytest.approx(avg_weighted_price, rel=1e-3) + assert info.volume_24h == pytest.approx(1000.0, rel=1e-3) assert info.quote_currency == "USD" def test_aggregate_product_info_multiple_symbols(self): @@ -65,17 +65,38 @@ class TestMarketDataAggregator: eth_info = next((p for p in aggregated if p.symbol == "ETH"), None) assert btc_info is not None - assert btc_info.price == pytest.approx(50050.0, rel=1e-3) - avg_weighted_volume_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0) - assert btc_info.volume_24h == pytest.approx(avg_weighted_volume_btc, rel=1e-3) + avg_weighted_price_btc = (50000.0 * 1000.0 + 50100.0 * 1100.0) / (1000.0 + 1100.0) + assert btc_info.price == pytest.approx(avg_weighted_price_btc, rel=1e-3) + assert btc_info.volume_24h == pytest.approx(1050.0, rel=1e-3) assert btc_info.quote_currency == "USD" assert eth_info is not None - assert eth_info.price == pytest.approx(4025.0, rel=1e-3) - avg_weighted_volume_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0) - assert eth_info.volume_24h == pytest.approx(avg_weighted_volume_eth, rel=1e-3) + avg_weighted_price_eth = (4000.0 * 2000.0 + 4050.0 * 2100.0) / (2000.0 + 2100.0) + assert eth_info.price == pytest.approx(avg_weighted_price_eth, rel=1e-3) + assert eth_info.volume_24h == pytest.approx(2050.0, rel=1e-3) assert eth_info.quote_currency == "USD" + def test_aggregate_product_info_with_no_data(self): + products = { + "Provider1": [], + "Provider2": [], + } + aggregated = aggregate_product_info(products) + assert len(aggregated) == 0 + + def test_aggregate_product_info_with_partial_data(self): + products = { + "Provider1": [self.__product("BTC", 50000.0, 1000.0, "USD")], + "Provider2": [], + } + aggregated = aggregate_product_info(products) + assert len(aggregated) == 1 + info = aggregated[0] + assert info.symbol == "BTC" + assert info.price == pytest.approx(50000.0, rel=1e-3) + assert info.volume_24h == pytest.approx(1000.0, rel=1e-3) + assert info.quote_currency == "USD" + def test_aggregate_history_prices(self): """Test aggregazione di prezzi storici usando aggregate_history_prices""" -- 2.49.1 From 27ad0957130abc9e62646a939f74d7b0e64a1967 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 2 Oct 2025 00:32:49 +0200 Subject: [PATCH 16/18] feat: add aggregated news retrieval methods for top headlines and latest news --- src/app/news/__init__.py | 25 +++++++++++++++++++++++-- src/app/social/reddit.py | 4 ++-- src/app/utils/market_aggregation.py | 3 --- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/app/news/__init__.py b/src/app/news/__init__.py index 080c3ef..94873fd 100644 --- a/src/app/news/__init__.py +++ b/src/app/news/__init__.py @@ -45,13 +45,32 @@ class NewsAPIsTool(NewsWrapper, Toolkit): ], ) - # TODO Pensare se ha senso restituire gli articoli da TUTTI i wrapper o solo dal primo che funziona - # la modifica è banale, basta usare try_call_all invece di try_call def get_top_headlines(self, limit: int = 100) -> list[Article]: return self.wrapper_handler.try_call(lambda w: w.get_top_headlines(limit)) def get_latest_news(self, query: str, limit: int = 100) -> list[Article]: return self.wrapper_handler.try_call(lambda w: w.get_latest_news(query, limit)) + def get_top_headlines_aggregated(self, limit: int = 100) -> dict[str, list[Article]]: + """ + Calls get_top_headlines on all wrappers/providers and returns a dictionary mapping their names to their articles. + Args: + limit (int): Maximum number of articles to retrieve from each provider. + Returns: + dict[str, list[Article]]: A dictionary mapping providers names to their list of Articles + """ + return self.wrapper_handler.try_call_all(lambda w: w.get_top_headlines(limit)) + + def get_latest_news_aggregated(self, query: str, limit: int = 100) -> dict[str, list[Article]]: + """ + Calls get_latest_news on all wrappers/providers and returns a dictionary mapping their names to their articles. + Args: + query (str): The search query to find relevant news articles. + limit (int): Maximum number of articles to retrieve from each provider. + Returns: + dict[str, list[Article]]: A dictionary mapping providers names to their list of Articles + """ + return self.wrapper_handler.try_call_all(lambda w: w.get_latest_news(query, limit)) + NEWS_INSTRUCTIONS = """ **TASK:** You are a specialized **Crypto News Analyst**. Your goal is to fetch the latest news or top headlines related to cryptocurrencies, and then **analyze the sentiment** of the content to provide a concise report to the team leader. Prioritize 'crypto' or specific cryptocurrency names (e.g., 'Bitcoin', 'Ethereum') in your searches. @@ -59,6 +78,8 @@ NEWS_INSTRUCTIONS = """ **AVAILABLE TOOLS:** 1. `get_latest_news(query: str, limit: int)`: Get the 'limit' most recent news articles for a specific 'query'. 2. `get_top_headlines(limit: int)`: Get the 'limit' top global news headlines. +3. `get_latest_news_aggregated(query: str, limit: int)`: Get aggregated latest news articles for a specific 'query'. +4. `get_top_headlines_aggregated(limit: int)`: Get aggregated top global news headlines. **USAGE GUIDELINE:** * Always use `get_latest_news` with a relevant crypto-related query first. diff --git a/src/app/social/reddit.py b/src/app/social/reddit.py index e0b4928..904448d 100644 --- a/src/app/social/reddit.py +++ b/src/app/social/reddit.py @@ -4,8 +4,8 @@ from praw.models import Submission, MoreComments from .base import SocialWrapper, SocialPost, SocialComment MAX_COMMENTS = 5 -# TODO mettere piu' subreddit? -# scelti da https://lkiconsulting.io/marketing/best-crypto-subreddits/ +# metterne altri se necessario. +# fonti: https://lkiconsulting.io/marketing/best-crypto-subreddits/ SUBREDDITS = [ "CryptoCurrency", "Bitcoin", diff --git a/src/app/utils/market_aggregation.py b/src/app/utils/market_aggregation.py index d0cdfb3..f20e4fb 100644 --- a/src/app/utils/market_aggregation.py +++ b/src/app/utils/market_aggregation.py @@ -63,9 +63,6 @@ def aggregate_product_info(products: dict[str, list[ProductInfo]]) -> list[Produ product.price = (prices / volume_sum) if volume_sum > 0 else 0.0 aggregated_products.append(product) - - confidence = _calculate_confidence(product_list, sources) # TODO necessary? - return aggregated_products def _calculate_confidence(products: list[ProductInfo], sources: list[str]) -> float: -- 2.49.1 From ca463f9f5fcec5e53056fb589535254873e2b627 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 2 Oct 2025 01:23:46 +0200 Subject: [PATCH 17/18] refactor: improve error messages in WrapperHandler for better clarity --- src/app/utils/wrapper_handler.py | 26 +++++++++++++++++++------- tests/utils/test_wrapper_handler.py | 24 ++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/app/utils/wrapper_handler.py b/src/app/utils/wrapper_handler.py index 3429c13..40fe371 100644 --- a/src/app/utils/wrapper_handler.py +++ b/src/app/utils/wrapper_handler.py @@ -1,3 +1,4 @@ +import inspect import time import traceback from typing import TypeVar, Callable, Generic, Iterable, Type @@ -45,17 +46,24 @@ class WrapperHandler(Generic[W]): Raises: Exception: If all wrappers fail after retries. """ + log_info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}") + iterations = 0 while iterations < len(self.wrappers): + wrapper = self.wrappers[self.index] + wrapper_name = wrapper.__class__.__name__ + try: - wrapper = self.wrappers[self.index] - log_info(f"Trying wrapper: {wrapper} - function {func}") + log_info(f"try_call {wrapper_name}") result = func(wrapper) + log_info(f"{wrapper_name} succeeded") self.retry_count = 0 return result + except Exception as e: self.retry_count += 1 - log_warning(f"{wrapper} failed {self.retry_count}/{self.retry_per_wrapper}: {WrapperHandler.__concise_error(e)}") + error = WrapperHandler.__concise_error(e) + log_warning(f"{wrapper_name} failed {self.retry_count}/{self.retry_per_wrapper}: {error}") if self.retry_count >= self.retry_per_wrapper: self.index = (self.index + 1) % len(self.wrappers) @@ -64,7 +72,7 @@ class WrapperHandler(Generic[W]): else: time.sleep(self.retry_delay) - raise Exception(f"All wrappers failed after retries") + raise Exception(f"All wrappers failed, latest error: {error}") def try_call_all(self, func: Callable[[W], T]) -> dict[str, T]: """ @@ -78,16 +86,20 @@ class WrapperHandler(Generic[W]): Raises: Exception: If all wrappers fail. """ + log_info(f"{inspect.getsource(func).strip()} {inspect.getclosurevars(func).nonlocals}") + results = {} - log_info(f"All wrappers: {[wrapper.__class__ for wrapper in self.wrappers]} - function {func}") for wrapper in self.wrappers: + wrapper_name = wrapper.__class__.__name__ try: result = func(wrapper) + log_info(f"{wrapper_name} succeeded") results[wrapper.__class__] = result except Exception as e: - log_warning(f"{wrapper} failed: {WrapperHandler.__concise_error(e)}") + error = WrapperHandler.__concise_error(e) + log_warning(f"{wrapper_name} failed: {error}") if not results: - raise Exception("All wrappers failed") + raise Exception(f"All wrappers failed, latest error: {error}") return results @staticmethod diff --git a/tests/utils/test_wrapper_handler.py b/tests/utils/test_wrapper_handler.py index 154d3dc..996f632 100644 --- a/tests/utils/test_wrapper_handler.py +++ b/tests/utils/test_wrapper_handler.py @@ -54,7 +54,7 @@ class TestWrapperHandler: with pytest.raises(Exception) as exc_info: handler.try_call(lambda w: w.do_something()) - assert "All wrappers failed after retries" in str(exc_info.value) + assert "All wrappers failed" in str(exc_info.value) def test_success_on_first_try(self): wrappers = [MockWrapper, FailingWrapper] @@ -121,7 +121,6 @@ class TestWrapperHandler: handler_all_fail.try_call_all(lambda w: w.do_something()) assert "All wrappers failed" in str(exc_info.value) - def test_wrappers_with_parameters(self): wrappers = [FailingWrapperWithParameters, MockWrapperWithParameters] handler: WrapperHandler[MockWrapperWithParameters] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0) @@ -130,3 +129,24 @@ class TestWrapperHandler: assert result == "Success test and 42" assert handler.index == 1 # Should have switched to the second wrapper assert handler.retry_count == 0 + + def test_wrappers_with_parameters_all_fail(self): + wrappers = [FailingWrapperWithParameters, FailingWrapperWithParameters] + handler: WrapperHandler[MockWrapperWithParameters] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0) + + with pytest.raises(Exception) as exc_info: + handler.try_call(lambda w: w.do_something("test", 42)) + assert "All wrappers failed" in str(exc_info.value) + + def test_try_call_all_with_parameters(self): + wrappers = [FailingWrapperWithParameters, MockWrapperWithParameters] + handler: WrapperHandler[MockWrapperWithParameters] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0) + results = handler.try_call_all(lambda w: w.do_something("param", 99)) + assert results == {MockWrapperWithParameters: "Success param and 99"} + + def test_try_call_all_with_parameters_all_fail(self): + wrappers = [FailingWrapperWithParameters, FailingWrapperWithParameters] + handler: WrapperHandler[MockWrapperWithParameters] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0) + with pytest.raises(Exception) as exc_info: + handler.try_call_all(lambda w: w.do_something("param", 99)) + assert "All wrappers failed" in str(exc_info.value) -- 2.49.1 From c978b92268bb16b2f19d0ece543caaccf038b8a0 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Thu, 2 Oct 2025 01:36:57 +0200 Subject: [PATCH 18/18] fix: correct quote currency extraction in create_product_info and remove debug prints from tests --- src/app/markets/yfinance.py | 2 +- tests/tools/test_news_tool.py | 4 ---- tests/tools/test_socials_tool.py | 2 -- tests/utils/test_market_aggregator.py | 1 - 4 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/app/markets/yfinance.py b/src/app/markets/yfinance.py index 1b54e0a..acfacb8 100644 --- a/src/app/markets/yfinance.py +++ b/src/app/markets/yfinance.py @@ -12,7 +12,7 @@ def create_product_info(stock_data: dict[str, str]) -> ProductInfo: product.symbol = product.id.split('-')[0] # Rimuovi il suffisso della valuta per le crypto product.price = float(stock_data.get('Current Stock Price', f"0.0 USD").split(" ")[0]) # prende solo il numero product.volume_24h = 0.0 # YFinance non fornisce il volume 24h direttamente - product.quote_currency = product.id.split('-')[0] # La valuta è la parte dopo il '-' + product.quote_currency = product.id.split('-')[1] # La valuta è la parte dopo il '-' return product def create_price_from_history(hist_data: dict[str, str]) -> Price: diff --git a/tests/tools/test_news_tool.py b/tests/tools/test_news_tool.py index cb820ba..5a57f82 100644 --- a/tests/tools/test_news_tool.py +++ b/tests/tools/test_news_tool.py @@ -33,10 +33,8 @@ class TestNewsAPITool: result = tool.wrapper_handler.try_call_all(lambda w: w.get_top_headlines(limit=2)) assert isinstance(result, dict) assert len(result.keys()) > 0 - print("Results from providers:", result.keys()) for provider, articles in result.items(): for article in articles: - print(provider, article.title) assert article.title is not None assert article.source is not None @@ -45,9 +43,7 @@ class TestNewsAPITool: result = tool.wrapper_handler.try_call_all(lambda w: w.get_latest_news(query="crypto", limit=2)) assert isinstance(result, dict) assert len(result.keys()) > 0 - print("Results from providers:", result.keys()) for provider, articles in result.items(): for article in articles: - print(provider, article.title) assert article.title is not None assert article.source is not None diff --git a/tests/tools/test_socials_tool.py b/tests/tools/test_socials_tool.py index 9c66afa..d08ed0f 100644 --- a/tests/tools/test_socials_tool.py +++ b/tests/tools/test_socials_tool.py @@ -24,9 +24,7 @@ class TestSocialAPIsTool: result = tool.wrapper_handler.try_call_all(lambda w: w.get_top_crypto_posts(limit=2)) assert isinstance(result, dict) assert len(result.keys()) > 0 - print("Results from providers:", result.keys()) for provider, posts in result.items(): for post in posts: - print(provider, post.title) assert post.title is not None assert post.time is not None diff --git a/tests/utils/test_market_aggregator.py b/tests/utils/test_market_aggregator.py index 4f5611c..d7881ef 100644 --- a/tests/utils/test_market_aggregator.py +++ b/tests/utils/test_market_aggregator.py @@ -34,7 +34,6 @@ class TestMarketDataAggregator: } aggregated = aggregate_product_info(products) - print(aggregated) assert len(aggregated) == 1 info = aggregated[0] -- 2.49.1