├── .env.example ├── .envrc ├── .gitattributes ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── api ├── cbbiinfo_api.py ├── coinsoto_api.py └── glassnode_api.py ├── asciinema ├── thumbnail.webp └── thumbnail.xcf ├── fetch_bitcoin_data.py ├── main.py ├── metrics ├── base_metric.py ├── mvrv_z_score.py ├── pi_cycle.py ├── puell_multiple.py ├── reserve_risk.py ├── rhodl_ratio.py ├── rupl.py ├── trolololo.py ├── two_year_moving_average.py └── woobull_topcap_cvdd.py ├── pyproject.toml ├── shell.nix ├── utils.py └── uv.lock /.env.example: -------------------------------------------------------------------------------- 1 | GLASSNODE_API_KEY= 2 | TELEGRAM_TOKEN= 3 | TELEGRAM_CHAT_ID= 4 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # shellcheck disable=SC2148 2 | 3 | use nix 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/dotenv,python,pycharm,visualstudiocode,direnv 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=dotenv,python,pycharm,visualstudiocode,direnv 3 | 4 | ### direnv ### 5 | .direnv 6 | 7 | ### dotenv ### 8 | .env 9 | 10 | ### PyCharm ### 11 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 12 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 13 | 14 | # User-specific stuff 15 | .idea/**/workspace.xml 16 | .idea/**/tasks.xml 17 | .idea/**/usage.statistics.xml 18 | .idea/**/dictionaries 19 | .idea/**/shelf 20 | 21 | # AWS User-specific 22 | .idea/**/aws.xml 23 | 24 | # Generated files 25 | .idea/**/contentModel.xml 26 | 27 | # Sensitive or high-churn files 28 | .idea/**/dataSources/ 29 | .idea/**/dataSources.ids 30 | .idea/**/dataSources.local.xml 31 | .idea/**/sqlDataSources.xml 32 | .idea/**/dynamic.xml 33 | .idea/**/uiDesigner.xml 34 | .idea/**/dbnavigator.xml 35 | 36 | # Gradle 37 | .idea/**/gradle.xml 38 | .idea/**/libraries 39 | 40 | # Gradle and Maven with auto-import 41 | # When using Gradle or Maven with auto-import, you should exclude module files, 42 | # since they will be recreated, and may cause churn. Uncomment if using 43 | # auto-import. 44 | # .idea/artifacts 45 | # .idea/compiler.xml 46 | # .idea/jarRepositories.xml 47 | # .idea/modules.xml 48 | # .idea/*.iml 49 | # .idea/modules 50 | # *.iml 51 | # *.ipr 52 | 53 | # CMake 54 | cmake-build-*/ 55 | 56 | # Mongo Explorer plugin 57 | .idea/**/mongoSettings.xml 58 | 59 | # File-based project format 60 | *.iws 61 | 62 | # IntelliJ 63 | out/ 64 | 65 | # mpeltonen/sbt-idea plugin 66 | .idea_modules/ 67 | 68 | # JIRA plugin 69 | atlassian-ide-plugin.xml 70 | 71 | # Cursive Clojure plugin 72 | .idea/replstate.xml 73 | 74 | # SonarLint plugin 75 | .idea/sonarlint/ 76 | 77 | # Crashlytics plugin (for Android Studio and IntelliJ) 78 | com_crashlytics_export_strings.xml 79 | crashlytics.properties 80 | crashlytics-build.properties 81 | fabric.properties 82 | 83 | # Editor-based Rest Client 84 | .idea/httpRequests 85 | 86 | # Android studio 3.1+ serialized cache file 87 | .idea/caches/build_file_checksums.ser 88 | 89 | ### PyCharm Patch ### 90 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 91 | 92 | # *.iml 93 | # modules.xml 94 | # .idea/misc.xml 95 | # *.ipr 96 | 97 | # Sonarlint plugin 98 | # https://plugins.jetbrains.com/plugin/7973-sonarlint 99 | .idea/**/sonarlint/ 100 | 101 | # SonarQube Plugin 102 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin 103 | .idea/**/sonarIssues.xml 104 | 105 | # Markdown Navigator plugin 106 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced 107 | .idea/**/markdown-navigator.xml 108 | .idea/**/markdown-navigator-enh.xml 109 | .idea/**/markdown-navigator/ 110 | 111 | # Cache file creation bug 112 | # See https://youtrack.jetbrains.com/issue/JBR-2257 113 | .idea/$CACHE_FILE$ 114 | 115 | # CodeStream plugin 116 | # https://plugins.jetbrains.com/plugin/12206-codestream 117 | .idea/codestream.xml 118 | 119 | # Azure Toolkit for IntelliJ plugin 120 | # https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij 121 | .idea/**/azureSettings.xml 122 | 123 | ### Python ### 124 | # Byte-compiled / optimized / DLL files 125 | __pycache__/ 126 | *.py[cod] 127 | *$py.class 128 | 129 | # C extensions 130 | *.so 131 | 132 | # Distribution / packaging 133 | .Python 134 | build/ 135 | develop-eggs/ 136 | dist/ 137 | downloads/ 138 | eggs/ 139 | .eggs/ 140 | lib/ 141 | lib64/ 142 | parts/ 143 | sdist/ 144 | var/ 145 | wheels/ 146 | share/python-wheels/ 147 | *.egg-info/ 148 | .installed.cfg 149 | *.egg 150 | MANIFEST 151 | 152 | # PyInstaller 153 | # Usually these files are written by a python script from a template 154 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 155 | *.manifest 156 | *.spec 157 | 158 | # Installer logs 159 | pip-log.txt 160 | pip-delete-this-directory.txt 161 | 162 | # Unit test / coverage reports 163 | htmlcov/ 164 | .tox/ 165 | .nox/ 166 | .coverage 167 | .coverage.* 168 | .cache 169 | nosetests.xml 170 | coverage.xml 171 | *.cover 172 | *.py,cover 173 | .hypothesis/ 174 | .pytest_cache/ 175 | cover/ 176 | 177 | # Translations 178 | *.mo 179 | *.pot 180 | 181 | # Django stuff: 182 | *.log 183 | local_settings.py 184 | db.sqlite3 185 | db.sqlite3-journal 186 | 187 | # Flask stuff: 188 | instance/ 189 | .webassets-cache 190 | 191 | # Scrapy stuff: 192 | .scrapy 193 | 194 | # Sphinx documentation 195 | docs/_build/ 196 | 197 | # PyBuilder 198 | .pybuilder/ 199 | target/ 200 | 201 | # Jupyter Notebook 202 | .ipynb_checkpoints 203 | 204 | # IPython 205 | profile_default/ 206 | ipython_config.py 207 | 208 | # pyenv 209 | # For a library or package, you might want to ignore these files since the code is 210 | # intended to run in multiple environments; otherwise, check them in: 211 | # .python-version 212 | 213 | # pipenv 214 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 215 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 216 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 217 | # install all needed dependencies. 218 | #Pipfile.lock 219 | 220 | # poetry 221 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 222 | # This is especially recommended for binary packages to ensure reproducibility, and is more 223 | # commonly ignored for libraries. 224 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 225 | #poetry.lock 226 | 227 | # pdm 228 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 229 | #pdm.lock 230 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 231 | # in version control. 232 | # https://pdm.fming.dev/#use-with-ide 233 | .pdm.toml 234 | 235 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 236 | __pypackages__/ 237 | 238 | # Celery stuff 239 | celerybeat-schedule 240 | celerybeat.pid 241 | 242 | # SageMath parsed files 243 | *.sage.py 244 | 245 | # Environments 246 | .venv 247 | env/ 248 | venv/ 249 | ENV/ 250 | env.bak/ 251 | venv.bak/ 252 | 253 | # Spyder project settings 254 | .spyderproject 255 | .spyproject 256 | 257 | # Rope project settings 258 | .ropeproject 259 | 260 | # mkdocs documentation 261 | /site 262 | 263 | # mypy 264 | .mypy_cache/ 265 | .dmypy.json 266 | dmypy.json 267 | 268 | # Pyre type checker 269 | .pyre/ 270 | 271 | # pytype static type analyzer 272 | .pytype/ 273 | 274 | # Cython debug symbols 275 | cython_debug/ 276 | 277 | # PyCharm 278 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 279 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 280 | # and can be added to the global gitignore or merged into this file. For a more nuclear 281 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 282 | #.idea/ 283 | 284 | ### Python Patch ### 285 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 286 | poetry.toml 287 | 288 | # ruff 289 | .ruff_cache/ 290 | 291 | # LSP config files 292 | pyrightconfig.json 293 | 294 | ### VisualStudioCode ### 295 | .vscode/* 296 | !.vscode/settings.json 297 | !.vscode/tasks.json 298 | !.vscode/launch.json 299 | !.vscode/extensions.json 300 | !.vscode/*.code-snippets 301 | 302 | # Local History for Visual Studio Code 303 | .history/ 304 | 305 | # Built Visual Studio Code Extensions 306 | *.vsix 307 | 308 | ### VisualStudioCode Patch ### 309 | # Ignore all local history of files 310 | .history 311 | .ionide 312 | 313 | # End of https://www.toptal.com/developers/gitignore/api/dotenv,python,pycharm,visualstudiocode,direnv 314 | 315 | # filecache 316 | *.cache.* 317 | *.cache 318 | 319 | # application specific 320 | output 321 | *.ignore 322 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "console": "integratedTerminal", 5 | "name": "Python Debugger: Current File", 6 | "program": "${file}", 7 | "request": "launch", 8 | "type": "debugpy" 9 | }, 10 | { 11 | "console": "integratedTerminal", 12 | "justMyCode": true, 13 | "name": "Python: main.py", 14 | "program": "${workspaceFolder}/main.py", 15 | "request": "launch", 16 | "type": "debugpy" 17 | } 18 | ], 19 | "version": "0.2.0" 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ColinTalksCrypto Bitcoin Bull Run Index (CBBI) 2 | 3 | ![Python version](https://shields.monicz.dev/badge/python-v3.13-blue) 4 | [![GitHub Repo stars](https://shields.monicz.dev/github/stars/Zaczero/CBBI?style=social)](https://github.com/Zaczero/CBBI) 5 | 6 | The official Python implementation of the **ColinTalksCrypto Bitcoin Bull Run Index** (CBBI). 7 | 8 | The CBBI is a Bitcoin index that utilizes advanced, real-time analysis of 9 metrics 9 | to help us understand what stage of the Bitcoin bull run and bear market cycles we are in. 10 | The CBBI is time-independent and price-independent. 11 | It simply indicates whether it believes we are approaching the top/bottom of a Bitcoin cycle. 12 | 13 | If you want to learn more, [check out this video](https://www.youtube.com/watch?v=bq7djf1n0j4). 14 | 15 | ## Visit our website 16 | 17 | Bookmark it and receive latest CBBI updates. 18 | 19 | - [CBBI.info](https://cbbi.info/) 20 | 21 | ## Script Demo 22 | 23 | [![asciicast](https://raw.githubusercontent.com/Zaczero/CBBI/main/asciinema/thumbnail.webp)](https://asciinema.org/a/KFkbKPULf9PGvY8Fmh4QLn0FE) 24 | 25 | ## Docker Usage 26 | 27 | To use the CBBI script with Docker, run the following command: 28 | 29 | ```sh 30 | docker run --rm --pull=always zaczero/cbbi --help 31 | docker run --rm --pull=always zaczero/cbbi 32 | ``` 33 | 34 | ## Manual Usage 35 | 36 | To use the CBBI script without Docker, follow these two simple steps: 37 | 38 | ### 1. Install nix 39 | 40 | Before you jump in, make sure to install the [❄️ Nix](https://nixos.org/download) package manager. It's your shortcut to seamless dependency management and reproducible environment setup. It will save you lots of time and spare you from unnecessary stress. 41 | 42 | ### 2. Run the application 43 | 44 | ```sh 45 | nix-shell --run "python main.py --help" 46 | ``` 47 | 48 | #### or with using interactive shell 49 | 50 | ```sh 51 | nix-shell 52 | python main.py --help 53 | ``` 54 | 55 | ## Metrics 56 | 57 | The current CBBI version _(November 2022)_ includes the following metrics: 58 | 59 | | Name | Link | 60 | | ---------------------------- | --------------------------------------------------------------------- | 61 | | Pi Cycle Top Indicator | [Visit page](https://coinank.com/indexdata/piCycleTop) | 62 | | RUPL/NUPL Chart | [Visit page](https://coinank.com/indexdata/realizedProf) | 63 | | RHODL Ratio | [Visit page](https://coinank.com/indexdata/rhodlRatio) | 64 | | Puell Multiple | [Visit page](https://coinank.com/indexdata/puellMultiple) | 65 | | 2 Year Moving Average | [Visit page](https://coinank.com/indexdata/year2MA) | 66 | | Bitcoin Trolololo Trend Line | [Visit page](https://www.blockchaincenter.net/bitcoin-rainbow-chart/) | 67 | | MVRV Z-Score | [Visit page](https://coinank.com/indexdata/score) | 68 | | Reserve Risk | [Visit page](https://coinank.com/indexdata/reserveRisk) | 69 | | Woobull Top Cap vs CVDD | [Visit page](https://woocharts.com/bitcoin-price-models/) | 70 | 71 | ## Environment Variables 72 | 73 | This project supports `.env` files, which provide a convenient way of setting environment variables. 74 | 75 | To use this feature, create a file called `.env` in the project's root directory, 76 | and add environment variables in the following format: 77 | 78 | ```sh 79 | VARIABLE_NAME=value 80 | ``` 81 | 82 | ### GLASSNODE_API_KEY 83 | 84 | Defines an API key to be used during GlassNode fallback requests. 85 | If unset or empty, a cache fallback will be used instead (via CBBI.info). 86 | 87 | #### Example usage 88 | 89 | - GLASSNODE_API_KEY=REPLACE_ME 90 | 91 | ### TELEGRAM_TOKEN, TELEGRAM_CHAT_ID 92 | 93 | Define both variables to receive Telegram notifications about metric errors that occur during the execution. 94 | 95 | #### Example usage 96 | 97 | - TELEGRAM_TOKEN=REPLACE_ME 98 | - TELEGRAM_CHAT_ID=123456 99 | 100 | ## Footer 101 | 102 | ### Contact me 103 | 104 | 105 | 106 | ### License 107 | 108 | This project is licensed under the GNU Affero General Public License v3.0. 109 | 110 | The complete license text can be accessed in the repository at [LICENSE](https://github.com/Zaczero/CBBI/blob/main/LICENSE). 111 | -------------------------------------------------------------------------------- /api/cbbiinfo_api.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | from utils import HTTP 4 | 5 | 6 | def cbbi_fetch(key: str) -> pd.DataFrame: 7 | response = HTTP.get('https://colintalkscrypto.com/cbbi/data/latest.json') 8 | response.raise_for_status() 9 | response_data = response.json()[key] 10 | 11 | df = pd.DataFrame( 12 | response_data.items(), 13 | columns=[ 14 | 'Date', 15 | 'Value', 16 | ], 17 | ) 18 | df['Date'] = pd.to_datetime(df['Date'], unit='s').dt.tz_localize(None) 19 | 20 | return df 21 | -------------------------------------------------------------------------------- /api/coinsoto_api.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | from utils import HTTP 4 | 5 | 6 | def cs_fetch(path: str, data_selector: str, col_name: str) -> pd.DataFrame: 7 | response = HTTP.get(f'https://coinank.com/indicatorapi/{path}') 8 | response.raise_for_status() 9 | data = response.json()['data'] 10 | 11 | if 'timeList' not in data and 'line' in data: 12 | data = data['line'] 13 | 14 | data_x = data['timeList'] 15 | data_y = data[data_selector] 16 | assert len(data_x) == len(data_y), f'{len(data_x)=} != {len(data_y)=}' 17 | 18 | df = pd.DataFrame( 19 | { 20 | 'Date': data_x[: len(data_y)], 21 | col_name: data_y, 22 | } 23 | ) 24 | 25 | df['Date'] = pd.to_datetime(df['Date'], unit='ms').dt.tz_localize(None) 26 | 27 | return df 28 | -------------------------------------------------------------------------------- /api/glassnode_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pandas as pd 4 | 5 | from utils import HTTP 6 | 7 | 8 | def gn_fetch(url_selector: str, col_name: str, **kwargs) -> pd.DataFrame: 9 | api_key = os.getenv('GLASSNODE_API_KEY') 10 | 11 | if not api_key: 12 | raise Exception('GlassNode fallback in unavailable (missing api key)') 13 | 14 | response = HTTP.get( 15 | f'https://api.glassnode.com/v1/metrics/indicators/{url_selector}', 16 | params=kwargs, 17 | headers={'X-Api-Key': api_key}, 18 | ) 19 | response.raise_for_status() 20 | response_json = response.json() 21 | response_x = [d['t'] for d in response_json] 22 | response_y = [d['v'] for d in response_json] 23 | 24 | df = pd.DataFrame( 25 | { 26 | 'Date': response_x, 27 | col_name: response_y, 28 | } 29 | ) 30 | df['Date'] = pd.to_datetime(df['Date'], unit='s').dt.tz_localize(None) 31 | 32 | return df 33 | -------------------------------------------------------------------------------- /asciinema/thumbnail.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zaczero/CBBI/4b275fa095b550bf5f7e9de15e6d8f0ab6fa14be/asciinema/thumbnail.webp -------------------------------------------------------------------------------- /asciinema/thumbnail.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zaczero/CBBI/4b275fa095b550bf5f7e9de15e6d8f0ab6fa14be/asciinema/thumbnail.xcf -------------------------------------------------------------------------------- /fetch_bitcoin_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from filecache import filecache 4 | 5 | from utils import HTTP, mark_days_since, mark_highs_lows 6 | 7 | 8 | @filecache(7200) # 2 hours 9 | def fetch_bitcoin_data() -> pd.DataFrame: 10 | """ 11 | Fetches historical Bitcoin data into a DataFrame. 12 | Very early data is discarded due to high volatility. 13 | 14 | Returns: 15 | DataFrame containing Bitcoin data. 16 | """ 17 | print('📈 Requesting historical Bitcoin data…') 18 | 19 | response = HTTP.get( 20 | 'https://api.blockchair.com/bitcoin/blocks', 21 | params={ 22 | 'a': 'date,count(),min(id),max(id),sum(generation),sum(generation_usd)', 23 | 's': 'date(desc)', 24 | }, 25 | ) 26 | response.raise_for_status() 27 | response_json = response.json() 28 | 29 | df = pd.DataFrame(response_json['data'][::-1]) 30 | df.rename( 31 | columns={ 32 | 'date': 'Date', 33 | 'count()': 'TotalBlocks', 34 | 'min(id)': 'MinBlockID', 35 | 'max(id)': 'MaxBlockID', 36 | 'sum(generation)': 'TotalGeneration', 37 | 'sum(generation_usd)': 'TotalGenerationUSD', 38 | }, 39 | inplace=True, 40 | ) 41 | 42 | df['Date'] = pd.to_datetime(df['Date']) 43 | df['TotalGeneration'] /= 1e8 44 | df['BlockGeneration'] = df['TotalGeneration'] / df['TotalBlocks'] 45 | df['BlockGenerationUSD'] = df['TotalGenerationUSD'] / df['TotalBlocks'] 46 | 47 | df = df.merge(fetch_price_data(), on='Date', how='left') 48 | df.loc[df['Price'].isna(), 'Price'] = df['BlockGenerationUSD'] / df['BlockGeneration'] 49 | df['PriceLog'] = np.log(df['Price']) 50 | df['PriceLogInterp'] = np.interp( 51 | x=df['PriceLog'], 52 | xp=(df['PriceLog'].min(), df['PriceLog'].max()), 53 | fp=(0, 1), 54 | ) 55 | 56 | df = df.loc[df['Date'] >= '2011-06-27'] 57 | df.reset_index(drop=True, inplace=True) 58 | 59 | df = fix_current_day_data(df) 60 | df = add_block_halving_data(df) 61 | df = mark_highs_lows(df, 'Price', False, round(365 * 2), 180) 62 | 63 | # move 2021' peak to the first price peak 64 | df.loc[df['Date'] == '2021-11-09', 'PriceHigh'] = 0 65 | df.loc[df['Date'] == '2021-04-14', 'PriceHigh'] = 1 66 | 67 | df = mark_days_since(df, ['PriceHigh', 'PriceLow', 'Halving']) 68 | return df 69 | 70 | 71 | def fetch_price_data() -> pd.DataFrame: 72 | response = HTTP.get( 73 | 'https://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail/chart', 74 | params={ 75 | 'id': 1, 76 | 'range': 'ALL', 77 | }, 78 | ) 79 | 80 | response.raise_for_status() 81 | response_json = response.json() 82 | response_x = [float(k) for k in response_json['data']['points']] 83 | response_y = [value['v'][0] for value in response_json['data']['points'].values()] 84 | 85 | df = pd.DataFrame( 86 | { 87 | 'Date': response_x, 88 | 'Price': response_y, 89 | } 90 | ) 91 | df['Date'] = pd.to_datetime(df['Date'], unit='s').dt.tz_localize(None).dt.floor('d') 92 | df.sort_values(by='Date', inplace=True) 93 | df.drop_duplicates('Date', keep='last', inplace=True) 94 | 95 | return df 96 | 97 | 98 | def fix_current_day_data(df: pd.DataFrame) -> pd.DataFrame: 99 | row = df.iloc[-1].copy() 100 | 101 | target_total_blocks = 24 * 6 102 | target_scale = target_total_blocks / row['TotalBlocks'] 103 | 104 | for col_name in ['TotalBlocks', 'TotalGeneration', 'TotalGenerationUSD']: 105 | row[col_name] *= target_scale 106 | 107 | df.iloc[-1] = row 108 | return df 109 | 110 | 111 | def add_block_halving_data(df: pd.DataFrame) -> pd.DataFrame: 112 | reward_halving_every = 210000 113 | current_block_halving_id = reward_halving_every 114 | current_block_production = 50 115 | df['Halving'] = 0 116 | df['NextHalvingBlock'] = current_block_halving_id 117 | 118 | while True: 119 | df.loc[ 120 | (current_block_halving_id - reward_halving_every) <= df['MaxBlockID'], 121 | 'BlockGeneration', 122 | ] = current_block_production 123 | 124 | block_halving_row = df[ 125 | (df['MinBlockID'] <= current_block_halving_id) & (df['MaxBlockID'] >= current_block_halving_id) 126 | ].squeeze() 127 | 128 | if block_halving_row.shape[0] == 0: 129 | break 130 | 131 | current_block_halving_id += reward_halving_every 132 | current_block_production /= 2 133 | df.loc[block_halving_row.name, 'Halving'] = 1 134 | df.loc[df.index > block_halving_row.name, 'NextHalvingBlock'] = current_block_halving_id 135 | 136 | df['DaysToHalving'] = pd.to_timedelta((df['NextHalvingBlock'] - df['MaxBlockID']) / (24 * 6), unit='D') 137 | df['NextHalvingDate'] = df['Date'] + df['DaysToHalving'] 138 | return df 139 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import traceback 4 | from pathlib import Path 5 | 6 | import fire 7 | import numpy as np 8 | import pandas as pd 9 | import seaborn as sns 10 | from matplotlib import pyplot as plt 11 | from pyfiglet import figlet_format 12 | from sty import bg, ef, fg, rs 13 | from tqdm import tqdm 14 | 15 | from fetch_bitcoin_data import fetch_bitcoin_data 16 | from metrics.base_metric import BaseMetric 17 | from metrics.mvrv_z_score import MVRVMetric 18 | from metrics.pi_cycle import PiCycleMetric 19 | from metrics.puell_multiple import PuellMetric 20 | from metrics.reserve_risk import ReserveRiskMetric 21 | from metrics.rhodl_ratio import RHODLMetric 22 | from metrics.rupl import RUPLMetric 23 | from metrics.trolololo import TrolololoMetric 24 | from metrics.two_year_moving_average import TwoYearMovingAverageMetric 25 | from metrics.woobull_topcap_cvdd import WoobullMetric 26 | from utils import format_percentage, get_color 27 | 28 | 29 | def get_metrics() -> list[BaseMetric]: 30 | """ 31 | Returns a list of available metrics to be calculated. 32 | """ 33 | return [ 34 | PiCycleMetric(), 35 | RUPLMetric(), 36 | RHODLMetric(), 37 | PuellMetric(), 38 | TwoYearMovingAverageMetric(), 39 | TrolololoMetric(), 40 | MVRVMetric(), 41 | ReserveRiskMetric(), 42 | WoobullMetric(), 43 | ] 44 | 45 | 46 | def calculate_confidence_score(df: pd.DataFrame, cols: list[str]) -> pd.Series: 47 | """ 48 | Calculate the confidence score for a DataFrame. 49 | 50 | This function takes in a DataFrame and a list of column names 51 | and returns a Series with the mean value of the specified columns for each row. 52 | 53 | Args: 54 | df: A pandas DataFrame. 55 | cols: A list of column names to include in the calculation. 56 | 57 | Returns: 58 | A pandas Series with the mean value for the specified columns for each row in the DataFrame. 59 | """ 60 | return df[cols].mean(axis=1) 61 | 62 | 63 | async def run(json_file: str, charts_file: str, output_dir: str | None) -> None: 64 | output_dir_path = Path.cwd() if output_dir is None else Path(output_dir) 65 | 66 | json_file_path = output_dir_path / Path(json_file) 67 | charts_file_path = output_dir_path / Path(charts_file) 68 | 69 | if not output_dir_path.exists(): 70 | output_dir_path.mkdir(mode=0o755, parents=True) 71 | 72 | df_bitcoin = fetch_bitcoin_data() 73 | df_bitcoin_org = df_bitcoin.copy() 74 | 75 | current_price = df_bitcoin['Price'].tail(1).values[0] 76 | print('Current Bitcoin price: ' + ef.b + fg.li_green + bg.da_green + f' $ {round(current_price):,} ' + rs.all) 77 | 78 | metrics = get_metrics() 79 | metrics_cols = [] 80 | metrics_descriptions = [] 81 | 82 | sns.set( 83 | font_scale=0.225, 84 | rc={ 85 | 'figure.titlesize': 12, # For suptitle (overridden later) 86 | 'axes.titlesize': 7.5, # 50% larger than original 5 87 | 'axes.labelsize': 6, # 50% larger than original 4 88 | 'xtick.labelsize': 4, 89 | 'ytick.labelsize': 4, 90 | 'lines.linewidth': 0.5, 91 | 'grid.linewidth': 0.3, 92 | 'savefig.dpi': 1000, 93 | 'figure.dpi': 300, 94 | }, 95 | ) 96 | 97 | axes_per_metric = 2 98 | fig, axes = plt.subplots(len(metrics), axes_per_metric, figsize=(4 * axes_per_metric, 3 * len(metrics))) 99 | axes = axes.reshape(-1, axes_per_metric) 100 | 101 | # Adjust layout 102 | plt.tight_layout(pad=10) 103 | plt.subplots_adjust(top=0.98) 104 | 105 | # Updated title 106 | plt.suptitle("CBBI metric data input → output", fontsize=11.25, weight='bold', y=0.99508) 107 | 108 | for metric, ax_row in zip(metrics, axes, strict=True): 109 | # Swap chart positions so visual flow goes from left to right. 110 | df_bitcoin[metric.name] = (await metric.calculate(df_bitcoin_org.copy(), [ax_row[1], ax_row[0]])).clip(0, 1) 111 | metrics_cols.append(metric.name) 112 | metrics_descriptions.append(metric.description) 113 | 114 | # Add black horizontal lines at y=1 and y=0 to show metric boundaries. 115 | ax_row[1].axhline(y=1, color='black', linewidth=0.5) 116 | ax_row[1].axhline(y=0, color='black', linewidth=0.5) 117 | 118 | # Shade above y=1 and below y=0 with 10% black, to bring focus to the data within range. 119 | y_min, y_max = ax_row[1].get_ylim() # Get the y-axis limits for reference 120 | # Shade above y=1 to the top edge 121 | ax_row[1].fill_betweenx( 122 | y=[1, y_max], # From y=1 to the top 123 | x1=0, x2=1, # Full width in axes fraction (0 to 1) 124 | transform=ax_row[1].get_yaxis_transform(), # Use y-data coordinates, x-axes fraction 125 | color='black', alpha=0.1, edgecolor='none', zorder=0 126 | ) 127 | # Shade below y=0 to the bottom edge 128 | ax_row[1].fill_betweenx( 129 | y=[y_min, 0], # From bottom to y=0 130 | x1=0, x2=1, # Full width in axes fraction (0 to 1) 131 | transform=ax_row[1].get_yaxis_transform(), # Use y-data coordinates, x-axes fraction 132 | color='black', alpha=0.1, edgecolor='none', zorder=0 133 | ) 134 | 135 | # Add a gray arrow between charts, to make directional flow very clear. 136 | ax_row[0].annotate( 137 | '', 138 | xy=(1.0967, 0.75), xycoords='axes fraction', 139 | xytext=(1.0367, 0.75), textcoords='axes fraction', 140 | arrowprops=dict(arrowstyle='->', color='darkgray', lw=1.5, shrinkA=0, shrinkB=0, mutation_scale=10), 141 | ha='center', va='center' 142 | ) 143 | 144 | print('Generating charts…') 145 | plt.savefig(charts_file_path) 146 | 147 | confidence_col = 'Confidence' 148 | 149 | df_result = pd.DataFrame(df_bitcoin[['Date', 'Price', *metrics_cols]]) 150 | df_result.set_index('Date', inplace=True) 151 | df_result[confidence_col] = calculate_confidence_score(df_result, metrics_cols) 152 | df_result.to_json(json_file_path, double_precision=4, date_unit='s', indent=2) 153 | 154 | df_result_last = df_result.tail(1) 155 | confidence_details = { 156 | description: df_result_last[name].iloc[0] 157 | for name, description in zip(metrics_cols, metrics_descriptions, strict=True) 158 | } 159 | 160 | print('\n' + ef.b + ':: Confidence we are at the peak ::' + rs.all) 161 | print( 162 | fg.cyan 163 | + ef.bold 164 | + figlet_format(format_percentage(df_result_last[confidence_col].iloc[0], ''), font='univers') 165 | + rs.all, 166 | end='', 167 | ) 168 | 169 | for description, value in confidence_details.items(): 170 | if not np.isnan(value): 171 | print(fg.white + get_color(value) + f'{format_percentage(value)} ' + rs.all, end='') 172 | print(f' - {description}') 173 | 174 | print() 175 | print('Source code: ' + ef.u + fg.li_blue + 'https://github.com/Zaczero/CBBI' + rs.all) 176 | print('License: ' + ef.b + 'AGPL-3.0' + rs.all) 177 | print() 178 | 179 | 180 | def run_and_retry( 181 | json_file: str = 'latest.json', 182 | charts_file: str = 'charts.svg', 183 | output_dir: str | None = 'output', 184 | max_attempts: int = 10, 185 | sleep_seconds_on_error: int = 10, 186 | ) -> None: 187 | """ 188 | Calculates the current CBBI confidence value alongside all the required metrics. 189 | Everything gets pretty printed to the current standard output and a clean copy 190 | is saved to a JSON file specified by the path in the ``json_file`` argument. 191 | A charts image is generated on the path specified by the ``charts_file`` argument 192 | which summarizes all individual metrics' historical data in a visual way. 193 | The execution is attempted multiple times in case an error occurs. 194 | 195 | Args: 196 | json_file: File path where the output is saved in the JSON format. 197 | charts_file: File path where the charts image is saved (formats supported by pyplot.savefig). 198 | output_dir: Directory path where the output is stored. 199 | If set to ``None`` then use the current working directory. 200 | If the directory does not exist, it will be created. 201 | max_attempts: Maximum number of attempts before termination. An attempt is counted when an error occurs. 202 | sleep_seconds_on_error: Duration of the sleep in seconds before attempting again after an error occurs. 203 | 204 | Returns: 205 | None 206 | """ 207 | assert max_attempts > 0, 'Value of the max_attempts argument must be positive' 208 | assert sleep_seconds_on_error >= 0, 'Value of the sleep_seconds_on_error argument must be non-negative' 209 | 210 | for _ in range(max_attempts): 211 | try: 212 | asyncio.run(run(json_file, charts_file, output_dir)) 213 | exit(0) 214 | 215 | except Exception: 216 | print(fg.black + bg.yellow + ' An error has occurred! ' + rs.all) 217 | traceback.print_exc() 218 | 219 | print(f'\nRetrying in {sleep_seconds_on_error} seconds…', flush=True) 220 | for _ in tqdm(range(sleep_seconds_on_error)): 221 | time.sleep(1) 222 | 223 | print(f'Max attempts limit has been reached ({max_attempts}).') 224 | print('Better luck next time!') 225 | exit(-1) 226 | 227 | 228 | if __name__ == '__main__': 229 | fire.Fire(run_and_retry) 230 | -------------------------------------------------------------------------------- /metrics/base_metric.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from abc import ABC, abstractmethod 3 | 4 | import pandas as pd 5 | from matplotlib.axes import Axes 6 | from sty import bg, fg, rs 7 | 8 | from api.cbbiinfo_api import cbbi_fetch 9 | from utils import send_error_notification 10 | 11 | 12 | class BaseMetric(ABC): 13 | @property 14 | @abstractmethod 15 | def name(self) -> str: 16 | pass 17 | 18 | @property 19 | @abstractmethod 20 | def description(self) -> str: 21 | pass 22 | 23 | @abstractmethod 24 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 25 | pass 26 | 27 | def _fallback(self, df: pd.DataFrame) -> pd.Series: 28 | df = df.merge(cbbi_fetch(self.name), on='Date', how='left') 29 | df['Value'] = df['Value'].ffill() 30 | 31 | return df['Value'] 32 | 33 | async def calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 34 | try: 35 | return self._calculate(df, ax) 36 | except Exception as ex: 37 | traceback.print_exc() 38 | await send_error_notification(ex) 39 | 40 | print(fg.black + bg.yellow + f' Requesting fallback values for {self.name} (from CBBI.info) ' + rs.all) 41 | return self._fallback(df) 42 | -------------------------------------------------------------------------------- /metrics/mvrv_z_score.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | from matplotlib.axes import Axes 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from api.coinsoto_api import cs_fetch 8 | from metrics.base_metric import BaseMetric 9 | from utils import add_common_markers 10 | 11 | 12 | class MVRVMetric(BaseMetric): 13 | @property 14 | def name(self) -> str: 15 | return 'MVRV' 16 | 17 | @property 18 | def description(self) -> str: 19 | return 'MVRV Z-Score' 20 | 21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 22 | bull_days_shift = 6 23 | low_model_adjust = 0.26 24 | 25 | df = df.merge( 26 | cs_fetch( 27 | path='chain/index/charts?type=/charts/mvrv-zscore/', 28 | data_selector='value4', 29 | col_name='MVRV', 30 | ), 31 | on='Date', 32 | how='left', 33 | ) 34 | df.loc[df['DaysSinceHalving'] < df['DaysSincePriceLow'], 'MVRV'] = df['MVRV'].shift(bull_days_shift) 35 | df['MVRV'] = df['MVRV'].ffill() 36 | df['MVRV'] = np.log(df['MVRV'] + 1) 37 | 38 | high_rows = df.loc[df['PriceHigh'] == 1] 39 | high_x = high_rows.index.values.reshape(-1, 1) 40 | high_y = high_rows['MVRV'].values.reshape(-1, 1) 41 | 42 | low_rows = df.loc[df['PriceLow'] == 1] 43 | low_x = low_rows.index.values.reshape(-1, 1) 44 | low_y = low_rows['MVRV'].values.reshape(-1, 1) 45 | 46 | x = df.index.values.reshape(-1, 1) 47 | 48 | lin_model = LinearRegression() 49 | lin_model.fit(high_x, high_y) 50 | df['HighModel'] = lin_model.predict(x) 51 | 52 | lin_model.fit(low_x, low_y) 53 | df['LowModel'] = lin_model.predict(x) + low_model_adjust 54 | 55 | df['Index'] = (df['MVRV'] - df['LowModel']) / (df['HighModel'] - df['LowModel']) 56 | 57 | df['IndexNoNa'] = df['Index'].fillna(0) 58 | ax[0].set_title(self.description) 59 | sns.lineplot(data=df, x='Date', y='IndexNoNa', ax=ax[0]) 60 | add_common_markers(df, ax[0]) 61 | 62 | sns.lineplot(data=df, x='Date', y='MVRV', ax=ax[1]) 63 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1]) 64 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1]) 65 | add_common_markers(df, ax[1], price_line=False) 66 | 67 | return df['Index'] 68 | -------------------------------------------------------------------------------- /metrics/pi_cycle.py: -------------------------------------------------------------------------------- 1 | from itertools import zip_longest 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import seaborn as sns 6 | from matplotlib.axes import Axes 7 | 8 | from metrics.base_metric import BaseMetric 9 | from utils import add_common_markers, mark_highs_lows, split_df_on_index_gap 10 | 11 | 12 | class PiCycleMetric(BaseMetric): 13 | @property 14 | def name(self) -> str: 15 | return 'PiCycle' 16 | 17 | @property 18 | def description(self) -> str: 19 | return 'Pi Cycle Top Indicator' 20 | 21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 22 | df['111DMA'] = df['Price'].rolling(111).mean() 23 | df['350DMAx2'] = df['Price'].rolling(350).mean() * 2 24 | 25 | df['111DMALog'] = np.log(df['111DMA']) 26 | df['350DMAx2Log'] = np.log(df['350DMAx2']) 27 | df['PiCycleDiff'] = np.abs(df['111DMALog'] - df['350DMAx2Log']) 28 | df['PiCycleDiffThreshold'] = 0.0 29 | 30 | df['PiCycleIsFluke'] = df['111DMA'] > df['350DMAx2'] 31 | 32 | df_flukes = [*split_df_on_index_gap(df[df['PiCycleIsFluke']])] 33 | df_actuals = [*split_df_on_index_gap(df[~df['PiCycleIsFluke']])] 34 | 35 | for df_fluke, df_actual, df_fluke_next in zip_longest(df_flukes, df_actuals[1:], df_flukes[1:], fillvalue=None): 36 | if df_fluke is None: 37 | break 38 | 39 | max_divergence_idx = df_fluke['PiCycleDiff'].argmax() 40 | max_divergence_row = df_fluke.iloc[max_divergence_idx] 41 | df.loc[max_divergence_row.name < df.index, 'PiCycleDiffThreshold'] = max_divergence_row['PiCycleDiff'] 42 | 43 | if df_actual is not None: 44 | df_actual_above = df_actual[df_actual['PiCycleDiff'] >= max_divergence_row['PiCycleDiff']] 45 | 46 | if df_actual_above.shape[0] > 0: 47 | df.loc[df_actual_above.index.min() <= df.index, 'PiCycleDiffThreshold'] = 0 48 | 49 | if df_fluke_next is not None: 50 | df.loc[df_fluke_next.index.min() <= df.index, 'PiCycleDiffThreshold'] = 0 51 | 52 | df.loc[df['PiCycleDiff'] < df['PiCycleDiffThreshold'], 'PiCycleDiff'] = df['PiCycleDiffThreshold'] 53 | df = mark_highs_lows(df, 'PiCycleDiff', True, round(365 * 2), 365) 54 | 55 | for _, row in df.loc[df['PiCycleDiffHigh'] == 1].iterrows(): 56 | df.loc[df.index > row.name, 'PreviousPiCycleDiffHighValue'] = row['PiCycleDiff'] 57 | 58 | df['PiCycleIndex'] = 1 - (df['PiCycleDiff'] / df['PreviousPiCycleDiffHighValue']) 59 | df.loc[df['PiCycleIndex'] < 0, 'PiCycleIndex'] = 0 60 | 61 | df['PiCycleIndexNoNa'] = df['PiCycleIndex'].fillna(0) 62 | ax[0].set_title(self.description) 63 | sns.lineplot(data=df, x='Date', y='PiCycleIndexNoNa', ax=ax[0]) 64 | add_common_markers(df, ax[0]) 65 | 66 | return df['PiCycleIndex'] 67 | -------------------------------------------------------------------------------- /metrics/puell_multiple.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | from matplotlib.axes import Axes 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from api.coinsoto_api import cs_fetch 8 | from metrics.base_metric import BaseMetric 9 | from utils import add_common_markers 10 | 11 | 12 | class PuellMetric(BaseMetric): 13 | @property 14 | def name(self) -> str: 15 | return 'Puell' 16 | 17 | @property 18 | def description(self) -> str: 19 | return 'Puell Multiple' 20 | 21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 22 | df = df.merge( 23 | cs_fetch( 24 | path='getPuellMultiple', 25 | data_selector='puellMultiplList', 26 | col_name='Puell', 27 | ), 28 | on='Date', 29 | how='left', 30 | ) 31 | df['Puell'] = df['Puell'].ffill() 32 | df['PuellLog'] = np.log(df['Puell']) 33 | 34 | high_rows = df.loc[df['PriceHigh'] == 1] 35 | high_x = high_rows.index.values.reshape(-1, 1) 36 | high_y = high_rows['PuellLog'].values.reshape(-1, 1) 37 | 38 | # low_rows = df.loc[df['PriceLow'] == 1][1:] 39 | # low_x = low_rows.index.values.reshape(-1, 1) 40 | # low_y = low_rows['PuellLog'].values.reshape(-1, 1) 41 | 42 | x = df.index.values.reshape(-1, 1) 43 | 44 | lin_model = LinearRegression() 45 | lin_model.fit(high_x, high_y) 46 | df['PuellLogHighModel'] = lin_model.predict(x) 47 | 48 | # lin_model.fit(low_x, low_y) 49 | # df['PuellLogLowModel'] = lin_model.predict(x) 50 | df['PuellLogLowModel'] = -1 51 | 52 | df['PuellIndex'] = (df['PuellLog'] - df['PuellLogLowModel']) / ( 53 | df['PuellLogHighModel'] - df['PuellLogLowModel'] 54 | ) 55 | 56 | df['PuellIndexNoNa'] = df['PuellIndex'].fillna(0) 57 | ax[0].set_title(self.description) 58 | sns.lineplot(data=df, x='Date', y='PuellIndexNoNa', ax=ax[0]) 59 | add_common_markers(df, ax[0]) 60 | 61 | sns.lineplot(data=df, x='Date', y='PuellLog', ax=ax[1]) 62 | sns.lineplot(data=df, x='Date', y='PuellLogHighModel', ax=ax[1]) 63 | sns.lineplot(data=df, x='Date', y='PuellLogLowModel', ax=ax[1]) 64 | add_common_markers(df, ax[1], price_line=False) 65 | 66 | return df['PuellIndex'] 67 | -------------------------------------------------------------------------------- /metrics/reserve_risk.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | from matplotlib.axes import Axes 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from api.coinsoto_api import cs_fetch 8 | from metrics.base_metric import BaseMetric 9 | from utils import add_common_markers 10 | 11 | 12 | class ReserveRiskMetric(BaseMetric): 13 | @property 14 | def name(self) -> str: 15 | return 'ReserveRisk' 16 | 17 | @property 18 | def description(self) -> str: 19 | return 'Reserve Risk' 20 | 21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 22 | days_shift = 1 23 | 24 | df = df.merge( 25 | cs_fetch( 26 | path='chain/index/charts?type=/charts/reserve-risk/', 27 | data_selector='value4', 28 | col_name='Risk', 29 | ), 30 | on='Date', 31 | how='left', 32 | ) 33 | df['Risk'] = df['Risk'].shift(days_shift, fill_value=np.nan) 34 | df['Risk'] = df['Risk'].ffill() 35 | df['RiskLog'] = np.log(df['Risk']) 36 | 37 | high_rows = df.loc[df['PriceHigh'] == 1] 38 | high_x = high_rows.index.values.reshape(-1, 1) 39 | high_y = high_rows['RiskLog'].values.reshape(-1, 1) 40 | 41 | low_rows = df.loc[df['PriceLow'] == 1][1:] 42 | low_x = low_rows.index.values.reshape(-1, 1) 43 | low_y = low_rows['RiskLog'].values.reshape(-1, 1) 44 | 45 | x = df.index.values.reshape(-1, 1) 46 | 47 | lin_model = LinearRegression() 48 | lin_model.fit(high_x, high_y) 49 | df['HighModel'] = lin_model.predict(x) 50 | df['HighModel'] = df['HighModel'] - 0.15 51 | 52 | lin_model.fit(low_x, low_y) 53 | df['LowModel'] = lin_model.predict(x) 54 | 55 | df['RiskIndex'] = (df['RiskLog'] - df['LowModel']) / (df['HighModel'] - df['LowModel']) 56 | 57 | df['RiskIndexNoNa'] = df['RiskIndex'].fillna(0) 58 | ax[0].set_title(self.description) 59 | sns.lineplot(data=df, x='Date', y='RiskIndexNoNa', ax=ax[0]) 60 | add_common_markers(df, ax[0]) 61 | 62 | sns.lineplot(data=df, x='Date', y='RiskLog', ax=ax[1]) 63 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1]) 64 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1]) 65 | add_common_markers(df, ax[1], price_line=False) 66 | 67 | return df['RiskIndex'] 68 | -------------------------------------------------------------------------------- /metrics/rhodl_ratio.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | import numpy as np 4 | import pandas as pd 5 | import seaborn as sns 6 | from matplotlib.axes import Axes 7 | from sklearn.linear_model import LinearRegression 8 | from sty import bg, fg, rs 9 | 10 | from api.coinsoto_api import cs_fetch 11 | from api.glassnode_api import gn_fetch 12 | from metrics.base_metric import BaseMetric 13 | from utils import add_common_markers 14 | 15 | 16 | class RHODLMetric(BaseMetric): 17 | @property 18 | def name(self) -> str: 19 | return 'RHODL' 20 | 21 | @property 22 | def description(self) -> str: 23 | return 'RHODL Ratio' 24 | 25 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 26 | try: 27 | remote_df = cs_fetch( 28 | path='chain/index/charts?type=/charts/rhodl-ratio/', 29 | data_selector='value1', 30 | col_name='RHODL', 31 | ) 32 | except Exception: 33 | traceback.print_exc() 34 | print(fg.black + bg.yellow + f' Requesting fallback values for {self.name} (from GlassNode) ' + rs.all) 35 | 36 | remote_df = gn_fetch(url_selector='rhodl_ratio', col_name='RHODL', a='BTC') 37 | 38 | df = df.merge(remote_df, on='Date', how='left') 39 | df['RHODL'] = df['RHODL'].ffill() 40 | df['RHODLLog'] = np.log(df['RHODL']) 41 | 42 | high_rows = df.loc[(df['PriceHigh'] == 1) | (df['Date'] == '2024-12-18')] 43 | high_x = high_rows.index.values.reshape(-1, 1) 44 | high_y = high_rows['RHODLLog'].values.reshape(-1, 1) 45 | 46 | low_rows = df.loc[df['PriceLow'] == 1][1:] 47 | low_x = low_rows.index.values.reshape(-1, 1) 48 | low_y = low_rows['RHODLLog'].values.reshape(-1, 1) 49 | 50 | x = df.index.values.reshape(-1, 1) 51 | 52 | lin_model = LinearRegression() 53 | lin_model.fit(high_x, high_y) 54 | df['RHODLLogHighModel'] = lin_model.predict(x) 55 | 56 | lin_model.fit(low_x, low_y) 57 | df['RHODLLogLowModel'] = lin_model.predict(x) 58 | 59 | df['RHODLIndex'] = (df['RHODLLog'] - df['RHODLLogLowModel']) / ( 60 | df['RHODLLogHighModel'] - df['RHODLLogLowModel'] 61 | ) 62 | 63 | df['RHODLIndexNoNa'] = df['RHODLIndex'].fillna(0) 64 | ax[0].set_title(self.description) 65 | sns.lineplot(data=df, x='Date', y='RHODLIndexNoNa', ax=ax[0]) 66 | add_common_markers(df, ax[0]) 67 | 68 | sns.lineplot(data=df, x='Date', y='RHODLLog', ax=ax[1]) 69 | sns.lineplot(data=df, x='Date', y='RHODLLogHighModel', ax=ax[1]) 70 | sns.lineplot(data=df, x='Date', y='RHODLLogLowModel', ax=ax[1]) 71 | add_common_markers(df, ax[1], price_line=False) 72 | 73 | return df['RHODLIndex'] 74 | -------------------------------------------------------------------------------- /metrics/rupl.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import seaborn as sns 3 | from matplotlib.axes import Axes 4 | from sklearn.linear_model import LinearRegression 5 | 6 | from api.coinsoto_api import cs_fetch 7 | from metrics.base_metric import BaseMetric 8 | from utils import add_common_markers 9 | 10 | 11 | class RUPLMetric(BaseMetric): 12 | @property 13 | def name(self) -> str: 14 | return 'RUPL' 15 | 16 | @property 17 | def description(self) -> str: 18 | return 'RUPL/NUPL Chart' 19 | 20 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 21 | df = df.merge( 22 | cs_fetch( 23 | path='chain/index/charts?type=/charts/relative-unrealized-prof/', 24 | data_selector='value1', 25 | col_name='RUPL', 26 | ), 27 | on='Date', 28 | how='left', 29 | ) 30 | df['RUPL'] = df['RUPL'].ffill() 31 | 32 | high_rows = df.loc[df['PriceHigh'] == 1] 33 | high_x = high_rows.index.values.reshape(-1, 1) 34 | high_y = high_rows['RUPL'].values.reshape(-1, 1) 35 | 36 | low_rows = df.loc[df['PriceLow'] == 1][1:] 37 | low_x = low_rows.index.values.reshape(-1, 1) 38 | low_y = low_rows['RUPL'].values.reshape(-1, 1) 39 | 40 | x = df.index.values.reshape(-1, 1) 41 | 42 | lin_model = LinearRegression() 43 | lin_model.fit(high_x, high_y) 44 | df['HighModel'] = lin_model.predict(x) 45 | 46 | lin_model.fit(low_x, low_y) 47 | df['LowModel'] = lin_model.predict(x) 48 | 49 | df['RUPLIndex'] = (df['RUPL'] - df['LowModel']) / (df['HighModel'] - df['LowModel']) 50 | 51 | ax[0].set_title(self.description) 52 | sns.lineplot(data=df, x='Date', y='RUPLIndex', ax=ax[0]) 53 | add_common_markers(df, ax[0]) 54 | 55 | sns.lineplot(data=df, x='Date', y='RUPL', ax=ax[1]) 56 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1]) 57 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1]) 58 | add_common_markers(df, ax[1], price_line=False) 59 | 60 | return df['RUPLIndex'] 61 | -------------------------------------------------------------------------------- /metrics/trolololo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | from matplotlib.axes import Axes 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from metrics.base_metric import BaseMetric 8 | from utils import add_common_markers 9 | 10 | 11 | class TrolololoMetric(BaseMetric): 12 | @property 13 | def name(self) -> str: 14 | return 'Trolololo' 15 | 16 | @property 17 | def description(self) -> str: 18 | return 'Bitcoin Trolololo Trend Line' 19 | 20 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 21 | begin_date = pd.to_datetime('2012-01-01') 22 | 23 | df['TroloDaysSinceBegin'] = (df['Date'] - begin_date).dt.days 24 | 25 | # Maximum Bubble Territory 26 | df['TroloTopPrice'] = np.power(10, 2.900 * np.log(df['TroloDaysSinceBegin'] + 1400) - 19.463) 27 | df['TroloTopPriceLog'] = np.log(df['TroloTopPrice']) 28 | 29 | # Basically a Fire Sale 30 | df['TroloBottomPrice'] = np.power(10, 2.788 * np.log(df['TroloDaysSinceBegin'] + 1200) - 19.463) 31 | df['TroloBottomPriceLog'] = np.log(df['TroloBottomPrice']) 32 | 33 | df['TroloDifference'] = df['TroloTopPriceLog'] - df['TroloBottomPriceLog'] 34 | df['TroloOvershootActual'] = df['PriceLog'] - df['TroloTopPriceLog'] 35 | df['TroloUndershootActual'] = df['PriceLog'] - df['TroloBottomPriceLog'] 36 | 37 | high_rows = df.loc[(df['PriceHigh'] == 1) & (df['Date'] >= begin_date)] 38 | high_x = high_rows.index.values.reshape(-1, 1) 39 | high_y = high_rows['TroloOvershootActual'].values.reshape(-1, 1) 40 | high_y[0] *= 0.6 # the first value seems too high 41 | 42 | low_rows = df.loc[(df['PriceLow'] == 1) & (df['Date'] >= begin_date)] 43 | low_x = low_rows.index.values.reshape(-1, 1) 44 | low_y = low_rows['TroloUndershootActual'].values.reshape(-1, 1) 45 | 46 | x = df.index.values.reshape(-1, 1) 47 | 48 | lin_model = LinearRegression() 49 | lin_model.fit(high_x, high_y) 50 | df['TroloOvershootModel'] = lin_model.predict(x) 51 | 52 | lin_model.fit(low_x, low_y) 53 | df['TroloUndershootModel'] = lin_model.predict(x) 54 | 55 | df['TroloHighModel'] = df['TroloTopPriceLog'] + df['TroloOvershootModel'] 56 | df['TroloLowModel'] = df['TroloBottomPriceLog'] + df['TroloUndershootModel'] 57 | 58 | df['TroloIndex'] = (df['PriceLog'] - df['TroloLowModel']) / (df['TroloHighModel'] - df['TroloLowModel']) 59 | 60 | ax[0].set_title(self.description) 61 | sns.lineplot(data=df, x='Date', y='TroloIndex', ax=ax[0]) 62 | add_common_markers(df, ax[0]) 63 | 64 | sns.lineplot(data=df, x='Date', y='PriceLog', ax=ax[1]) 65 | sns.lineplot(data=df, x='Date', y='TroloHighModel', ax=ax[1]) 66 | sns.lineplot(data=df, x='Date', y='TroloLowModel', ax=ax[1]) 67 | add_common_markers(df, ax[1], price_line=False) 68 | 69 | return df['TroloIndex'] 70 | -------------------------------------------------------------------------------- /metrics/two_year_moving_average.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | from matplotlib.axes import Axes 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from api.coinsoto_api import cs_fetch 8 | from metrics.base_metric import BaseMetric 9 | from utils import add_common_markers 10 | 11 | 12 | class TwoYearMovingAverageMetric(BaseMetric): 13 | @property 14 | def name(self) -> str: 15 | return '2YMA' 16 | 17 | @property 18 | def description(self) -> str: 19 | return '2 Year Moving Average' 20 | 21 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 22 | df = df.merge( 23 | cs_fetch( 24 | path='getBtcMultiplier', 25 | data_selector='mA730List', 26 | col_name='2YMA', 27 | ), 28 | on='Date', 29 | how='left', 30 | ) 31 | df['2YMA'] = df['2YMA'].ffill() 32 | df['2YMALog'] = np.log(df['2YMA']) 33 | df['2YMALogDiff'] = df['PriceLog'] - df['2YMALog'] 34 | 35 | high_rows = df.loc[df['PriceHigh'] == 1] 36 | high_x = high_rows.index.values.reshape(-1, 1) 37 | high_y = high_rows['2YMALogDiff'].values.reshape(-1, 1) 38 | 39 | low_rows = df.loc[df['PriceLow'] == 1] 40 | low_x = low_rows.index.values.reshape(-1, 1) 41 | low_y = low_rows['2YMALogDiff'].values.reshape(-1, 1) 42 | 43 | x = df.index.values.reshape(-1, 1) 44 | 45 | lin_model = LinearRegression() 46 | lin_model.fit(high_x, high_y) 47 | df['2YMALogOvershootModel'] = lin_model.predict(x) 48 | 49 | lin_model.fit(low_x, low_y) 50 | df['2YMALogUndershootModel'] = lin_model.predict(x) 51 | 52 | df['2YMAHighModel'] = df['2YMALogOvershootModel'] + df['2YMALog'] 53 | df['2YMALowModel'] = df['2YMALogUndershootModel'] + df['2YMALog'] 54 | 55 | df['2YMAIndex'] = (df['PriceLog'] - df['2YMALowModel']) / (df['2YMAHighModel'] - df['2YMALowModel']) 56 | 57 | df['2YMAIndexNoNa'] = df['2YMAIndex'].fillna(0) 58 | ax[0].set_title(self.description) 59 | sns.lineplot(data=df, x='Date', y='2YMAIndexNoNa', ax=ax[0]) 60 | add_common_markers(df, ax[0]) 61 | 62 | sns.lineplot(data=df, x='Date', y='PriceLog', ax=ax[1]) 63 | sns.lineplot(data=df, x='Date', y='2YMAHighModel', ax=ax[1]) 64 | sns.lineplot(data=df, x='Date', y='2YMALowModel', ax=ax[1]) 65 | add_common_markers(df, ax[1], price_line=False) 66 | 67 | return df['2YMAIndex'] 68 | -------------------------------------------------------------------------------- /metrics/woobull_topcap_cvdd.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | from matplotlib.axes import Axes 5 | from sklearn.linear_model import LinearRegression 6 | 7 | from metrics.base_metric import BaseMetric 8 | from utils import HTTP, add_common_markers 9 | 10 | 11 | def _fetch_df() -> pd.DataFrame: 12 | response = HTTP.get('https://woocharts.com/bitcoin-price-models/data/chart.json') 13 | response.raise_for_status() 14 | data = response.json() 15 | 16 | df_top = pd.DataFrame( 17 | { 18 | 'Date': data['top_']['x'], 19 | 'Top': data['top_']['y'], 20 | } 21 | ) 22 | df_top['Date'] = pd.to_datetime(df_top['Date'], unit='ms').dt.tz_localize(None) 23 | 24 | df_cvdd = pd.DataFrame( 25 | { 26 | 'Date': data['cvdd']['x'], 27 | 'CVDD': data['cvdd']['y'], 28 | } 29 | ) 30 | df_cvdd['Date'] = pd.to_datetime(df_cvdd['Date'], unit='ms').dt.tz_localize(None) 31 | 32 | df = df_top.merge(df_cvdd, on='Date') 33 | 34 | return df 35 | 36 | 37 | class WoobullMetric(BaseMetric): 38 | @property 39 | def name(self) -> str: 40 | return 'Woobull' 41 | 42 | @property 43 | def description(self) -> str: 44 | return 'Woobull Top Cap vs CVDD' 45 | 46 | def _calculate(self, df: pd.DataFrame, ax: list[Axes]) -> pd.Series: 47 | df = df.merge(_fetch_df(), on='Date', how='left') 48 | df['Top'] = df['Top'].ffill() 49 | df['TopLog'] = np.log(df['Top']) 50 | df['CVDD'] = df['CVDD'].ffill() 51 | df['CVDDLog'] = np.log(df['CVDD']) 52 | 53 | df['Woobull'] = (df['PriceLog'] - df['CVDDLog']) / (df['TopLog'] - df['CVDDLog']) 54 | 55 | high_rows = df.loc[df['PriceHigh'] == 1] 56 | high_x = high_rows.index.values.reshape(-1, 1) 57 | high_y = high_rows['Woobull'].values.reshape(-1, 1) 58 | 59 | low_rows = df.loc[df['PriceLow'] == 1][1:] 60 | low_x = low_rows.index.values.reshape(-1, 1) 61 | low_y = low_rows['Woobull'].values.reshape(-1, 1) 62 | 63 | x = df.index.values.reshape(-1, 1) 64 | 65 | lin_model = LinearRegression() 66 | lin_model.fit(high_x, high_y) 67 | df['HighModel'] = lin_model.predict(x) 68 | df['HighModel'] = df['HighModel'] - 0.025 69 | 70 | lin_model.fit(low_x, low_y) 71 | df['LowModel'] = lin_model.predict(x) 72 | 73 | df['WoobullIndex'] = (df['Woobull'] - df['LowModel']) / (df['HighModel'] - df['LowModel']) 74 | 75 | ax[0].set_title(self.description) 76 | sns.lineplot(data=df, x='Date', y='WoobullIndex', ax=ax[0]) 77 | add_common_markers(df, ax[0]) 78 | 79 | sns.lineplot(data=df, x='Date', y='Woobull', ax=ax[1]) 80 | sns.lineplot(data=df, x='Date', y='HighModel', ax=ax[1]) 81 | sns.lineplot(data=df, x='Date', y='LowModel', ax=ax[1]) 82 | add_common_markers(df, ax[1], price_line=False) 83 | 84 | return df['WoobullIndex'] 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | dependencies = [ 3 | "filecache", 4 | "fire", 5 | "httpx[brotli,zstd]", 6 | "matplotlib", 7 | "numpy", 8 | "pandas", 9 | "pyfiglet", 10 | "python-telegram-bot", 11 | "scikit-learn", 12 | "seaborn", 13 | "sty", 14 | "tqdm", 15 | ] 16 | name = "cbbi" 17 | requires-python = "~=3.13" 18 | version = "0.0.0" 19 | 20 | [tool.uv] 21 | package = false 22 | python-downloads = "never" 23 | python-preference = "only-system" 24 | 25 | [tool.setuptools] 26 | packages = ["."] 27 | 28 | [tool.ruff] 29 | # Exclude a variety of commonly ignored directories. 30 | exclude = [ 31 | ".bzr", 32 | ".direnv", 33 | ".eggs", 34 | ".git", 35 | ".git-rewrite", 36 | ".hg", 37 | ".mypy_cache", 38 | ".nox", 39 | ".pants.d", 40 | ".pytype", 41 | ".ruff_cache", 42 | ".svn", 43 | ".tox", 44 | ".venv", 45 | "__pypackages__", 46 | "_build", 47 | "buck-out", 48 | "build", 49 | "dist", 50 | "node_modules", 51 | "venv", 52 | ] 53 | 54 | indent-width = 4 55 | line-length = 120 56 | target-version = "py313" 57 | 58 | [tool.ruff.lint] 59 | ignore = [ 60 | "S101", # assert 61 | ] 62 | # see https://docs.astral.sh/ruff/rules/ for rules documentation 63 | select = [ 64 | "A", # flake8-builtins 65 | "ARG", # flake8-unused-argumentsf 66 | "ASYNC", # flake8-async 67 | "B", # flake8-bugbear 68 | # "COM", # flake8-commas 69 | "C4", # flake8-comprehensions 70 | "E4", # pycodestyle 71 | "E7", 72 | "E9", 73 | "F", # pyflakes 74 | # "FBT", # flake8-boolean-trap 75 | "FLY", # flynt 76 | # "FURB", # refurb (preview) 77 | "G", # flake8-logging-format 78 | "I", # isort 79 | "INT", # flake8-gettext 80 | # "LOG", # flake8-logging (preview) 81 | "N", # pep8-naming 82 | "NPY", # numpy 83 | "PERF", # perflint 84 | "PGH", # pygrep-hooks 85 | "PIE", # flake8-pie 86 | "Q", # flake8-quotes 87 | "UP", # pyupgrade 88 | # "PL", # pylint 89 | "PT", # flake8-pytest-style 90 | "PTH", # flake8-use-pathlib 91 | "PYI", # flake8-pyi 92 | "RSE", # flake8-raise 93 | "RUF", # ruff 94 | "S", # flake8-bandit 95 | "SIM", # flake8-simplify 96 | "SLF", # flake8-self 97 | "SLOT", # flake8-slots 98 | "T10", # flake8-debugger 99 | # "T20", # flake8-print 100 | # "TRY", # tryceratops 101 | "YTT", # flake8-2020 102 | ] 103 | 104 | fixable = ["ALL"] 105 | unfixable = [] 106 | 107 | [tool.ruff.format] 108 | quote-style = "single" 109 | indent-style = "space" 110 | skip-magic-trailing-comma = false 111 | line-ending = "lf" 112 | 113 | [tool.ruff.lint.flake8-builtins] 114 | builtins-ignorelist = ["id", "open", "type"] 115 | 116 | [tool.ruff.lint.flake8-quotes] 117 | docstring-quotes = "double" 118 | inline-quotes = "single" 119 | multiline-quotes = "double" 120 | 121 | [tool.ruff.lint.pylint] 122 | max-args = 10 123 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | {}: 2 | 3 | let 4 | # Update with `nixpkgs-update` command 5 | pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/2bfc080955153be0be56724be6fa5477b4eefabb.tar.gz") { }; 6 | 7 | pythonLibs = with pkgs; [ 8 | stdenv.cc.cc.lib 9 | zlib.out 10 | ]; 11 | python' = with pkgs; (symlinkJoin { 12 | name = "python"; 13 | paths = [ python313 ]; 14 | buildInputs = [ makeWrapper ]; 15 | postBuild = '' 16 | wrapProgram "$out/bin/python3.13" --prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath pythonLibs}" 17 | ''; 18 | }); 19 | 20 | packages' = with pkgs; [ 21 | python' 22 | uv 23 | ruff 24 | 25 | (writeShellScriptBin "nixpkgs-update" '' 26 | set -e 27 | hash=$( 28 | curl --silent --location \ 29 | https://prometheus.nixos.org/api/v1/query \ 30 | -d "query=channel_revision{channel=\"nixpkgs-unstable\"}" | \ 31 | grep --only-matching --extended-regexp "[0-9a-f]{40}") 32 | sed -i -E "s|/nixpkgs/archive/[0-9a-f]{40}\.tar\.gz|/nixpkgs/archive/$hash.tar.gz|" shell.nix 33 | echo "Nixpkgs updated to $hash" 34 | '') 35 | (writeShellScriptBin "docker-build-push" '' 36 | set -e 37 | if command -v podman &> /dev/null; then docker() { podman "$@"; } fi 38 | docker push $(docker load < $(nix-build --no-out-link) | sed -En 's/Loaded image: (\S+)/\1/p') 39 | '') 40 | ]; 41 | 42 | shell' = '' 43 | export SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt 44 | export PYTHONNOUSERSITE=1 45 | export PYTHONPATH="" 46 | export TZ=UTC 47 | 48 | current_python=$(readlink -e .venv/bin/python || echo "") 49 | current_python=''${current_python%/bin/*} 50 | [ "$current_python" != "${python'}" ] && rm -rf .venv/ 51 | 52 | echo "Installing Python dependencies" 53 | export UV_PYTHON="${python'}/bin/python" 54 | uv sync --frozen 55 | 56 | echo "Activating Python virtual environment" 57 | source .venv/bin/activate 58 | 59 | if [ -f .env ]; then 60 | echo "Loading .env file" 61 | set -o allexport 62 | source .env set 63 | set +o allexport 64 | else 65 | echo "Skipped loading .env file (not found)" 66 | fi 67 | ''; 68 | in 69 | pkgs.mkShell { 70 | buildInputs = packages'; 71 | shellHook = shell'; 72 | } 73 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | from datetime import datetime 4 | from math import ceil 5 | 6 | import numpy as np 7 | import pandas as pd 8 | import seaborn as sns 9 | import telegram 10 | from httpx import Client 11 | from matplotlib.axes import Axes 12 | from sty import bg 13 | 14 | HTTP = Client( 15 | headers={'User-Agent': 'Mozilla/5.0 (Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0'}, 16 | timeout=30, 17 | follow_redirects=True, 18 | ) 19 | 20 | 21 | def mark_highs_lows( 22 | df: pd.DataFrame, 23 | col: str, 24 | begin_with_high: bool, 25 | window_size: float, 26 | ignore_last_rows: int, 27 | ) -> pd.DataFrame: 28 | """ 29 | Marks highs and lows (peaks) of the column values inside the given DataFrame. 30 | Marked points are indicated by the value '1' inside their corresponding, newly added, '``col``High' and '``col``Low' columns. 31 | 32 | Args: 33 | df: DataFrame from which the column values are selected and to which marked points columns are added. 34 | col: Column name of which values are selected inside the given DataFrame. 35 | begin_with_high: Indicates whether the first peak is high or low. 36 | window_size: Window size for the algorithm to consider. 37 | Too low value will mark too many peaks, whereas, too high value will mark too little peaks. 38 | ignore_last_rows: Amount of trailing DataFrame rows for which highs and lows should not be marked. 39 | 40 | Returns: 41 | Modified input DataFrame with columns, indicating the marked points, added. 42 | """ 43 | col_high = col + 'High' 44 | col_low = col + 'Low' 45 | 46 | assert col in df.columns, f'The column name "{col}" (col) could not be found inside the given DataFrame (df)' 47 | assert col_high not in df.columns, 'The DataFrame (df) already contains the "High" column - bug prone' 48 | assert col_low not in df.columns, 'The DataFrame (df) already contains the "Low" column - bug prone' 49 | assert window_size > 0, 'Value of the window_size argument must be at least 1' 50 | 51 | df[col_high] = 0 52 | df[col_low] = 0 53 | 54 | searching_high = begin_with_high 55 | current_index = df.index[0] 56 | 57 | while True: 58 | window = df.loc[current_index : current_index + window_size, col] 59 | 60 | if sum(~np.isnan(window)) == 0 and window.shape[0] > 1: 61 | current_index += window.shape[0] 62 | continue 63 | 64 | if window.shape[0] <= 1: 65 | break 66 | 67 | window_index = window.idxmax() if searching_high else window.idxmin() 68 | 69 | if window_index == current_index: 70 | df.loc[window_index, col_high if searching_high else col_low] = 1 71 | searching_high = not searching_high 72 | window_index = window_index + 1 73 | 74 | current_index = window_index 75 | 76 | df.loc[df.shape[0] - ignore_last_rows :, (col_high, col_low)] = 0 77 | 78 | # stabilize the algorithm until a next major update 79 | df.loc[df['Date'] >= '2023-07-01', (col_high, col_low)] = 0 80 | return df 81 | 82 | 83 | def mark_days_since(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame: 84 | """ 85 | This function takes a DataFrame and a list of column names 86 | and calculates the number of days since the last value of 1 for each column in the list. 87 | 88 | The resulting DataFrame will have new columns for each input column, with the column name prefixed by 'DaysSince'. 89 | The value in these new columns will be the number of days since the last value of 1 in the corresponding input column. 90 | 91 | Args: 92 | df: The input DataFrame. 93 | cols: The list of columns in the DataFrame to calculate the days since the last value of 1. 94 | 95 | Returns: 96 | The modified DataFrame with the new columns added. 97 | """ 98 | for col in cols: 99 | indexes = df.loc[df[col] == 1].index 100 | df[f'DaysSince{col}'] = df.index.to_series().apply( 101 | lambda v: min([v - index if index <= v else np.nan for index in indexes]) # noqa: B023 102 | ) 103 | 104 | return df 105 | 106 | 107 | def add_common_markers(df: pd.DataFrame, ax: Axes, price_line: bool = True) -> None: 108 | """ 109 | This function adds common markers to a plot. 110 | 111 | Args: 112 | df: The DataFrame containing the data to be plotted. 113 | ax: The Axes object to be plotted on. 114 | price_line: If True, a line plot of the 'PriceLogInterp' column will be added to the Axes. Default is True. 115 | 116 | Returns: 117 | None 118 | """ 119 | if price_line: 120 | sns.lineplot(data=df, x='Date', y='PriceLogInterp', alpha=0.4, color='orange', ax=ax) 121 | 122 | for _, row in df[df['Halving'] == 1].iterrows(): 123 | days_since_epoch = (row['Date'] - datetime(1970, 1, 1)).days 124 | ax.axvline(x=days_since_epoch, color='navy', linestyle=':') 125 | 126 | for _, row in df[df['PriceHigh'] == 1].iterrows(): 127 | days_since_epoch = (row['Date'] - datetime(1970, 1, 1)).days 128 | ax.axvline(x=days_since_epoch, color='green', linestyle=':') 129 | 130 | for _, row in df[df['PriceLow'] == 1].iterrows(): 131 | days_since_epoch = (row['Date'] - datetime(1970, 1, 1)).days 132 | ax.axvline(x=days_since_epoch, color='red', linestyle=':') 133 | 134 | 135 | def split_df_on_index_gap(df: pd.DataFrame, min_gap: int = 1): 136 | """ 137 | Split a Pandas DataFrame on gaps in the index values. 138 | 139 | Args: 140 | df: The DataFrame to split. 141 | min_gap: The minimum gap size in the index values to split on. 142 | 143 | Returns: 144 | A list of DataFrames split on the specified gaps in the index values. 145 | """ 146 | begin_idx = None 147 | end_idx = None 148 | 149 | for i, _ in df.iterrows(): 150 | if begin_idx is None: 151 | begin_idx = i 152 | end_idx = i 153 | elif (i - end_idx) <= min_gap: 154 | end_idx = i 155 | else: 156 | yield df.loc[begin_idx:end_idx] 157 | begin_idx = i 158 | end_idx = i 159 | 160 | if begin_idx is not None: 161 | yield df.loc[begin_idx:end_idx] 162 | 163 | 164 | def format_percentage(val: float, suffix: str = ' %') -> str: 165 | """ 166 | Formats a percentage value (0.0 - 1.0) in the standardized way. 167 | Returned value has a constant width and a trailing '%' sign. 168 | 169 | Args: 170 | val: Percentage value to be formatted. 171 | suffix: String to be appended to the result. 172 | 173 | Returns: 174 | Formatted percentage value with a constant width and trailing '%' sign. 175 | 176 | Examples: 177 | >>> print(format_percentage(0.359)) 178 | str(' 36 %') 179 | 180 | >>> print(format_percentage(1.1)) 181 | str('110 %') 182 | """ 183 | 184 | return f'{ceil(val * 100): >3d}{suffix}' 185 | 186 | 187 | def get_color(val: float) -> str: 188 | """ 189 | Maps a percentage value (0.0 - 1.0) to its corresponding color. 190 | The color is used to indicate whether the value is low (0.0) or high (1.0). 191 | Returned value is a valid sty-package color string. 192 | 193 | Args: 194 | val: Percentage value to be mapped into a color. 195 | 196 | Returns: 197 | Valid sty-package color string. 198 | """ 199 | 200 | config = [ 201 | bg.da_red, 202 | 0.3, 203 | bg.da_yellow, 204 | 0.65, 205 | bg.da_green, 206 | 0.85, 207 | bg.da_cyan, 208 | 0.97, 209 | bg.da_magenta, 210 | ] 211 | 212 | bin_index = np.digitize([round(val, 2)], config[1::2])[0] 213 | return config[::2][bin_index] 214 | 215 | 216 | async def send_error_notification(exception: Exception) -> bool: 217 | """ 218 | This function sends a notification to a Telegram chat with details of the provided exception. 219 | 220 | Args: 221 | exception: The exception to be reported. 222 | 223 | Returns: 224 | A boolean indicating whether the notification was sent successfully. 225 | """ 226 | telegram_token = os.getenv('TELEGRAM_TOKEN') 227 | telegram_chat_id = os.getenv('TELEGRAM_CHAT_ID') 228 | if not telegram_token or not telegram_chat_id: 229 | return False 230 | 231 | async with telegram.Bot(telegram_token) as bot: 232 | await bot.send_message( 233 | telegram_chat_id, 234 | f'🚨 An error has occurred: {exception!s}\n' 235 | f'\n' 236 | f'🔍️ Stack trace\n' 237 | f'
{"".join(traceback.format_exception(exception))}
', 238 | parse_mode='HTML', 239 | ) 240 | return True 241 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.13, <4" 4 | 5 | [[package]] 6 | name = "anyio" 7 | version = "4.9.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | dependencies = [ 10 | { name = "idna" }, 11 | { name = "sniffio" }, 12 | ] 13 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 14 | wheels = [ 15 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 16 | ] 17 | 18 | [[package]] 19 | name = "brotli" 20 | version = "1.1.0" 21 | source = { registry = "https://pypi.org/simple" } 22 | sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270 } 23 | wheels = [ 24 | { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681 }, 25 | { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475 }, 26 | { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173 }, 27 | { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803 }, 28 | { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946 }, 29 | { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707 }, 30 | { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231 }, 31 | { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157 }, 32 | { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122 }, 33 | { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206 }, 34 | { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804 }, 35 | { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517 }, 36 | ] 37 | 38 | [[package]] 39 | name = "brotlicffi" 40 | version = "1.1.0.0" 41 | source = { registry = "https://pypi.org/simple" } 42 | dependencies = [ 43 | { name = "cffi" }, 44 | ] 45 | sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192 } 46 | wheels = [ 47 | { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786 }, 48 | { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165 }, 49 | { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895 }, 50 | { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834 }, 51 | { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731 }, 52 | { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783 }, 53 | ] 54 | 55 | [[package]] 56 | name = "cbbi" 57 | version = "0.0.0" 58 | source = { virtual = "." } 59 | dependencies = [ 60 | { name = "filecache" }, 61 | { name = "fire" }, 62 | { name = "httpx", extra = ["brotli", "zstd"] }, 63 | { name = "matplotlib" }, 64 | { name = "numpy" }, 65 | { name = "pandas" }, 66 | { name = "pyfiglet" }, 67 | { name = "python-telegram-bot" }, 68 | { name = "scikit-learn" }, 69 | { name = "seaborn" }, 70 | { name = "sty" }, 71 | { name = "tqdm" }, 72 | ] 73 | 74 | [package.metadata] 75 | requires-dist = [ 76 | { name = "filecache" }, 77 | { name = "fire" }, 78 | { name = "httpx", extras = ["brotli", "zstd"] }, 79 | { name = "matplotlib" }, 80 | { name = "numpy" }, 81 | { name = "pandas" }, 82 | { name = "pyfiglet" }, 83 | { name = "python-telegram-bot" }, 84 | { name = "scikit-learn" }, 85 | { name = "seaborn" }, 86 | { name = "sty" }, 87 | { name = "tqdm" }, 88 | ] 89 | 90 | [[package]] 91 | name = "certifi" 92 | version = "2025.1.31" 93 | source = { registry = "https://pypi.org/simple" } 94 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 95 | wheels = [ 96 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 97 | ] 98 | 99 | [[package]] 100 | name = "cffi" 101 | version = "1.17.1" 102 | source = { registry = "https://pypi.org/simple" } 103 | dependencies = [ 104 | { name = "pycparser" }, 105 | ] 106 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } 107 | wheels = [ 108 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, 109 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, 110 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, 111 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, 112 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, 113 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, 114 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, 115 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, 116 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, 117 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, 118 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, 119 | ] 120 | 121 | [[package]] 122 | name = "colorama" 123 | version = "0.4.6" 124 | source = { registry = "https://pypi.org/simple" } 125 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 126 | wheels = [ 127 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 128 | ] 129 | 130 | [[package]] 131 | name = "contourpy" 132 | version = "1.3.1" 133 | source = { registry = "https://pypi.org/simple" } 134 | dependencies = [ 135 | { name = "numpy" }, 136 | ] 137 | sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } 138 | wheels = [ 139 | { url = "https://files.pythonhosted.org/packages/9a/e7/de62050dce687c5e96f946a93546910bc67e483fe05324439e329ff36105/contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2", size = 271548 }, 140 | { url = "https://files.pythonhosted.org/packages/78/4d/c2a09ae014ae984c6bdd29c11e74d3121b25eaa117eca0bb76340efd7e1c/contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5", size = 255576 }, 141 | { url = "https://files.pythonhosted.org/packages/ab/8a/915380ee96a5638bda80cd061ccb8e666bfdccea38d5741cb69e6dbd61fc/contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81", size = 306635 }, 142 | { url = "https://files.pythonhosted.org/packages/29/5c/c83ce09375428298acd4e6582aeb68b1e0d1447f877fa993d9bf6cd3b0a0/contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2", size = 345925 }, 143 | { url = "https://files.pythonhosted.org/packages/29/63/5b52f4a15e80c66c8078a641a3bfacd6e07106835682454647aca1afc852/contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7", size = 318000 }, 144 | { url = "https://files.pythonhosted.org/packages/9a/e2/30ca086c692691129849198659bf0556d72a757fe2769eb9620a27169296/contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c", size = 322689 }, 145 | { url = "https://files.pythonhosted.org/packages/6b/77/f37812ef700f1f185d348394debf33f22d531e714cf6a35d13d68a7003c7/contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3", size = 1268413 }, 146 | { url = "https://files.pythonhosted.org/packages/3f/6d/ce84e79cdd128542ebeb268f84abb4b093af78e7f8ec504676673d2675bc/contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1", size = 1326530 }, 147 | { url = "https://files.pythonhosted.org/packages/72/22/8282f4eae20c73c89bee7a82a19c4e27af9b57bb602ecaa00713d5bdb54d/contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82", size = 175315 }, 148 | { url = "https://files.pythonhosted.org/packages/e3/d5/28bca491f65312b438fbf076589dcde7f6f966b196d900777f5811b9c4e2/contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd", size = 220987 }, 149 | { url = "https://files.pythonhosted.org/packages/2f/24/a4b285d6adaaf9746e4700932f579f1a7b6f9681109f694cfa233ae75c4e/contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30", size = 285001 }, 150 | { url = "https://files.pythonhosted.org/packages/48/1d/fb49a401b5ca4f06ccf467cd6c4f1fd65767e63c21322b29b04ec40b40b9/contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751", size = 268553 }, 151 | { url = "https://files.pythonhosted.org/packages/79/1e/4aef9470d13fd029087388fae750dccb49a50c012a6c8d1d634295caa644/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342", size = 310386 }, 152 | { url = "https://files.pythonhosted.org/packages/b0/34/910dc706ed70153b60392b5305c708c9810d425bde12499c9184a1100888/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c", size = 349806 }, 153 | { url = "https://files.pythonhosted.org/packages/31/3c/faee6a40d66d7f2a87f7102236bf4780c57990dd7f98e5ff29881b1b1344/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f", size = 321108 }, 154 | { url = "https://files.pythonhosted.org/packages/17/69/390dc9b20dd4bb20585651d7316cc3054b7d4a7b4f8b710b2b698e08968d/contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda", size = 327291 }, 155 | { url = "https://files.pythonhosted.org/packages/ef/74/7030b67c4e941fe1e5424a3d988080e83568030ce0355f7c9fc556455b01/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242", size = 1263752 }, 156 | { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 }, 157 | { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 }, 158 | { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 }, 159 | ] 160 | 161 | [[package]] 162 | name = "cycler" 163 | version = "0.12.1" 164 | source = { registry = "https://pypi.org/simple" } 165 | sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615 } 166 | wheels = [ 167 | { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, 168 | ] 169 | 170 | [[package]] 171 | name = "filecache" 172 | version = "0.81" 173 | source = { registry = "https://pypi.org/simple" } 174 | sdist = { url = "https://files.pythonhosted.org/packages/b3/f5/647f13b1cae32f8d3b84866f6bac688b7923c5d7643b994e5e89865c9a2a/filecache-0.81.tar.gz", hash = "sha256:be071ad64937b51f38b03ecd82b9b68c08d0f570cdddb30aa8f90150fe54b30a", size = 6423 } 175 | wheels = [ 176 | { url = "https://files.pythonhosted.org/packages/eb/79/f96a2addff21798ea11aa51ae15052514e9ac0ab4ab9470ddd1a0da6fd3e/filecache-0.81-py3-none-any.whl", hash = "sha256:91ce1a42b532d0e9ad75364c13159bafc3015973d4a5a0dbf37e4b4feb194055", size = 4449 }, 177 | ] 178 | 179 | [[package]] 180 | name = "fire" 181 | version = "0.7.0" 182 | source = { registry = "https://pypi.org/simple" } 183 | dependencies = [ 184 | { name = "termcolor" }, 185 | ] 186 | sdist = { url = "https://files.pythonhosted.org/packages/6b/b6/82c7e601d6d3c3278c40b7bd35e17e82aa227f050aa9f66cb7b7fce29471/fire-0.7.0.tar.gz", hash = "sha256:961550f07936eaf65ad1dc8360f2b2bf8408fad46abbfa4d2a3794f8d2a95cdf", size = 87189 } 187 | 188 | [[package]] 189 | name = "fonttools" 190 | version = "4.57.0" 191 | source = { registry = "https://pypi.org/simple" } 192 | sdist = { url = "https://files.pythonhosted.org/packages/03/2d/a9a0b6e3a0cf6bd502e64fc16d894269011930cabfc89aee20d1635b1441/fonttools-4.57.0.tar.gz", hash = "sha256:727ece10e065be2f9dd239d15dd5d60a66e17eac11aea47d447f9f03fdbc42de", size = 3492448 } 193 | wheels = [ 194 | { url = "https://files.pythonhosted.org/packages/e9/2f/11439f3af51e4bb75ac9598c29f8601aa501902dcedf034bdc41f47dd799/fonttools-4.57.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:408ce299696012d503b714778d89aa476f032414ae57e57b42e4b92363e0b8ef", size = 2739175 }, 195 | { url = "https://files.pythonhosted.org/packages/25/52/677b55a4c0972dc3820c8dba20a29c358197a78229daa2ea219fdb19e5d5/fonttools-4.57.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bbceffc80aa02d9e8b99f2a7491ed8c4a783b2fc4020119dc405ca14fb5c758c", size = 2276583 }, 196 | { url = "https://files.pythonhosted.org/packages/64/79/184555f8fa77b827b9460a4acdbbc0b5952bb6915332b84c615c3a236826/fonttools-4.57.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f022601f3ee9e1f6658ed6d184ce27fa5216cee5b82d279e0f0bde5deebece72", size = 4766437 }, 197 | { url = "https://files.pythonhosted.org/packages/f8/ad/c25116352f456c0d1287545a7aa24e98987b6d99c5b0456c4bd14321f20f/fonttools-4.57.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dea5893b58d4637ffa925536462ba626f8a1b9ffbe2f5c272cdf2c6ebadb817", size = 4838431 }, 198 | { url = "https://files.pythonhosted.org/packages/53/ae/398b2a833897297797a44f519c9af911c2136eb7aa27d3f1352c6d1129fa/fonttools-4.57.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dff02c5c8423a657c550b48231d0a48d7e2b2e131088e55983cfe74ccc2c7cc9", size = 4951011 }, 199 | { url = "https://files.pythonhosted.org/packages/b7/5d/7cb31c4bc9ffb9a2bbe8b08f8f53bad94aeb158efad75da645b40b62cb73/fonttools-4.57.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:767604f244dc17c68d3e2dbf98e038d11a18abc078f2d0f84b6c24571d9c0b13", size = 5205679 }, 200 | { url = "https://files.pythonhosted.org/packages/4c/e4/6934513ec2c4d3d69ca1bc3bd34d5c69dafcbf68c15388dd3bb062daf345/fonttools-4.57.0-cp313-cp313-win32.whl", hash = "sha256:8e2e12d0d862f43d51e5afb8b9751c77e6bec7d2dc00aad80641364e9df5b199", size = 2144833 }, 201 | { url = "https://files.pythonhosted.org/packages/c4/0d/2177b7fdd23d017bcfb702fd41e47d4573766b9114da2fddbac20dcc4957/fonttools-4.57.0-cp313-cp313-win_amd64.whl", hash = "sha256:f1d6bc9c23356908db712d282acb3eebd4ae5ec6d8b696aa40342b1d84f8e9e3", size = 2190799 }, 202 | { url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605 }, 203 | ] 204 | 205 | [[package]] 206 | name = "h11" 207 | version = "0.14.0" 208 | source = { registry = "https://pypi.org/simple" } 209 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 210 | wheels = [ 211 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 212 | ] 213 | 214 | [[package]] 215 | name = "httpcore" 216 | version = "1.0.7" 217 | source = { registry = "https://pypi.org/simple" } 218 | dependencies = [ 219 | { name = "certifi" }, 220 | { name = "h11" }, 221 | ] 222 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 223 | wheels = [ 224 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 225 | ] 226 | 227 | [[package]] 228 | name = "httpx" 229 | version = "0.28.1" 230 | source = { registry = "https://pypi.org/simple" } 231 | dependencies = [ 232 | { name = "anyio" }, 233 | { name = "certifi" }, 234 | { name = "httpcore" }, 235 | { name = "idna" }, 236 | ] 237 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 238 | wheels = [ 239 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 240 | ] 241 | 242 | [package.optional-dependencies] 243 | brotli = [ 244 | { name = "brotli", marker = "platform_python_implementation == 'CPython'" }, 245 | { name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" }, 246 | ] 247 | zstd = [ 248 | { name = "zstandard" }, 249 | ] 250 | 251 | [[package]] 252 | name = "idna" 253 | version = "3.10" 254 | source = { registry = "https://pypi.org/simple" } 255 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 256 | wheels = [ 257 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 258 | ] 259 | 260 | [[package]] 261 | name = "joblib" 262 | version = "1.4.2" 263 | source = { registry = "https://pypi.org/simple" } 264 | sdist = { url = "https://files.pythonhosted.org/packages/64/33/60135848598c076ce4b231e1b1895170f45fbcaeaa2c9d5e38b04db70c35/joblib-1.4.2.tar.gz", hash = "sha256:2382c5816b2636fbd20a09e0f4e9dad4736765fdfb7dca582943b9c1366b3f0e", size = 2116621 } 265 | wheels = [ 266 | { url = "https://files.pythonhosted.org/packages/91/29/df4b9b42f2be0b623cbd5e2140cafcaa2bef0759a00b7b70104dcfe2fb51/joblib-1.4.2-py3-none-any.whl", hash = "sha256:06d478d5674cbc267e7496a410ee875abd68e4340feff4490bcb7afb88060ae6", size = 301817 }, 267 | ] 268 | 269 | [[package]] 270 | name = "kiwisolver" 271 | version = "1.4.8" 272 | source = { registry = "https://pypi.org/simple" } 273 | sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } 274 | wheels = [ 275 | { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156 }, 276 | { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555 }, 277 | { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071 }, 278 | { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053 }, 279 | { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278 }, 280 | { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139 }, 281 | { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517 }, 282 | { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952 }, 283 | { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132 }, 284 | { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997 }, 285 | { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060 }, 286 | { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471 }, 287 | { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793 }, 288 | { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855 }, 289 | { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430 }, 290 | { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294 }, 291 | { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736 }, 292 | { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194 }, 293 | { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942 }, 294 | { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341 }, 295 | { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455 }, 296 | { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138 }, 297 | { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857 }, 298 | { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129 }, 299 | { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538 }, 300 | { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, 301 | { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, 302 | { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, 303 | ] 304 | 305 | [[package]] 306 | name = "matplotlib" 307 | version = "3.10.1" 308 | source = { registry = "https://pypi.org/simple" } 309 | dependencies = [ 310 | { name = "contourpy" }, 311 | { name = "cycler" }, 312 | { name = "fonttools" }, 313 | { name = "kiwisolver" }, 314 | { name = "numpy" }, 315 | { name = "packaging" }, 316 | { name = "pillow" }, 317 | { name = "pyparsing" }, 318 | { name = "python-dateutil" }, 319 | ] 320 | sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 } 321 | wheels = [ 322 | { url = "https://files.pythonhosted.org/packages/60/73/6770ff5e5523d00f3bc584acb6031e29ee5c8adc2336b16cd1d003675fe0/matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b", size = 8176112 }, 323 | { url = "https://files.pythonhosted.org/packages/08/97/b0ca5da0ed54a3f6599c3ab568bdda65269bc27c21a2c97868c1625e4554/matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1", size = 8046931 }, 324 | { url = "https://files.pythonhosted.org/packages/df/9a/1acbdc3b165d4ce2dcd2b1a6d4ffb46a7220ceee960c922c3d50d8514067/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3", size = 8453422 }, 325 | { url = "https://files.pythonhosted.org/packages/51/d0/2bc4368abf766203e548dc7ab57cf7e9c621f1a3c72b516cc7715347b179/matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6", size = 8596819 }, 326 | { url = "https://files.pythonhosted.org/packages/ab/1b/8b350f8a1746c37ab69dda7d7528d1fc696efb06db6ade9727b7887be16d/matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b", size = 9402782 }, 327 | { url = "https://files.pythonhosted.org/packages/89/06/f570373d24d93503988ba8d04f213a372fa1ce48381c5eb15da985728498/matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473", size = 8063812 }, 328 | { url = "https://files.pythonhosted.org/packages/fc/e0/8c811a925b5a7ad75135f0e5af46408b78af88bbb02a1df775100ef9bfef/matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01", size = 8214021 }, 329 | { url = "https://files.pythonhosted.org/packages/4a/34/319ec2139f68ba26da9d00fce2ff9f27679fb799a6c8e7358539801fd629/matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb", size = 8090782 }, 330 | { url = "https://files.pythonhosted.org/packages/77/ea/9812124ab9a99df5b2eec1110e9b2edc0b8f77039abf4c56e0a376e84a29/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972", size = 8478901 }, 331 | { url = "https://files.pythonhosted.org/packages/c9/db/b05bf463689134789b06dea85828f8ebe506fa1e37593f723b65b86c9582/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3", size = 8613864 }, 332 | { url = "https://files.pythonhosted.org/packages/c2/04/41ccec4409f3023a7576df3b5c025f1a8c8b81fbfe922ecfd837ac36e081/matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f", size = 9409487 }, 333 | { url = "https://files.pythonhosted.org/packages/ac/c2/0d5aae823bdcc42cc99327ecdd4d28585e15ccd5218c453b7bcd827f3421/matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9", size = 8134832 }, 334 | ] 335 | 336 | [[package]] 337 | name = "numpy" 338 | version = "2.2.4" 339 | source = { registry = "https://pypi.org/simple" } 340 | sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 } 341 | wheels = [ 342 | { url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 }, 343 | { url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 }, 344 | { url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 }, 345 | { url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 }, 346 | { url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 }, 347 | { url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 }, 348 | { url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 }, 349 | { url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 }, 350 | { url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 }, 351 | { url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 }, 352 | { url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 }, 353 | { url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 }, 354 | { url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 }, 355 | { url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 }, 356 | { url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 }, 357 | { url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 }, 358 | { url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 }, 359 | { url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 }, 360 | { url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 }, 361 | { url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 }, 362 | ] 363 | 364 | [[package]] 365 | name = "packaging" 366 | version = "24.2" 367 | source = { registry = "https://pypi.org/simple" } 368 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 369 | wheels = [ 370 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 371 | ] 372 | 373 | [[package]] 374 | name = "pandas" 375 | version = "2.2.3" 376 | source = { registry = "https://pypi.org/simple" } 377 | dependencies = [ 378 | { name = "numpy" }, 379 | { name = "python-dateutil" }, 380 | { name = "pytz" }, 381 | { name = "tzdata" }, 382 | ] 383 | sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 } 384 | wheels = [ 385 | { url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 }, 386 | { url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 }, 387 | { url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 }, 388 | { url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 }, 389 | { url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 }, 390 | { url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 }, 391 | { url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 }, 392 | { url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 }, 393 | { url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 }, 394 | { url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 }, 395 | { url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 }, 396 | { url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 }, 397 | { url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 }, 398 | ] 399 | 400 | [[package]] 401 | name = "pillow" 402 | version = "11.1.0" 403 | source = { registry = "https://pypi.org/simple" } 404 | sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } 405 | wheels = [ 406 | { url = "https://files.pythonhosted.org/packages/b3/31/9ca79cafdce364fd5c980cd3416c20ce1bebd235b470d262f9d24d810184/pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc", size = 3226640 }, 407 | { url = "https://files.pythonhosted.org/packages/ac/0f/ff07ad45a1f172a497aa393b13a9d81a32e1477ef0e869d030e3c1532521/pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0", size = 3101437 }, 408 | { url = "https://files.pythonhosted.org/packages/08/2f/9906fca87a68d29ec4530be1f893149e0cb64a86d1f9f70a7cfcdfe8ae44/pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1", size = 4326605 }, 409 | { url = "https://files.pythonhosted.org/packages/b0/0f/f3547ee15b145bc5c8b336401b2d4c9d9da67da9dcb572d7c0d4103d2c69/pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec", size = 4411173 }, 410 | { url = "https://files.pythonhosted.org/packages/b1/df/bf8176aa5db515c5de584c5e00df9bab0713548fd780c82a86cba2c2fedb/pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5", size = 4369145 }, 411 | { url = "https://files.pythonhosted.org/packages/de/7c/7433122d1cfadc740f577cb55526fdc39129a648ac65ce64db2eb7209277/pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114", size = 4496340 }, 412 | { url = "https://files.pythonhosted.org/packages/25/46/dd94b93ca6bd555588835f2504bd90c00d5438fe131cf01cfa0c5131a19d/pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352", size = 4296906 }, 413 | { url = "https://files.pythonhosted.org/packages/a8/28/2f9d32014dfc7753e586db9add35b8a41b7a3b46540e965cb6d6bc607bd2/pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3", size = 4431759 }, 414 | { url = "https://files.pythonhosted.org/packages/33/48/19c2cbe7403870fbe8b7737d19eb013f46299cdfe4501573367f6396c775/pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9", size = 2291657 }, 415 | { url = "https://files.pythonhosted.org/packages/3b/ad/285c556747d34c399f332ba7c1a595ba245796ef3e22eae190f5364bb62b/pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c", size = 2626304 }, 416 | { url = "https://files.pythonhosted.org/packages/e5/7b/ef35a71163bf36db06e9c8729608f78dedf032fc8313d19bd4be5c2588f3/pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65", size = 2375117 }, 417 | { url = "https://files.pythonhosted.org/packages/79/30/77f54228401e84d6791354888549b45824ab0ffde659bafa67956303a09f/pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861", size = 3230060 }, 418 | { url = "https://files.pythonhosted.org/packages/ce/b1/56723b74b07dd64c1010fee011951ea9c35a43d8020acd03111f14298225/pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081", size = 3106192 }, 419 | { url = "https://files.pythonhosted.org/packages/e1/cd/7bf7180e08f80a4dcc6b4c3a0aa9e0b0ae57168562726a05dc8aa8fa66b0/pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c", size = 4446805 }, 420 | { url = "https://files.pythonhosted.org/packages/97/42/87c856ea30c8ed97e8efbe672b58c8304dee0573f8c7cab62ae9e31db6ae/pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547", size = 4530623 }, 421 | { url = "https://files.pythonhosted.org/packages/ff/41/026879e90c84a88e33fb00cc6bd915ac2743c67e87a18f80270dfe3c2041/pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab", size = 4465191 }, 422 | { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, 423 | { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, 424 | { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, 425 | ] 426 | 427 | [[package]] 428 | name = "pycparser" 429 | version = "2.22" 430 | source = { registry = "https://pypi.org/simple" } 431 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } 432 | wheels = [ 433 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, 434 | ] 435 | 436 | [[package]] 437 | name = "pyfiglet" 438 | version = "1.0.2" 439 | source = { registry = "https://pypi.org/simple" } 440 | sdist = { url = "https://files.pythonhosted.org/packages/a0/f2/2649b2acace54f861eccd4ab163bfd914236fc93ddb1df02dad2a2552b14/pyfiglet-1.0.2.tar.gz", hash = "sha256:758788018ab8faaddc0984e1ea05ff330d3c64be663c513cc1f105f6a3066dab", size = 832345 } 441 | wheels = [ 442 | { url = "https://files.pythonhosted.org/packages/1a/03/bef6fff907e212d67a0003f8ea4819307bba91b2856074a0763dd483ccc4/pyfiglet-1.0.2-py3-none-any.whl", hash = "sha256:889b351d79c99e50a3f619c8f8e6ffdb27fd8c939fc43ecbd7559bd57d5f93ea", size = 1085824 }, 443 | ] 444 | 445 | [[package]] 446 | name = "pyparsing" 447 | version = "3.2.3" 448 | source = { registry = "https://pypi.org/simple" } 449 | sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608 } 450 | wheels = [ 451 | { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, 452 | ] 453 | 454 | [[package]] 455 | name = "python-dateutil" 456 | version = "2.9.0.post0" 457 | source = { registry = "https://pypi.org/simple" } 458 | dependencies = [ 459 | { name = "six" }, 460 | ] 461 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 462 | wheels = [ 463 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 464 | ] 465 | 466 | [[package]] 467 | name = "python-telegram-bot" 468 | version = "22.0" 469 | source = { registry = "https://pypi.org/simple" } 470 | dependencies = [ 471 | { name = "httpx" }, 472 | ] 473 | sdist = { url = "https://files.pythonhosted.org/packages/61/8c/0bd0d5c6de549ee0ebc2ddf4d49618eec1ece6d25084f3b4ef72bba6590c/python_telegram_bot-22.0.tar.gz", hash = "sha256:acf86f28d86d81cab736177d2988e5bcb27f2248137efd62e02c46e9ba1fe44c", size = 440017 } 474 | wheels = [ 475 | { url = "https://files.pythonhosted.org/packages/15/9f/b8c116f606074c19ec2600a7edc222f158c307ca949de568d67fe2b9d364/python_telegram_bot-22.0-py3-none-any.whl", hash = "sha256:23237f778655e634f08cfebbada96ed3692c2bdd3c20c122e90a6d606d6a4516", size = 673473 }, 476 | ] 477 | 478 | [[package]] 479 | name = "pytz" 480 | version = "2025.2" 481 | source = { registry = "https://pypi.org/simple" } 482 | sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } 483 | wheels = [ 484 | { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, 485 | ] 486 | 487 | [[package]] 488 | name = "scikit-learn" 489 | version = "1.6.1" 490 | source = { registry = "https://pypi.org/simple" } 491 | dependencies = [ 492 | { name = "joblib" }, 493 | { name = "numpy" }, 494 | { name = "scipy" }, 495 | { name = "threadpoolctl" }, 496 | ] 497 | sdist = { url = "https://files.pythonhosted.org/packages/9e/a5/4ae3b3a0755f7b35a280ac90b28817d1f380318973cff14075ab41ef50d9/scikit_learn-1.6.1.tar.gz", hash = "sha256:b4fc2525eca2c69a59260f583c56a7557c6ccdf8deafdba6e060f94c1c59738e", size = 7068312 } 498 | wheels = [ 499 | { url = "https://files.pythonhosted.org/packages/2e/59/8eb1872ca87009bdcdb7f3cdc679ad557b992c12f4b61f9250659e592c63/scikit_learn-1.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ffa1e9e25b3d93990e74a4be2c2fc61ee5af85811562f1288d5d055880c4322", size = 12010001 }, 500 | { url = "https://files.pythonhosted.org/packages/9d/05/f2fc4effc5b32e525408524c982c468c29d22f828834f0625c5ef3d601be/scikit_learn-1.6.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dc5cf3d68c5a20ad6d571584c0750ec641cc46aeef1c1507be51300e6003a7e1", size = 11096360 }, 501 | { url = "https://files.pythonhosted.org/packages/c8/e4/4195d52cf4f113573fb8ebc44ed5a81bd511a92c0228889125fac2f4c3d1/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06beb2e839ecc641366000ca84f3cf6fa9faa1777e29cf0c04be6e4d096a348", size = 12209004 }, 502 | { url = "https://files.pythonhosted.org/packages/94/be/47e16cdd1e7fcf97d95b3cb08bde1abb13e627861af427a3651fcb80b517/scikit_learn-1.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8ca8cb270fee8f1f76fa9bfd5c3507d60c6438bbee5687f81042e2bb98e5a97", size = 13171776 }, 503 | { url = "https://files.pythonhosted.org/packages/34/b0/ca92b90859070a1487827dbc672f998da95ce83edce1270fc23f96f1f61a/scikit_learn-1.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:7a1c43c8ec9fde528d664d947dc4c0789be4077a3647f232869f41d9bf50e0fb", size = 11071865 }, 504 | { url = "https://files.pythonhosted.org/packages/12/ae/993b0fb24a356e71e9a894e42b8a9eec528d4c70217353a1cd7a48bc25d4/scikit_learn-1.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a17c1dea1d56dcda2fac315712f3651a1fea86565b64b48fa1bc090249cbf236", size = 11955804 }, 505 | { url = "https://files.pythonhosted.org/packages/d6/54/32fa2ee591af44507eac86406fa6bba968d1eb22831494470d0a2e4a1eb1/scikit_learn-1.6.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a7aa5f9908f0f28f4edaa6963c0a6183f1911e63a69aa03782f0d924c830a35", size = 11100530 }, 506 | { url = "https://files.pythonhosted.org/packages/3f/58/55856da1adec655bdce77b502e94a267bf40a8c0b89f8622837f89503b5a/scikit_learn-1.6.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0650e730afb87402baa88afbf31c07b84c98272622aaba002559b614600ca691", size = 12433852 }, 507 | { url = "https://files.pythonhosted.org/packages/ff/4f/c83853af13901a574f8f13b645467285a48940f185b690936bb700a50863/scikit_learn-1.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:3f59fe08dc03ea158605170eb52b22a105f238a5d512c4470ddeca71feae8e5f", size = 11337256 }, 508 | ] 509 | 510 | [[package]] 511 | name = "scipy" 512 | version = "1.15.2" 513 | source = { registry = "https://pypi.org/simple" } 514 | dependencies = [ 515 | { name = "numpy" }, 516 | ] 517 | sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 } 518 | wheels = [ 519 | { url = "https://files.pythonhosted.org/packages/53/40/09319f6e0f276ea2754196185f95cd191cb852288440ce035d5c3a931ea2/scipy-1.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01edfac9f0798ad6b46d9c4c9ca0e0ad23dbf0b1eb70e96adb9fa7f525eff0bf", size = 38717587 }, 520 | { url = "https://files.pythonhosted.org/packages/fe/c3/2854f40ecd19585d65afaef601e5e1f8dbf6758b2f95b5ea93d38655a2c6/scipy-1.15.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:08b57a9336b8e79b305a143c3655cc5bdbe6d5ece3378578888d2afbb51c4e37", size = 30100266 }, 521 | { url = "https://files.pythonhosted.org/packages/dd/b1/f9fe6e3c828cb5930b5fe74cb479de5f3d66d682fa8adb77249acaf545b8/scipy-1.15.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:54c462098484e7466362a9f1672d20888f724911a74c22ae35b61f9c5919183d", size = 22373768 }, 522 | { url = "https://files.pythonhosted.org/packages/15/9d/a60db8c795700414c3f681908a2b911e031e024d93214f2d23c6dae174ab/scipy-1.15.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:cf72ff559a53a6a6d77bd8eefd12a17995ffa44ad86c77a5df96f533d4e6c6bb", size = 25154719 }, 523 | { url = "https://files.pythonhosted.org/packages/37/3b/9bda92a85cd93f19f9ed90ade84aa1e51657e29988317fabdd44544f1dd4/scipy-1.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9de9d1416b3d9e7df9923ab23cd2fe714244af10b763975bea9e4f2e81cebd27", size = 35163195 }, 524 | { url = "https://files.pythonhosted.org/packages/03/5a/fc34bf1aa14dc7c0e701691fa8685f3faec80e57d816615e3625f28feb43/scipy-1.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb530e4794fc8ea76a4a21ccb67dea33e5e0e60f07fc38a49e821e1eae3b71a0", size = 37255404 }, 525 | { url = "https://files.pythonhosted.org/packages/4a/71/472eac45440cee134c8a180dbe4c01b3ec247e0338b7c759e6cd71f199a7/scipy-1.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5ea7ed46d437fc52350b028b1d44e002646e28f3e8ddc714011aaf87330f2f32", size = 36860011 }, 526 | { url = "https://files.pythonhosted.org/packages/01/b3/21f890f4f42daf20e4d3aaa18182dddb9192771cd47445aaae2e318f6738/scipy-1.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11e7ad32cf184b74380f43d3c0a706f49358b904fa7d5345f16ddf993609184d", size = 39657406 }, 527 | { url = "https://files.pythonhosted.org/packages/0d/76/77cf2ac1f2a9cc00c073d49e1e16244e389dd88e2490c91d84e1e3e4d126/scipy-1.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:a5080a79dfb9b78b768cebf3c9dcbc7b665c5875793569f48bf0e2b1d7f68f6f", size = 40961243 }, 528 | { url = "https://files.pythonhosted.org/packages/4c/4b/a57f8ddcf48e129e6054fa9899a2a86d1fc6b07a0e15c7eebff7ca94533f/scipy-1.15.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:447ce30cee6a9d5d1379087c9e474628dab3db4a67484be1b7dc3196bfb2fac9", size = 38870286 }, 529 | { url = "https://files.pythonhosted.org/packages/0c/43/c304d69a56c91ad5f188c0714f6a97b9c1fed93128c691148621274a3a68/scipy-1.15.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c90ebe8aaa4397eaefa8455a8182b164a6cc1d59ad53f79943f266d99f68687f", size = 30141634 }, 530 | { url = "https://files.pythonhosted.org/packages/44/1a/6c21b45d2548eb73be9b9bff421aaaa7e85e22c1f9b3bc44b23485dfce0a/scipy-1.15.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:def751dd08243934c884a3221156d63e15234a3155cf25978b0a668409d45eb6", size = 22415179 }, 531 | { url = "https://files.pythonhosted.org/packages/74/4b/aefac4bba80ef815b64f55da06f62f92be5d03b467f2ce3668071799429a/scipy-1.15.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:302093e7dfb120e55515936cb55618ee0b895f8bcaf18ff81eca086c17bd80af", size = 25126412 }, 532 | { url = "https://files.pythonhosted.org/packages/b1/53/1cbb148e6e8f1660aacd9f0a9dfa2b05e9ff1cb54b4386fe868477972ac2/scipy-1.15.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd5b77413e1855351cdde594eca99c1f4a588c2d63711388b6a1f1c01f62274", size = 34952867 }, 533 | { url = "https://files.pythonhosted.org/packages/2c/23/e0eb7f31a9c13cf2dca083828b97992dd22f8184c6ce4fec5deec0c81fcf/scipy-1.15.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d0194c37037707b2afa7a2f2a924cf7bac3dc292d51b6a925e5fcb89bc5c776", size = 36890009 }, 534 | { url = "https://files.pythonhosted.org/packages/03/f3/e699e19cabe96bbac5189c04aaa970718f0105cff03d458dc5e2b6bd1e8c/scipy-1.15.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bae43364d600fdc3ac327db99659dcb79e6e7ecd279a75fe1266669d9a652828", size = 36545159 }, 535 | { url = "https://files.pythonhosted.org/packages/af/f5/ab3838e56fe5cc22383d6fcf2336e48c8fe33e944b9037fbf6cbdf5a11f8/scipy-1.15.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f031846580d9acccd0044efd1a90e6f4df3a6e12b4b6bd694a7bc03a89892b28", size = 39136566 }, 536 | { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 }, 537 | ] 538 | 539 | [[package]] 540 | name = "seaborn" 541 | version = "0.13.2" 542 | source = { registry = "https://pypi.org/simple" } 543 | dependencies = [ 544 | { name = "matplotlib" }, 545 | { name = "numpy" }, 546 | { name = "pandas" }, 547 | ] 548 | sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696 } 549 | wheels = [ 550 | { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914 }, 551 | ] 552 | 553 | [[package]] 554 | name = "six" 555 | version = "1.17.0" 556 | source = { registry = "https://pypi.org/simple" } 557 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 558 | wheels = [ 559 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, 560 | ] 561 | 562 | [[package]] 563 | name = "sniffio" 564 | version = "1.3.1" 565 | source = { registry = "https://pypi.org/simple" } 566 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 567 | wheels = [ 568 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 569 | ] 570 | 571 | [[package]] 572 | name = "sty" 573 | version = "1.0.6" 574 | source = { registry = "https://pypi.org/simple" } 575 | sdist = { url = "https://files.pythonhosted.org/packages/76/6a/aad1817e60f07e5ebc111affee15d4dff9d324981005310566c40f08786c/sty-1.0.6.tar.gz", hash = "sha256:d43ecb71b7bad0b56d622cb219d0be303c16fcb4143b84d1465ded22e29baa00", size = 12217 } 576 | wheels = [ 577 | { url = "https://files.pythonhosted.org/packages/ec/89/22b3b7f25f67d04690e3b565ca89062a6e7afb1e7124342d5b5b22e8f014/sty-1.0.6-py3-none-any.whl", hash = "sha256:2b1eba187b3961644f797f97177f939c109c916d3d3a2cb6784454d1f1ce4983", size = 12553 }, 578 | ] 579 | 580 | [[package]] 581 | name = "termcolor" 582 | version = "3.0.1" 583 | source = { registry = "https://pypi.org/simple" } 584 | sdist = { url = "https://files.pythonhosted.org/packages/f8/b6/8e2aaa8aeb570b5cc955cd913b083d96c5447bbe27eaf330dfd7cc8e3329/termcolor-3.0.1.tar.gz", hash = "sha256:a6abd5c6e1284cea2934443ba806e70e5ec8fd2449021be55c280f8a3731b611", size = 12935 } 585 | wheels = [ 586 | { url = "https://files.pythonhosted.org/packages/a6/7e/a574ccd49ad07e8b117407bac361f1e096b01f1b620365daf60ff702c936/termcolor-3.0.1-py3-none-any.whl", hash = "sha256:da1ed4ec8a5dc5b2e17476d859febdb3cccb612be1c36e64511a6f2485c10c69", size = 7157 }, 587 | ] 588 | 589 | [[package]] 590 | name = "threadpoolctl" 591 | version = "3.6.0" 592 | source = { registry = "https://pypi.org/simple" } 593 | sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274 } 594 | wheels = [ 595 | { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638 }, 596 | ] 597 | 598 | [[package]] 599 | name = "tqdm" 600 | version = "4.67.1" 601 | source = { registry = "https://pypi.org/simple" } 602 | dependencies = [ 603 | { name = "colorama", marker = "sys_platform == 'win32'" }, 604 | ] 605 | sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } 606 | wheels = [ 607 | { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, 608 | ] 609 | 610 | [[package]] 611 | name = "tzdata" 612 | version = "2025.2" 613 | source = { registry = "https://pypi.org/simple" } 614 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } 615 | wheels = [ 616 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, 617 | ] 618 | 619 | [[package]] 620 | name = "zstandard" 621 | version = "0.23.0" 622 | source = { registry = "https://pypi.org/simple" } 623 | dependencies = [ 624 | { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, 625 | ] 626 | sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701 } 627 | wheels = [ 628 | { url = "https://files.pythonhosted.org/packages/80/f1/8386f3f7c10261fe85fbc2c012fdb3d4db793b921c9abcc995d8da1b7a80/zstandard-0.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:576856e8594e6649aee06ddbfc738fec6a834f7c85bf7cadd1c53d4a58186ef9", size = 788975 }, 629 | { url = "https://files.pythonhosted.org/packages/16/e8/cbf01077550b3e5dc86089035ff8f6fbbb312bc0983757c2d1117ebba242/zstandard-0.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:38302b78a850ff82656beaddeb0bb989a0322a8bbb1bf1ab10c17506681d772a", size = 633448 }, 630 | { url = "https://files.pythonhosted.org/packages/06/27/4a1b4c267c29a464a161aeb2589aff212b4db653a1d96bffe3598f3f0d22/zstandard-0.23.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2240ddc86b74966c34554c49d00eaafa8200a18d3a5b6ffbf7da63b11d74ee2", size = 4945269 }, 631 | { url = "https://files.pythonhosted.org/packages/7c/64/d99261cc57afd9ae65b707e38045ed8269fbdae73544fd2e4a4d50d0ed83/zstandard-0.23.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ef230a8fd217a2015bc91b74f6b3b7d6522ba48be29ad4ea0ca3a3775bf7dd5", size = 5306228 }, 632 | { url = "https://files.pythonhosted.org/packages/7a/cf/27b74c6f22541f0263016a0fd6369b1b7818941de639215c84e4e94b2a1c/zstandard-0.23.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:774d45b1fac1461f48698a9d4b5fa19a69d47ece02fa469825b442263f04021f", size = 5336891 }, 633 | { url = "https://files.pythonhosted.org/packages/fa/18/89ac62eac46b69948bf35fcd90d37103f38722968e2981f752d69081ec4d/zstandard-0.23.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f77fa49079891a4aab203d0b1744acc85577ed16d767b52fc089d83faf8d8ed", size = 5436310 }, 634 | { url = "https://files.pythonhosted.org/packages/a8/a8/5ca5328ee568a873f5118d5b5f70d1f36c6387716efe2e369010289a5738/zstandard-0.23.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ac184f87ff521f4840e6ea0b10c0ec90c6b1dcd0bad2f1e4a9a1b4fa177982ea", size = 4859912 }, 635 | { url = "https://files.pythonhosted.org/packages/ea/ca/3781059c95fd0868658b1cf0440edd832b942f84ae60685d0cfdb808bca1/zstandard-0.23.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c363b53e257246a954ebc7c488304b5592b9c53fbe74d03bc1c64dda153fb847", size = 4936946 }, 636 | { url = "https://files.pythonhosted.org/packages/ce/11/41a58986f809532742c2b832c53b74ba0e0a5dae7e8ab4642bf5876f35de/zstandard-0.23.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e7792606d606c8df5277c32ccb58f29b9b8603bf83b48639b7aedf6df4fe8171", size = 5466994 }, 637 | { url = "https://files.pythonhosted.org/packages/83/e3/97d84fe95edd38d7053af05159465d298c8b20cebe9ccb3d26783faa9094/zstandard-0.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a0817825b900fcd43ac5d05b8b3079937073d2b1ff9cf89427590718b70dd840", size = 4848681 }, 638 | { url = "https://files.pythonhosted.org/packages/6e/99/cb1e63e931de15c88af26085e3f2d9af9ce53ccafac73b6e48418fd5a6e6/zstandard-0.23.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9da6bc32faac9a293ddfdcb9108d4b20416219461e4ec64dfea8383cac186690", size = 4694239 }, 639 | { url = "https://files.pythonhosted.org/packages/ab/50/b1e703016eebbc6501fc92f34db7b1c68e54e567ef39e6e59cf5fb6f2ec0/zstandard-0.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fd7699e8fd9969f455ef2926221e0233f81a2542921471382e77a9e2f2b57f4b", size = 5200149 }, 640 | { url = "https://files.pythonhosted.org/packages/aa/e0/932388630aaba70197c78bdb10cce2c91fae01a7e553b76ce85471aec690/zstandard-0.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d477ed829077cd945b01fc3115edd132c47e6540ddcd96ca169facff28173057", size = 5655392 }, 641 | { url = "https://files.pythonhosted.org/packages/02/90/2633473864f67a15526324b007a9f96c96f56d5f32ef2a56cc12f9548723/zstandard-0.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ce8b52c5987b3e34d5674b0ab529a4602b632ebab0a93b07bfb4dfc8f8a33", size = 5191299 }, 642 | { url = "https://files.pythonhosted.org/packages/b0/4c/315ca5c32da7e2dc3455f3b2caee5c8c2246074a61aac6ec3378a97b7136/zstandard-0.23.0-cp313-cp313-win32.whl", hash = "sha256:a9b07268d0c3ca5c170a385a0ab9fb7fdd9f5fd866be004c4ea39e44edce47dd", size = 430862 }, 643 | { url = "https://files.pythonhosted.org/packages/a2/bf/c6aaba098e2d04781e8f4f7c0ba3c7aa73d00e4c436bcc0cf059a66691d1/zstandard-0.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:f3513916e8c645d0610815c257cbfd3242adfd5c4cfa78be514e5a3ebb42a41b", size = 495578 }, 644 | ] 645 | --------------------------------------------------------------------------------