├── .gitignore ├── LICENSE ├── README.md ├── data ├── input │ └── .gitkeep └── output │ └── .gitkeep ├── excel_to_markdown ├── __init__.py ├── detector.py ├── main copy.py ├── main.py ├── markdown_generator.py ├── parser.py └── utils.py ├── poetry.lock ├── pyproject.toml ├── run_streamlit.py ├── src ├── app.py ├── components │ ├── inputs_files_selector.py │ └── sheet_selector.py ├── pages │ ├── column_to_doc.py │ ├── edit.py │ ├── preview.py │ ├── row_to_doc.py │ └── select_range_to_markdown.py └── utils.py └── tests ├── test_detector.py ├── test_main.py ├── test_markdown_generator.py └── test_parser.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Virtual environments 7 | .env 8 | .venv 9 | env/ 10 | venv/ 11 | ENV/ 12 | 13 | # Poetry 14 | .poetry/ 15 | poetry.lock 16 | 17 | # IDEs and editors 18 | .idea/ 19 | .vscode/ 20 | *.swp 21 | *.swo 22 | 23 | # Operating System Files 24 | .DS_Store 25 | Thumbs.db 26 | 27 | # Project specific 28 | /data/input/* 29 | /data/output/* 30 | !/data/input/.gitkeep 31 | !/data/output/.gitkeep 32 | 33 | # Jupyter Notebooks 34 | .ipynb_checkpoints 35 | 36 | # Large model files 37 | *.bin 38 | *.gguf 39 | *.ggml 40 | 41 | # Logs 42 | *.log 43 | 44 | # Distribution / packaging 45 | .Python 46 | build/ 47 | develop-eggs/ 48 | dist/ 49 | downloads/ 50 | eggs/ 51 | .eggs/ 52 | lib/ 53 | lib64/ 54 | parts/ 55 | sdist/ 56 | var/ 57 | wheels/ 58 | *.egg-info/ 59 | .installed.cfg 60 | *.egg 61 | 62 | # Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage.xml 70 | *.cover 71 | .hypothesis/ 72 | .pytest_cache/ 73 | 74 | # Excel files 75 | *.xlsx 76 | *.xls -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 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 General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EXCEL-TO-MARKDOWN 2 | 3 | ![License](https://img.shields.io/badge/license-GPLv3-blue) 4 | ![Python](https://img.shields.io/badge/python-3.1%2B-blue.svg) 5 | 6 | **EXCEL-TO-MARKDOWN** is a robust Python tool designed to convert Excel files (`.xlsx` and `.xls`) into well-formatted Markdown tables. Leveraging a modular architecture, this tool offers enhanced table detection capabilities, interactive prompts for handling complex Excel layouts, and seamless integration with various project workflows. 7 | 8 | ## 🛠️ Features 9 | 10 | - **Automated Table Detection:** Identifies the first fully populated row as the table header, ensuring accurate Markdown conversion. 11 | - **Interactive Mode:** Prompts users to specify table regions when automatic detection fails, handling complex and irregular Excel structures. 12 | - **Modular Design:** Organized into distinct modules for detection, parsing, Markdown generation, and utilities, promoting maintainability and scalability. 13 | - **Supports Multiple Sheets:** Processes all sheets within an Excel file, generating separate Markdown files for each. 14 | - **Flexible Column Specification:** Allows users to define column ranges using both letter-based (e.g., `A:D`) and number-based (e.g., `1-4`) inputs. 15 | - **Unit Tested:** Comprehensive unit tests ensure reliability and facilitate future enhancements. 16 | - **Easy Integration:** Compatible with Poetry for dependency management and can be integrated into larger projects or CI/CD pipelines. 17 | 18 | ## 📁 Project Structure 19 | 20 | ``` 21 | EXCEL-TO-MARKDOWN 22 | │ 23 | ├── .venv 24 | ├── data 25 | │ ├── input 26 | │ └── output 27 | ├── docs 28 | ├── excel_to_markdown 29 | │ ├── __init__.py 30 | │ ├── main.py 31 | │ ├── detector.py 32 | │ ├── parser.py 33 | │ ├── markdown_generator.py 34 | │ └── utils.py 35 | ├── src 36 | ├── tests 37 | │ ├── test_detector.py 38 | │ ├── test_parser.py 39 | │ ├── test_markdown_generator.py 40 | │ └── test_main.py 41 | ├── .gitignore 42 | ├── LICENSE 43 | ├── poetry.lock 44 | ├── pyproject.toml 45 | └── readme.md 46 | ``` 47 | 48 | ### **Module Breakdown** 49 | 50 | - **`excel_to_markdown/`** 51 | - **`main.py`**: Entry point of the application. Handles argument parsing, orchestrates the workflow, and manages file I/O. 52 | - **`detector.py`**: Contains functions related to detecting the table start within Excel sheets. 53 | - **`parser.py`**: Handles parsing user inputs, such as column specifications. 54 | - **`markdown_generator.py`**: Responsible for converting pandas DataFrames to Markdown format. 55 | - **`utils.py`**: Utility functions like column letter to index conversion and filename sanitization. 56 | 57 | - **`tests/`** 58 | - **`test_detector.py`** 59 | - **`test_parser.py`** 60 | - **`test_markdown_generator.py`** 61 | - **`test_main.py`** 62 | 63 | *Each test file contains unit tests for their respective modules, ensuring functionality and reliability.* 64 | 65 | ## 🚀 Installation 66 | 67 | ### **Prerequisites** 68 | 69 | - **Python 3.7+**: Ensure you have Python installed. You can download it from [python.org](https://www.python.org/downloads/). 70 | - **Poetry**: Python dependency management tool. Install it using the following command: 71 | 72 | ```bash 73 | curl -sSL https://install.python-poetry.org | python3 - 74 | ``` 75 | 76 | ### **Clone the Repository** 77 | 78 | ```bash 79 | git clone https://github.com/yourusername/EXCEL-TO-MARKDOWN.git 80 | cd EXCEL-TO-MARKDOWN 81 | ``` 82 | 83 | ### **Set Up the Virtual Environment** 84 | 85 | Poetry manages virtual environments automatically. To install dependencies: 86 | 87 | ```bash 88 | poetry install 89 | ``` 90 | 91 | To activate the virtual environment: 92 | 93 | ```bash 94 | poetry shell 95 | ``` 96 | 97 | ## 📋 Usage 98 | 99 | ### **Preparing Your Data** 100 | 101 | 1. **Input Directory:** Place all your Excel files (`.xlsx` or `.xls`) in the `data/input` directory. 102 | 103 | 2. **Output Directory:** The converted Markdown files will be saved in the `data/output` directory by default. If this directory doesn't exist, the script will create it. 104 | 105 | - **`data/input`**: Directory containing your Excel files. 106 | - **`data/output`**: (Optional) Directory where Markdown files will be saved. If not specified, an `output` folder will be created inside the input directory. 107 | 108 | 109 | ### **Running the Localhost Server** 110 | 111 | You can also start a localhost server for real-time editing using: 112 | 113 | ```bash 114 | poetry run app 115 | ``` 116 | 117 | This will start a server on your localhost, allowing you to make edits to your spreadsheets locally and see immediate updates. 118 | 119 | ### **Running the CLI Script** 120 | 121 | Execute the main script over CLI using the following command: 122 | 123 | ```bash 124 | python -m excel_to_markdown.main data/input data/output 125 | ``` 126 | 127 | **Example:** 128 | 129 | ```bash 130 | python -m excel_to_markdown.main data/input data/output 131 | ``` 132 | 133 | ### **Interactive Prompts** 134 | 135 | For each sheet in each Excel file: 136 | 137 | 1. **Automatic Detection:** 138 | - The script attempts to detect the header row based on the enhanced logic (first fully populated row). 139 | - If successful, it proceeds to convert without prompts. 140 | 141 | 2. **Manual Specification:** 142 | - If automatic detection fails, you'll be prompted to enter: 143 | - **Header Row Number:** The row where your table headers are located (1-based index). 144 | - **Columns to Include:** Specify the range of columns, e.g., `A:D` or `1-4`. 145 | 146 | **Sample Interaction:** 147 | 148 | ``` 149 | Processing sheet: 'Sales Data' in file 'report1.xlsx' 150 | Automatically detected table starting at row 2. 151 | Markdown file 'report1_Sales_Data.md' for sheet 'Sales Data' has been created successfully. 152 | 153 | Processing sheet: 'Summary' in file 'report1.xlsx' 154 | Automatic table detection failed. 155 | Enter the header row number (1-based index): 5 156 | Enter the columns to include (e.g., A:D or 1-4): B:E 157 | Markdown file 'report1_Summary.md' for sheet 'Summary' has been created successfully. 158 | ``` 159 | 160 | ## 🧩 Contributing 161 | 162 | Contributions are welcome! To contribute: 163 | 164 | 1. **Fork the Repository** 165 | 166 | 2. **Create a Feature Branch** 167 | 168 | ```bash 169 | git checkout -b feature/YourFeatureName 170 | ``` 171 | 172 | 3. **Commit Your Changes** 173 | 174 | ```bash 175 | git commit -m "Add some feature" 176 | ``` 177 | 178 | 4. **Push to the Branch** 179 | 180 | ```bash 181 | git push origin feature/YourFeatureName 182 | ``` 183 | 184 | 5. **Open a Pull Request** 185 | 186 | Please ensure that your contributions adhere to the existing code style and include relevant tests. 187 | 188 | ## 🧪 Testing 189 | 190 | Unit tests are located in the `tests/` directory. To run the tests: 191 | 192 | ```bash 193 | poetry run pytest 194 | ``` 195 | 196 | Ensure that you have the virtual environment activated via Poetry. 197 | 198 | ## 📜 License 199 | 200 | This project is licensed under the [GPLv3](LICENSE). 201 | 202 | ## 📧 Contact 203 | 204 | For any inquiries or support, please contact [devin.r.liu@gmail.com](mailto:devin.r.liu@gmail.com). 205 | 206 | --- 207 | 208 | **Happy Converting! 🚀** 209 | 210 | --- -------------------------------------------------------------------------------- /data/input/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devin-liu/excel-to-markdown/0eb1a23f89919dc61c75c4c14afcd17cc3806086/data/input/.gitkeep -------------------------------------------------------------------------------- /data/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devin-liu/excel-to-markdown/0eb1a23f89919dc61c75c4c14afcd17cc3806086/data/output/.gitkeep -------------------------------------------------------------------------------- /excel_to_markdown/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devin-liu/excel-to-markdown/0eb1a23f89919dc61c75c4c14afcd17cc3806086/excel_to_markdown/__init__.py -------------------------------------------------------------------------------- /excel_to_markdown/detector.py: -------------------------------------------------------------------------------- 1 | # excel_to_markdown/detector.py 2 | 3 | import pandas as pd 4 | 5 | 6 | def detect_table_start(df): 7 | """ 8 | Detect the starting row of the table by finding the first row 9 | that is completely filled within the non-null columns. 10 | """ 11 | # Identify columns that have any non-null values 12 | non_null_columns = df.columns[df.notnull().any(axis=0)] 13 | if len(non_null_columns) == 0: 14 | return None # No non-null columns found 15 | 16 | # Get the indices of the leftmost and rightmost non-null columns 17 | left_col_index = df.columns.get_loc(non_null_columns[0]) 18 | right_col_index = df.columns.get_loc(non_null_columns[-1]) 19 | 20 | # Iterate through each row to find the first fully populated row within the non-null column range 21 | for idx, row in df.iterrows(): 22 | row_slice = row.iloc[left_col_index:right_col_index + 1] 23 | if row_slice.notnull().all() and not row_slice.astype(str).str.strip().eq('').any(): 24 | return idx 25 | 26 | return None 27 | 28 | 29 | def get_table_region(df): 30 | """ 31 | Improved logic to detect all relevant columns based on the presence of header names and non-null values. 32 | """ 33 | start_row = detect_table_start(df) 34 | if start_row is not None: 35 | print(f"Automatically detected table starting at row {start_row + 1}.") 36 | # Consider all columns that have at least a certain percentage of non-null values as part of the table 37 | threshold = 0.49 # At least 49% non-null to be considered 38 | valid_cols = [ 39 | col for col in df.columns if df[col].notnull().mean() > threshold] 40 | return start_row, valid_cols 41 | else: 42 | print("Automatic table detection failed.") 43 | # Manual detection as fallback 44 | while True: 45 | try: 46 | headers_row = int( 47 | input("Enter the header row number (1-based index): ")) - 1 48 | cols_input = input( 49 | "Enter the columns to include (e.g., A:D or 1-4): ") 50 | from .parser import parse_columns 51 | usecols = parse_columns(cols_input, df) 52 | return headers_row, usecols 53 | except Exception as e: 54 | print(f"Invalid input: {e}. Please try again.") 55 | -------------------------------------------------------------------------------- /excel_to_markdown/main copy.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import sys 3 | from pathlib import Path 4 | 5 | def excel_to_markdown(excel_file, sheet_name=0): 6 | """ 7 | Convert a specific sheet in an Excel file to a Markdown table. 8 | 9 | Args: 10 | excel_file (Path): The path to the Excel file. 11 | sheet_name (str or int): The name or index of the sheet to convert. 12 | 13 | Returns: 14 | str: The Markdown representation of the sheet. 15 | """ 16 | # Read the specified sheet from the Excel file 17 | df = pd.read_excel(excel_file, sheet_name=sheet_name) 18 | 19 | # Start building the Markdown table 20 | markdown = "| " + " | ".join(df.columns) + " |\n" 21 | markdown += "| " + " | ".join(["---"] * len(df.columns)) + " |\n" 22 | 23 | # Add each row of the DataFrame to the Markdown table 24 | for _, row in df.iterrows(): 25 | markdown += "| " + " | ".join(str(cell) for cell in row) + " |\n" 26 | 27 | return markdown 28 | 29 | def process_file(input_file, output_dir): 30 | """ 31 | Process an Excel file by converting each of its sheets to separate Markdown files. 32 | 33 | Args: 34 | input_file (Path): The path to the Excel file. 35 | output_dir (Path): The directory where Markdown files will be saved. 36 | """ 37 | try: 38 | # Load the Excel file to get all sheet names 39 | excel = pd.ExcelFile(input_file) 40 | sheet_names = excel.sheet_names 41 | 42 | # Iterate through each sheet in the Excel file 43 | for sheet in sheet_names: 44 | # Convert the current sheet to Markdown 45 | markdown = excel_to_markdown(input_file, sheet) 46 | 47 | # Sanitize the sheet name to create a valid filename 48 | safe_sheet_name = "".join(c if c.isalnum() or c in (' ', '_') else "_" for c in sheet).strip().replace(" ", "_") 49 | 50 | # Create the output filename by appending the sheet name 51 | output_file = output_dir / f"{input_file.stem}_{safe_sheet_name}.md" 52 | 53 | # Write the Markdown content to the output file 54 | with open(output_file, 'w', encoding='utf-8') as f: 55 | f.write(markdown) 56 | 57 | print(f"Markdown file '{output_file}' for sheet '{sheet}' has been created successfully.") 58 | 59 | except Exception as e: 60 | print(f"An error occurred processing {input_file}: {e}") 61 | 62 | def main(): 63 | """ 64 | The main function to execute the script. It parses command-line arguments and processes Excel files. 65 | """ 66 | if len(sys.argv) < 2: 67 | print("Usage: python script.py ") 68 | sys.exit(1) 69 | 70 | # Define input and output directories from command-line arguments 71 | input_dir = Path(sys.argv[1]) 72 | output_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else input_dir / 'output' 73 | 74 | # Create the output directory if it doesn't exist 75 | output_dir.mkdir(parents=True, exist_ok=True) 76 | 77 | # Iterate through all Excel files in the input directory 78 | for excel_file in input_dir.glob('*.xlsx'): 79 | process_file(excel_file, output_dir) 80 | 81 | if __name__ == "__main__": 82 | main() 83 | -------------------------------------------------------------------------------- /excel_to_markdown/main.py: -------------------------------------------------------------------------------- 1 | # excel_to_markdown/main.py 2 | 3 | import pandas as pd 4 | import sys 5 | from pathlib import Path 6 | 7 | from .detector import get_table_region 8 | from .markdown_generator import dataframe_to_markdown 9 | from .utils import create_output_filename 10 | 11 | def excel_to_markdown(excel_file, sheet_name=0): 12 | """ 13 | Convert a specific sheet in an Excel file to a Markdown table interactively. 14 | 15 | Args: 16 | excel_file (Path): The path to the Excel file. 17 | sheet_name (str or int): The name or index of the sheet to convert. 18 | 19 | Returns: 20 | str: The Markdown representation of the sheet. 21 | """ 22 | # Read the entire sheet without specifying headers or columns 23 | df_full = pd.read_excel(excel_file, sheet_name=sheet_name, header=None, engine='openpyxl') 24 | 25 | # Detect table region 26 | headers_row, usecols = get_table_region(df_full) 27 | 28 | # Read the table with detected or user-specified parameters 29 | df = pd.read_excel( 30 | excel_file, 31 | sheet_name=sheet_name, 32 | header=headers_row, 33 | usecols=usecols, 34 | engine='openpyxl' 35 | ) 36 | 37 | # Drop completely empty rows 38 | df.dropna(how='all', inplace=True) 39 | 40 | # Reset index after dropping rows 41 | df.reset_index(drop=True, inplace=True) 42 | 43 | # Generate the markdown table 44 | markdown = dataframe_to_markdown(df) 45 | 46 | return markdown 47 | 48 | def process_file(input_file, output_dir): 49 | """ 50 | Process an Excel file by converting each of its sheets to separate Markdown files interactively. 51 | 52 | Args: 53 | input_file (Path): The path to the Excel file. 54 | output_dir (Path): The directory where Markdown files will be saved. 55 | """ 56 | try: 57 | # Load the Excel file to get all sheet names 58 | excel = pd.ExcelFile(input_file, engine='openpyxl') 59 | sheet_names = excel.sheet_names 60 | 61 | # Iterate through each sheet in the Excel file 62 | for sheet in sheet_names: 63 | print(f"\nProcessing sheet: '{sheet}' in file '{input_file.name}'") 64 | # Convert the current sheet to Markdown interactively 65 | markdown = excel_to_markdown(input_file, sheet) 66 | 67 | # Create the output filename using utility function 68 | output_file = create_output_filename(input_file, sheet, output_dir) 69 | 70 | # Write the Markdown content to the output file 71 | with open(output_file, 'w', encoding='utf-8') as f: 72 | f.write(markdown) 73 | 74 | print(f"Markdown file '{output_file}' for sheet '{sheet}' has been created successfully.") 75 | 76 | except Exception as e: 77 | print(f"An error occurred processing {input_file}: {e}") 78 | 79 | def main(): 80 | """ 81 | The main function to execute the script. It parses command-line arguments and processes Excel files interactively. 82 | """ 83 | if len(sys.argv) < 2: 84 | print("Usage: python -m excel_to_markdown.main ") 85 | sys.exit(1) 86 | 87 | # Define input and output directories from command-line arguments 88 | input_dir = Path(sys.argv[1]) 89 | output_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else input_dir / 'output' 90 | 91 | # Create the output directory if it doesn't exist 92 | output_dir.mkdir(parents=True, exist_ok=True) 93 | 94 | # Iterate through all Excel files in the input directory 95 | excel_files = list(input_dir.glob('*.xlsx')) + list(input_dir.glob('*.xls')) 96 | if not excel_files: 97 | print(f"No Excel files found in {input_dir}.") 98 | sys.exit(1) 99 | 100 | for excel_file in excel_files: 101 | process_file(excel_file, output_dir) 102 | 103 | if __name__ == "__main__": 104 | main() 105 | -------------------------------------------------------------------------------- /excel_to_markdown/markdown_generator.py: -------------------------------------------------------------------------------- 1 | # excel_to_markdown/markdown_generator.py 2 | import pandas as pd 3 | 4 | 5 | def dataframe_to_markdown(df): 6 | """ 7 | Convert a pandas DataFrame to a Markdown table. 8 | 9 | Args: 10 | df (pd.DataFrame): The DataFrame to convert. 11 | 12 | Returns: 13 | str: Markdown-formatted table. 14 | """ 15 | if df.empty: 16 | return "" 17 | # Generate the header row 18 | markdown = "| " + " | ".join(df.columns) + " |\n" 19 | # Generate the separator row 20 | markdown += "| " + " | ".join(["---"] * len(df.columns)) + " |\n" 21 | 22 | # Generate each data row 23 | for _, row in df.iterrows(): 24 | row_values = [str(cell) if pd.notnull(cell) else "" for cell in row] 25 | markdown += "| " + " | ".join(row_values) + " |\n" 26 | 27 | return markdown 28 | -------------------------------------------------------------------------------- /excel_to_markdown/parser.py: -------------------------------------------------------------------------------- 1 | from .utils import column_letter_to_index 2 | 3 | 4 | def parse_columns(cols_input, df): 5 | """ 6 | Parse the user input for columns. 7 | 8 | Args: 9 | cols_input (str): User input specifying columns (e.g., "A:D" or "1-4"). 10 | 11 | Returns: 12 | list: List of column names based on the input. 13 | """ 14 | cols_input = cols_input.replace(" ", "").upper() 15 | if ':' in cols_input: 16 | start, end = cols_input.split(':') 17 | elif '-' in cols_input: 18 | start, end = cols_input.split('-') 19 | else: 20 | start, end = cols_input, cols_input 21 | 22 | if start.isalpha() and end.isalpha(): 23 | start_idx = column_letter_to_index(start) 24 | end_idx = column_letter_to_index(end) 25 | elif start.isdigit() and end.isdigit(): 26 | start_idx = int(start) - 1 27 | end_idx = int(end) - 1 28 | else: 29 | raise ValueError("Mixed or invalid column specification.") 30 | 31 | if start_idx > end_idx: 32 | raise ValueError("Start column comes after end column.") 33 | 34 | selected_columns = df.columns[start_idx:end_idx + 1].tolist() 35 | return selected_columns 36 | -------------------------------------------------------------------------------- /excel_to_markdown/utils.py: -------------------------------------------------------------------------------- 1 | import string 2 | import re 3 | from pathlib import Path 4 | 5 | 6 | def column_letter_to_index(letter): 7 | """ 8 | Convert Excel column letter to zero-based index. 9 | 10 | Args: 11 | letter (str): Column letter (e.g., 'A', 'AA'). 12 | 13 | Returns: 14 | int: Zero-based column index. 15 | """ 16 | letter = letter.upper() 17 | result = 0 18 | for char in letter: 19 | if char in string.ascii_uppercase: 20 | result = result * 26 + (ord(char) - ord('A') + 1) 21 | else: 22 | raise ValueError(f"Invalid column letter: {char}") 23 | return result - 1 24 | 25 | 26 | def sanitize_sheet_name(sheet_name): 27 | """ 28 | Sanitize sheet name to create a valid filename. 29 | 30 | Args: 31 | sheet_name (str): Original sheet name. 32 | 33 | Returns: 34 | str: Sanitized sheet name. 35 | """ 36 | sanitized = re.sub(r'[^\w\s]', '_', sheet_name).strip().replace(" ", "_") 37 | return sanitized 38 | 39 | 40 | def create_output_filename(input_file, sheet_name, output_dir): 41 | """ 42 | Create a sanitized output filename based on input file and sheet name. 43 | 44 | Args: 45 | input_file (Path): Path to the input Excel file. 46 | sheet_name (str): Name of the sheet. 47 | output_dir (Path): Directory to save the output. 48 | 49 | Returns: 50 | Path: Full path to the output Markdown file. 51 | """ 52 | safe_sheet_name = sanitize_sheet_name(sheet_name) 53 | output_filename = f"{input_file.stem}_{safe_sheet_name}.md" 54 | return output_dir / output_filename 55 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "altair" 5 | version = "5.5.0" 6 | description = "Vega-Altair: A declarative statistical visualization library for Python." 7 | optional = false 8 | python-versions = ">=3.9" 9 | groups = ["main"] 10 | files = [ 11 | {file = "altair-5.5.0-py3-none-any.whl", hash = "sha256:91a310b926508d560fe0148d02a194f38b824122641ef528113d029fcd129f8c"}, 12 | {file = "altair-5.5.0.tar.gz", hash = "sha256:d960ebe6178c56de3855a68c47b516be38640b73fb3b5111c2a9ca90546dd73d"}, 13 | ] 14 | 15 | [package.dependencies] 16 | jinja2 = "*" 17 | jsonschema = ">=3.0" 18 | narwhals = ">=1.14.2" 19 | packaging = "*" 20 | typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.14\""} 21 | 22 | [package.extras] 23 | all = ["altair-tiles (>=0.3.0)", "anywidget (>=0.9.0)", "numpy", "pandas (>=1.1.3)", "pyarrow (>=11)", "vega-datasets (>=0.9.0)", "vegafusion[embed] (>=1.6.6)", "vl-convert-python (>=1.7.0)"] 24 | dev = ["duckdb (>=1.0)", "geopandas", "hatch (>=1.13.0)", "ipython[kernel]", "mistune", "mypy", "pandas (>=1.1.3)", "pandas-stubs", "polars (>=0.20.3)", "pyarrow-stubs", "pytest", "pytest-cov", "pytest-xdist[psutil] (>=3.5,<4.0)", "ruff (>=0.6.0)", "types-jsonschema", "types-setuptools"] 25 | doc = ["docutils", "jinja2", "myst-parser", "numpydoc", "pillow (>=9,<10)", "pydata-sphinx-theme (>=0.14.1)", "scipy", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinxext-altair"] 26 | save = ["vl-convert-python (>=1.7.0)"] 27 | 28 | [[package]] 29 | name = "attrs" 30 | version = "25.3.0" 31 | description = "Classes Without Boilerplate" 32 | optional = false 33 | python-versions = ">=3.8" 34 | groups = ["main"] 35 | files = [ 36 | {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, 37 | {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, 38 | ] 39 | 40 | [package.extras] 41 | benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 42 | cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 43 | dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 44 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] 45 | tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 46 | tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] 47 | 48 | [[package]] 49 | name = "blinker" 50 | version = "1.9.0" 51 | description = "Fast, simple object-to-object and broadcast signaling" 52 | optional = false 53 | python-versions = ">=3.9" 54 | groups = ["main"] 55 | files = [ 56 | {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, 57 | {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, 58 | ] 59 | 60 | [[package]] 61 | name = "cachetools" 62 | version = "5.5.2" 63 | description = "Extensible memoizing collections and decorators" 64 | optional = false 65 | python-versions = ">=3.7" 66 | groups = ["main"] 67 | files = [ 68 | {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, 69 | {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, 70 | ] 71 | 72 | [[package]] 73 | name = "certifi" 74 | version = "2025.1.31" 75 | description = "Python package for providing Mozilla's CA Bundle." 76 | optional = false 77 | python-versions = ">=3.6" 78 | groups = ["main"] 79 | files = [ 80 | {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, 81 | {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, 82 | ] 83 | 84 | [[package]] 85 | name = "charset-normalizer" 86 | version = "3.4.1" 87 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 88 | optional = false 89 | python-versions = ">=3.7" 90 | groups = ["main"] 91 | files = [ 92 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 93 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 94 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 95 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 96 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 97 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 98 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 99 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 100 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 101 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 102 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 103 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 104 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 105 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 106 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 107 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 108 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 109 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 110 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 111 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 112 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 113 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 114 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 115 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 116 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 117 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 118 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 119 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 120 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 121 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 122 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 123 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 124 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 125 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 126 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 127 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 128 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 129 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 130 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 131 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 132 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 133 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 134 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 135 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 136 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 137 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 138 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 139 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 140 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 141 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 142 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 143 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 144 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 145 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 146 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 147 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 148 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 149 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 150 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 151 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 152 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 153 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 154 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 155 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 156 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 157 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 158 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 159 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 160 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 161 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 162 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 163 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 164 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 165 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 166 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 167 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 168 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 169 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 170 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 171 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 172 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 173 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 174 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 175 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 176 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 177 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 178 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 179 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 180 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 181 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 182 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 183 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 184 | ] 185 | 186 | [[package]] 187 | name = "click" 188 | version = "8.1.8" 189 | description = "Composable command line interface toolkit" 190 | optional = false 191 | python-versions = ">=3.7" 192 | groups = ["main"] 193 | files = [ 194 | {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, 195 | {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, 196 | ] 197 | 198 | [package.dependencies] 199 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 200 | 201 | [[package]] 202 | name = "colorama" 203 | version = "0.4.6" 204 | description = "Cross-platform colored terminal text." 205 | optional = false 206 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 207 | groups = ["main", "dev"] 208 | files = [ 209 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 210 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 211 | ] 212 | markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} 213 | 214 | [[package]] 215 | name = "et-xmlfile" 216 | version = "2.0.0" 217 | description = "An implementation of lxml.xmlfile for the standard library" 218 | optional = false 219 | python-versions = ">=3.8" 220 | groups = ["main"] 221 | files = [ 222 | {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, 223 | {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, 224 | ] 225 | 226 | [[package]] 227 | name = "gitdb" 228 | version = "4.0.12" 229 | description = "Git Object Database" 230 | optional = false 231 | python-versions = ">=3.7" 232 | groups = ["main"] 233 | files = [ 234 | {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, 235 | {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, 236 | ] 237 | 238 | [package.dependencies] 239 | smmap = ">=3.0.1,<6" 240 | 241 | [[package]] 242 | name = "gitpython" 243 | version = "3.1.44" 244 | description = "GitPython is a Python library used to interact with Git repositories" 245 | optional = false 246 | python-versions = ">=3.7" 247 | groups = ["main"] 248 | files = [ 249 | {file = "GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110"}, 250 | {file = "gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269"}, 251 | ] 252 | 253 | [package.dependencies] 254 | gitdb = ">=4.0.1,<5" 255 | 256 | [package.extras] 257 | doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] 258 | test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock ; python_version < \"3.8\"", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions ; python_version < \"3.11\""] 259 | 260 | [[package]] 261 | name = "idna" 262 | version = "3.10" 263 | description = "Internationalized Domain Names in Applications (IDNA)" 264 | optional = false 265 | python-versions = ">=3.6" 266 | groups = ["main"] 267 | files = [ 268 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 269 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 270 | ] 271 | 272 | [package.extras] 273 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 274 | 275 | [[package]] 276 | name = "iniconfig" 277 | version = "2.1.0" 278 | description = "brain-dead simple config-ini parsing" 279 | optional = false 280 | python-versions = ">=3.8" 281 | groups = ["dev"] 282 | files = [ 283 | {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, 284 | {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, 285 | ] 286 | 287 | [[package]] 288 | name = "jinja2" 289 | version = "3.1.6" 290 | description = "A very fast and expressive template engine." 291 | optional = false 292 | python-versions = ">=3.7" 293 | groups = ["main"] 294 | files = [ 295 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 296 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 297 | ] 298 | 299 | [package.dependencies] 300 | MarkupSafe = ">=2.0" 301 | 302 | [package.extras] 303 | i18n = ["Babel (>=2.7)"] 304 | 305 | [[package]] 306 | name = "jsonschema" 307 | version = "4.23.0" 308 | description = "An implementation of JSON Schema validation for Python" 309 | optional = false 310 | python-versions = ">=3.8" 311 | groups = ["main"] 312 | files = [ 313 | {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, 314 | {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, 315 | ] 316 | 317 | [package.dependencies] 318 | attrs = ">=22.2.0" 319 | jsonschema-specifications = ">=2023.03.6" 320 | referencing = ">=0.28.4" 321 | rpds-py = ">=0.7.1" 322 | 323 | [package.extras] 324 | format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] 325 | format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] 326 | 327 | [[package]] 328 | name = "jsonschema-specifications" 329 | version = "2024.10.1" 330 | description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" 331 | optional = false 332 | python-versions = ">=3.9" 333 | groups = ["main"] 334 | files = [ 335 | {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, 336 | {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, 337 | ] 338 | 339 | [package.dependencies] 340 | referencing = ">=0.31.0" 341 | 342 | [[package]] 343 | name = "markupsafe" 344 | version = "3.0.2" 345 | description = "Safely add untrusted strings to HTML/XML markup." 346 | optional = false 347 | python-versions = ">=3.9" 348 | groups = ["main"] 349 | files = [ 350 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 351 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 352 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 353 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 354 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 355 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 356 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 357 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 358 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 359 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 360 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 361 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 362 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 363 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 364 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 365 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 366 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 367 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 368 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 369 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 370 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 371 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 372 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 373 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 374 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 375 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 376 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 377 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 378 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 379 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 380 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 381 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 382 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 383 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 384 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 385 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 386 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 387 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 388 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 389 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 390 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 391 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 392 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 393 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 394 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 395 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 396 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 397 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 398 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 399 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 400 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 401 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 402 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 403 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 404 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 405 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 406 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 407 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 408 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 409 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 410 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 411 | ] 412 | 413 | [[package]] 414 | name = "narwhals" 415 | version = "1.33.0" 416 | description = "Extremely lightweight compatibility layer between dataframe libraries" 417 | optional = false 418 | python-versions = ">=3.8" 419 | groups = ["main"] 420 | files = [ 421 | {file = "narwhals-1.33.0-py3-none-any.whl", hash = "sha256:f653319112fd121a1f1c18a40cf70dada773cdacfd53e62c2aa0afae43c17129"}, 422 | {file = "narwhals-1.33.0.tar.gz", hash = "sha256:6233d2457debf4b5fe4a1da54530c6fe2d84326f4a8e3bca35bbbff580a347cb"}, 423 | ] 424 | 425 | [package.extras] 426 | cudf = ["cudf (>=24.10.0)"] 427 | dask = ["dask[dataframe] (>=2024.8)"] 428 | duckdb = ["duckdb (>=1.0)"] 429 | ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] 430 | modin = ["modin"] 431 | pandas = ["pandas (>=0.25.3)"] 432 | polars = ["polars (>=0.20.3)"] 433 | pyarrow = ["pyarrow (>=11.0.0)"] 434 | pyspark = ["pyspark (>=3.5.0)"] 435 | sqlframe = ["sqlframe (>=3.22.0)"] 436 | 437 | [[package]] 438 | name = "numpy" 439 | version = "2.2.4" 440 | description = "Fundamental package for array computing in Python" 441 | optional = false 442 | python-versions = ">=3.10" 443 | groups = ["main"] 444 | files = [ 445 | {file = "numpy-2.2.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8146f3550d627252269ac42ae660281d673eb6f8b32f113538e0cc2a9aed42b9"}, 446 | {file = "numpy-2.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e642d86b8f956098b564a45e6f6ce68a22c2c97a04f5acd3f221f57b8cb850ae"}, 447 | {file = "numpy-2.2.4-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:a84eda42bd12edc36eb5b53bbcc9b406820d3353f1994b6cfe453a33ff101775"}, 448 | {file = "numpy-2.2.4-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:4ba5054787e89c59c593a4169830ab362ac2bee8a969249dc56e5d7d20ff8df9"}, 449 | {file = "numpy-2.2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7716e4a9b7af82c06a2543c53ca476fa0b57e4d760481273e09da04b74ee6ee2"}, 450 | {file = "numpy-2.2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf8c1d66f432ce577d0197dceaac2ac00c0759f573f28516246351c58a85020"}, 451 | {file = "numpy-2.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:218f061d2faa73621fa23d6359442b0fc658d5b9a70801373625d958259eaca3"}, 452 | {file = "numpy-2.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df2f57871a96bbc1b69733cd4c51dc33bea66146b8c63cacbfed73eec0883017"}, 453 | {file = "numpy-2.2.4-cp310-cp310-win32.whl", hash = "sha256:a0258ad1f44f138b791327961caedffbf9612bfa504ab9597157806faa95194a"}, 454 | {file = "numpy-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:0d54974f9cf14acf49c60f0f7f4084b6579d24d439453d5fc5805d46a165b542"}, 455 | {file = "numpy-2.2.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9e0a277bb2eb5d8a7407e14688b85fd8ad628ee4e0c7930415687b6564207a4"}, 456 | {file = "numpy-2.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eeea959168ea555e556b8188da5fa7831e21d91ce031e95ce23747b7609f8a4"}, 457 | {file = "numpy-2.2.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bd3ad3b0a40e713fc68f99ecfd07124195333f1e689387c180813f0e94309d6f"}, 458 | {file = "numpy-2.2.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cf28633d64294969c019c6df4ff37f5698e8326db68cc2b66576a51fad634880"}, 459 | {file = "numpy-2.2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fa8fa7697ad1646b5c93de1719965844e004fcad23c91228aca1cf0800044a1"}, 460 | {file = "numpy-2.2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4162988a360a29af158aeb4a2f4f09ffed6a969c9776f8f3bdee9b06a8ab7e5"}, 461 | {file = "numpy-2.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:892c10d6a73e0f14935c31229e03325a7b3093fafd6ce0af704be7f894d95687"}, 462 | {file = "numpy-2.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db1f1c22173ac1c58db249ae48aa7ead29f534b9a948bc56828337aa84a32ed6"}, 463 | {file = "numpy-2.2.4-cp311-cp311-win32.whl", hash = "sha256:ea2bb7e2ae9e37d96835b3576a4fa4b3a97592fbea8ef7c3587078b0068b8f09"}, 464 | {file = "numpy-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:f7de08cbe5551911886d1ab60de58448c6df0f67d9feb7d1fb21e9875ef95e91"}, 465 | {file = "numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4"}, 466 | {file = "numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854"}, 467 | {file = "numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24"}, 468 | {file = "numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee"}, 469 | {file = "numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba"}, 470 | {file = "numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592"}, 471 | {file = "numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb"}, 472 | {file = "numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f"}, 473 | {file = "numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00"}, 474 | {file = "numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146"}, 475 | {file = "numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7"}, 476 | {file = "numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0"}, 477 | {file = "numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392"}, 478 | {file = "numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc"}, 479 | {file = "numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298"}, 480 | {file = "numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7"}, 481 | {file = "numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6"}, 482 | {file = "numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd"}, 483 | {file = "numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c"}, 484 | {file = "numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3"}, 485 | {file = "numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8"}, 486 | {file = "numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39"}, 487 | {file = "numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd"}, 488 | {file = "numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0"}, 489 | {file = "numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960"}, 490 | {file = "numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8"}, 491 | {file = "numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc"}, 492 | {file = "numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff"}, 493 | {file = "numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286"}, 494 | {file = "numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d"}, 495 | {file = "numpy-2.2.4-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7051ee569db5fbac144335e0f3b9c2337e0c8d5c9fee015f259a5bd70772b7e8"}, 496 | {file = "numpy-2.2.4-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ab2939cd5bec30a7430cbdb2287b63151b77cf9624de0532d629c9a1c59b1d5c"}, 497 | {file = "numpy-2.2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f35b19894a9e08639fd60a1ec1978cb7f5f7f1eace62f38dd36be8aecdef4d"}, 498 | {file = "numpy-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b4adfbbc64014976d2f91084915ca4e626fbf2057fb81af209c1a6d776d23e3d"}, 499 | {file = "numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f"}, 500 | ] 501 | 502 | [[package]] 503 | name = "openpyxl" 504 | version = "3.1.5" 505 | description = "A Python library to read/write Excel 2010 xlsx/xlsm files" 506 | optional = false 507 | python-versions = ">=3.8" 508 | groups = ["main"] 509 | files = [ 510 | {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, 511 | {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, 512 | ] 513 | 514 | [package.dependencies] 515 | et-xmlfile = "*" 516 | 517 | [[package]] 518 | name = "packaging" 519 | version = "24.2" 520 | description = "Core utilities for Python packages" 521 | optional = false 522 | python-versions = ">=3.8" 523 | groups = ["main", "dev"] 524 | files = [ 525 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 526 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 527 | ] 528 | 529 | [[package]] 530 | name = "pandas" 531 | version = "2.2.3" 532 | description = "Powerful data structures for data analysis, time series, and statistics" 533 | optional = false 534 | python-versions = ">=3.9" 535 | groups = ["main"] 536 | files = [ 537 | {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, 538 | {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, 539 | {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, 540 | {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, 541 | {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, 542 | {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, 543 | {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, 544 | {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, 545 | {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, 546 | {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, 547 | {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, 548 | {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, 549 | {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, 550 | {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, 551 | {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, 552 | {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, 553 | {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, 554 | {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, 555 | {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, 556 | {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, 557 | {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, 558 | {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, 559 | {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, 560 | {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, 561 | {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, 562 | {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, 563 | {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, 564 | {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, 565 | {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, 566 | {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, 567 | {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, 568 | {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, 569 | {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, 570 | {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, 571 | {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, 572 | {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, 573 | {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, 574 | {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, 575 | {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, 576 | {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, 577 | {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, 578 | {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, 579 | ] 580 | 581 | [package.dependencies] 582 | numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} 583 | python-dateutil = ">=2.8.2" 584 | pytz = ">=2020.1" 585 | tzdata = ">=2022.7" 586 | 587 | [package.extras] 588 | all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] 589 | aws = ["s3fs (>=2022.11.0)"] 590 | clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] 591 | compression = ["zstandard (>=0.19.0)"] 592 | computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] 593 | consortium-standard = ["dataframe-api-compat (>=0.1.7)"] 594 | excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] 595 | feather = ["pyarrow (>=10.0.1)"] 596 | fss = ["fsspec (>=2022.11.0)"] 597 | gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] 598 | hdf5 = ["tables (>=3.8.0)"] 599 | html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] 600 | mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] 601 | output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] 602 | parquet = ["pyarrow (>=10.0.1)"] 603 | performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] 604 | plot = ["matplotlib (>=3.6.3)"] 605 | postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] 606 | pyarrow = ["pyarrow (>=10.0.1)"] 607 | spss = ["pyreadstat (>=1.2.0)"] 608 | sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] 609 | test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] 610 | xml = ["lxml (>=4.9.2)"] 611 | 612 | [[package]] 613 | name = "pillow" 614 | version = "11.1.0" 615 | description = "Python Imaging Library (Fork)" 616 | optional = false 617 | python-versions = ">=3.9" 618 | groups = ["main"] 619 | files = [ 620 | {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, 621 | {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, 622 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, 623 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, 624 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, 625 | {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, 626 | {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, 627 | {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, 628 | {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, 629 | {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, 630 | {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, 631 | {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, 632 | {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, 633 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, 634 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, 635 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, 636 | {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, 637 | {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, 638 | {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, 639 | {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, 640 | {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, 641 | {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, 642 | {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, 643 | {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, 644 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, 645 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, 646 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, 647 | {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, 648 | {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, 649 | {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, 650 | {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, 651 | {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, 652 | {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, 653 | {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, 654 | {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, 655 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, 656 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, 657 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, 658 | {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, 659 | {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, 660 | {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, 661 | {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, 662 | {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, 663 | {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, 664 | {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, 665 | {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, 666 | {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, 667 | {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, 668 | {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, 669 | {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, 670 | {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, 671 | {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, 672 | {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, 673 | {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, 674 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, 675 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, 676 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, 677 | {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, 678 | {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, 679 | {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, 680 | {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, 681 | {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, 682 | {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, 683 | {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, 684 | {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, 685 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, 686 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, 687 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, 688 | {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, 689 | {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, 690 | {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, 691 | ] 692 | 693 | [package.extras] 694 | docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] 695 | fpx = ["olefile"] 696 | mic = ["olefile"] 697 | tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] 698 | typing = ["typing-extensions ; python_version < \"3.10\""] 699 | xmp = ["defusedxml"] 700 | 701 | [[package]] 702 | name = "pluggy" 703 | version = "1.5.0" 704 | description = "plugin and hook calling mechanisms for python" 705 | optional = false 706 | python-versions = ">=3.8" 707 | groups = ["dev"] 708 | files = [ 709 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 710 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 711 | ] 712 | 713 | [package.extras] 714 | dev = ["pre-commit", "tox"] 715 | testing = ["pytest", "pytest-benchmark"] 716 | 717 | [[package]] 718 | name = "protobuf" 719 | version = "5.29.4" 720 | description = "" 721 | optional = false 722 | python-versions = ">=3.8" 723 | groups = ["main"] 724 | files = [ 725 | {file = "protobuf-5.29.4-cp310-abi3-win32.whl", hash = "sha256:13eb236f8eb9ec34e63fc8b1d6efd2777d062fa6aaa68268fb67cf77f6839ad7"}, 726 | {file = "protobuf-5.29.4-cp310-abi3-win_amd64.whl", hash = "sha256:bcefcdf3976233f8a502d265eb65ea740c989bacc6c30a58290ed0e519eb4b8d"}, 727 | {file = "protobuf-5.29.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:307ecba1d852ec237e9ba668e087326a67564ef83e45a0189a772ede9e854dd0"}, 728 | {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e"}, 729 | {file = "protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922"}, 730 | {file = "protobuf-5.29.4-cp38-cp38-win32.whl", hash = "sha256:1832f0515b62d12d8e6ffc078d7e9eb06969aa6dc13c13e1036e39d73bebc2de"}, 731 | {file = "protobuf-5.29.4-cp38-cp38-win_amd64.whl", hash = "sha256:476cb7b14914c780605a8cf62e38c2a85f8caff2e28a6a0bad827ec7d6c85d68"}, 732 | {file = "protobuf-5.29.4-cp39-cp39-win32.whl", hash = "sha256:fd32223020cb25a2cc100366f1dedc904e2d71d9322403224cdde5fdced0dabe"}, 733 | {file = "protobuf-5.29.4-cp39-cp39-win_amd64.whl", hash = "sha256:678974e1e3a9b975b8bc2447fca458db5f93a2fb6b0c8db46b6675b5b5346812"}, 734 | {file = "protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862"}, 735 | {file = "protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99"}, 736 | ] 737 | 738 | [[package]] 739 | name = "pyarrow" 740 | version = "19.0.1" 741 | description = "Python library for Apache Arrow" 742 | optional = false 743 | python-versions = ">=3.9" 744 | groups = ["main"] 745 | files = [ 746 | {file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:fc28912a2dc924dddc2087679cc8b7263accc71b9ff025a1362b004711661a69"}, 747 | {file = "pyarrow-19.0.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:fca15aabbe9b8355800d923cc2e82c8ef514af321e18b437c3d782aa884eaeec"}, 748 | {file = "pyarrow-19.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad76aef7f5f7e4a757fddcdcf010a8290958f09e3470ea458c80d26f4316ae89"}, 749 | {file = "pyarrow-19.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d03c9d6f2a3dffbd62671ca070f13fc527bb1867b4ec2b98c7eeed381d4f389a"}, 750 | {file = "pyarrow-19.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:65cf9feebab489b19cdfcfe4aa82f62147218558d8d3f0fc1e9dea0ab8e7905a"}, 751 | {file = "pyarrow-19.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:41f9706fbe505e0abc10e84bf3a906a1338905cbbcf1177b71486b03e6ea6608"}, 752 | {file = "pyarrow-19.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6cb2335a411b713fdf1e82a752162f72d4a7b5dbc588e32aa18383318b05866"}, 753 | {file = "pyarrow-19.0.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:cc55d71898ea30dc95900297d191377caba257612f384207fe9f8293b5850f90"}, 754 | {file = "pyarrow-19.0.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:7a544ec12de66769612b2d6988c36adc96fb9767ecc8ee0a4d270b10b1c51e00"}, 755 | {file = "pyarrow-19.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0148bb4fc158bfbc3d6dfe5001d93ebeed253793fff4435167f6ce1dc4bddeae"}, 756 | {file = "pyarrow-19.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f24faab6ed18f216a37870d8c5623f9c044566d75ec586ef884e13a02a9d62c5"}, 757 | {file = "pyarrow-19.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4982f8e2b7afd6dae8608d70ba5bd91699077323f812a0448d8b7abdff6cb5d3"}, 758 | {file = "pyarrow-19.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:49a3aecb62c1be1d822f8bf629226d4a96418228a42f5b40835c1f10d42e4db6"}, 759 | {file = "pyarrow-19.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:008a4009efdb4ea3d2e18f05cd31f9d43c388aad29c636112c2966605ba33466"}, 760 | {file = "pyarrow-19.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:80b2ad2b193e7d19e81008a96e313fbd53157945c7be9ac65f44f8937a55427b"}, 761 | {file = "pyarrow-19.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee8dec072569f43835932a3b10c55973593abc00936c202707a4ad06af7cb294"}, 762 | {file = "pyarrow-19.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5d1ec7ec5324b98887bdc006f4d2ce534e10e60f7ad995e7875ffa0ff9cb14"}, 763 | {file = "pyarrow-19.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ad4c0eb4e2a9aeb990af6c09e6fa0b195c8c0e7b272ecc8d4d2b6574809d34"}, 764 | {file = "pyarrow-19.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d383591f3dcbe545f6cc62daaef9c7cdfe0dff0fb9e1c8121101cabe9098cfa6"}, 765 | {file = "pyarrow-19.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b4c4156a625f1e35d6c0b2132635a237708944eb41df5fbe7d50f20d20c17832"}, 766 | {file = "pyarrow-19.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5bd1618ae5e5476b7654c7b55a6364ae87686d4724538c24185bbb2952679960"}, 767 | {file = "pyarrow-19.0.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:e45274b20e524ae5c39d7fc1ca2aa923aab494776d2d4b316b49ec7572ca324c"}, 768 | {file = "pyarrow-19.0.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:d9dedeaf19097a143ed6da37f04f4051aba353c95ef507764d344229b2b740ae"}, 769 | {file = "pyarrow-19.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebfb5171bb5f4a52319344ebbbecc731af3f021e49318c74f33d520d31ae0c4"}, 770 | {file = "pyarrow-19.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a21d39fbdb948857f67eacb5bbaaf36802de044ec36fbef7a1c8f0dd3a4ab2"}, 771 | {file = "pyarrow-19.0.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:99bc1bec6d234359743b01e70d4310d0ab240c3d6b0da7e2a93663b0158616f6"}, 772 | {file = "pyarrow-19.0.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1b93ef2c93e77c442c979b0d596af45e4665d8b96da598db145b0fec014b9136"}, 773 | {file = "pyarrow-19.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9d46e06846a41ba906ab25302cf0fd522f81aa2a85a71021826f34639ad31ef"}, 774 | {file = "pyarrow-19.0.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:c0fe3dbbf054a00d1f162fda94ce236a899ca01123a798c561ba307ca38af5f0"}, 775 | {file = "pyarrow-19.0.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:96606c3ba57944d128e8a8399da4812f56c7f61de8c647e3470b417f795d0ef9"}, 776 | {file = "pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f04d49a6b64cf24719c080b3c2029a3a5b16417fd5fd7c4041f94233af732f3"}, 777 | {file = "pyarrow-19.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9137cf7e1640dce4c190551ee69d478f7121b5c6f323553b319cac936395f6"}, 778 | {file = "pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:7c1bca1897c28013db5e4c83944a2ab53231f541b9e0c3f4791206d0c0de389a"}, 779 | {file = "pyarrow-19.0.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:58d9397b2e273ef76264b45531e9d552d8ec8a6688b7390b5be44c02a37aade8"}, 780 | {file = "pyarrow-19.0.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:b9766a47a9cb56fefe95cb27f535038b5a195707a08bf61b180e642324963b46"}, 781 | {file = "pyarrow-19.0.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:6c5941c1aac89a6c2f2b16cd64fe76bcdb94b2b1e99ca6459de4e6f07638d755"}, 782 | {file = "pyarrow-19.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd44d66093a239358d07c42a91eebf5015aa54fccba959db899f932218ac9cc8"}, 783 | {file = "pyarrow-19.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:335d170e050bcc7da867a1ed8ffb8b44c57aaa6e0843b156a501298657b1e972"}, 784 | {file = "pyarrow-19.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:1c7556165bd38cf0cd992df2636f8bcdd2d4b26916c6b7e646101aff3c16f76f"}, 785 | {file = "pyarrow-19.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:699799f9c80bebcf1da0983ba86d7f289c5a2a5c04b945e2f2bcf7e874a91911"}, 786 | {file = "pyarrow-19.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8464c9fbe6d94a7fe1599e7e8965f350fd233532868232ab2596a71586c5a429"}, 787 | {file = "pyarrow-19.0.1.tar.gz", hash = "sha256:3bf266b485df66a400f282ac0b6d1b500b9d2ae73314a153dbe97d6d5cc8a99e"}, 788 | ] 789 | 790 | [package.extras] 791 | test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] 792 | 793 | [[package]] 794 | name = "pydeck" 795 | version = "0.9.1" 796 | description = "Widget for deck.gl maps" 797 | optional = false 798 | python-versions = ">=3.8" 799 | groups = ["main"] 800 | files = [ 801 | {file = "pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038"}, 802 | {file = "pydeck-0.9.1.tar.gz", hash = "sha256:f74475ae637951d63f2ee58326757f8d4f9cd9f2a457cf42950715003e2cb605"}, 803 | ] 804 | 805 | [package.dependencies] 806 | jinja2 = ">=2.10.1" 807 | numpy = ">=1.16.4" 808 | 809 | [package.extras] 810 | carto = ["pydeck-carto"] 811 | jupyter = ["ipykernel (>=5.1.2) ; python_version >= \"3.4\"", "ipython (>=5.8.0) ; python_version < \"3.4\"", "ipywidgets (>=7,<8)", "traitlets (>=4.3.2)"] 812 | 813 | [[package]] 814 | name = "pytest" 815 | version = "7.4.4" 816 | description = "pytest: simple powerful testing with Python" 817 | optional = false 818 | python-versions = ">=3.7" 819 | groups = ["dev"] 820 | files = [ 821 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 822 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 823 | ] 824 | 825 | [package.dependencies] 826 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 827 | iniconfig = "*" 828 | packaging = "*" 829 | pluggy = ">=0.12,<2.0" 830 | 831 | [package.extras] 832 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 833 | 834 | [[package]] 835 | name = "pytest-mock" 836 | version = "3.14.0" 837 | description = "Thin-wrapper around the mock package for easier use with pytest" 838 | optional = false 839 | python-versions = ">=3.8" 840 | groups = ["dev"] 841 | files = [ 842 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 843 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 844 | ] 845 | 846 | [package.dependencies] 847 | pytest = ">=6.2.5" 848 | 849 | [package.extras] 850 | dev = ["pre-commit", "pytest-asyncio", "tox"] 851 | 852 | [[package]] 853 | name = "python-dateutil" 854 | version = "2.9.0.post0" 855 | description = "Extensions to the standard Python datetime module" 856 | optional = false 857 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 858 | groups = ["main"] 859 | files = [ 860 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 861 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 862 | ] 863 | 864 | [package.dependencies] 865 | six = ">=1.5" 866 | 867 | [[package]] 868 | name = "python-decouple" 869 | version = "3.8" 870 | description = "Strict separation of settings from code." 871 | optional = false 872 | python-versions = "*" 873 | groups = ["main"] 874 | files = [ 875 | {file = "python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f"}, 876 | {file = "python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66"}, 877 | ] 878 | 879 | [[package]] 880 | name = "pytz" 881 | version = "2025.2" 882 | description = "World timezone definitions, modern and historical" 883 | optional = false 884 | python-versions = "*" 885 | groups = ["main"] 886 | files = [ 887 | {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, 888 | {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, 889 | ] 890 | 891 | [[package]] 892 | name = "referencing" 893 | version = "0.36.2" 894 | description = "JSON Referencing + Python" 895 | optional = false 896 | python-versions = ">=3.9" 897 | groups = ["main"] 898 | files = [ 899 | {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, 900 | {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, 901 | ] 902 | 903 | [package.dependencies] 904 | attrs = ">=22.2.0" 905 | rpds-py = ">=0.7.0" 906 | typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} 907 | 908 | [[package]] 909 | name = "requests" 910 | version = "2.32.3" 911 | description = "Python HTTP for Humans." 912 | optional = false 913 | python-versions = ">=3.8" 914 | groups = ["main"] 915 | files = [ 916 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 917 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 918 | ] 919 | 920 | [package.dependencies] 921 | certifi = ">=2017.4.17" 922 | charset-normalizer = ">=2,<4" 923 | idna = ">=2.5,<4" 924 | urllib3 = ">=1.21.1,<3" 925 | 926 | [package.extras] 927 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 928 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 929 | 930 | [[package]] 931 | name = "rpds-py" 932 | version = "0.24.0" 933 | description = "Python bindings to Rust's persistent data structures (rpds)" 934 | optional = false 935 | python-versions = ">=3.9" 936 | groups = ["main"] 937 | files = [ 938 | {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, 939 | {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, 940 | {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, 941 | {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, 942 | {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, 943 | {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, 944 | {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, 945 | {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, 946 | {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, 947 | {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, 948 | {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, 949 | {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, 950 | {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, 951 | {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, 952 | {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, 953 | {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, 954 | {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, 955 | {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, 956 | {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, 957 | {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, 958 | {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, 959 | {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, 960 | {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, 961 | {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, 962 | {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, 963 | {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, 964 | {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, 965 | {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, 966 | {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, 967 | {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, 968 | {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, 969 | {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, 970 | {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, 971 | {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, 972 | {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, 973 | {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, 974 | {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, 975 | {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, 976 | {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, 977 | {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, 978 | {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, 979 | {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, 980 | {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, 981 | {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, 982 | {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, 983 | {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, 984 | {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, 985 | {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, 986 | {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, 987 | {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, 988 | {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, 989 | {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, 990 | {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, 991 | {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, 992 | {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, 993 | {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, 994 | {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, 995 | {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, 996 | {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, 997 | {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, 998 | {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, 999 | {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, 1000 | {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, 1001 | {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, 1002 | {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, 1003 | {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, 1004 | {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, 1005 | {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, 1006 | {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, 1007 | {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, 1008 | {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, 1009 | {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, 1010 | {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, 1011 | {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, 1012 | {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, 1013 | {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, 1014 | {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, 1015 | {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, 1016 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, 1017 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, 1018 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, 1019 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, 1020 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, 1021 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, 1022 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, 1023 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, 1024 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, 1025 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, 1026 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, 1027 | {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, 1028 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, 1029 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, 1030 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, 1031 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, 1032 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, 1033 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, 1034 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, 1035 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, 1036 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, 1037 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, 1038 | {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, 1039 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, 1040 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, 1041 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, 1042 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, 1043 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, 1044 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, 1045 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, 1046 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, 1047 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, 1048 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, 1049 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, 1050 | {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, 1051 | {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "six" 1056 | version = "1.17.0" 1057 | description = "Python 2 and 3 compatibility utilities" 1058 | optional = false 1059 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1060 | groups = ["main"] 1061 | files = [ 1062 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 1063 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 1064 | ] 1065 | 1066 | [[package]] 1067 | name = "smmap" 1068 | version = "5.0.2" 1069 | description = "A pure Python implementation of a sliding window memory map manager" 1070 | optional = false 1071 | python-versions = ">=3.7" 1072 | groups = ["main"] 1073 | files = [ 1074 | {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, 1075 | {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "streamlit" 1080 | version = "1.44.0" 1081 | description = "A faster way to build and share data apps" 1082 | optional = false 1083 | python-versions = "!=3.9.7,>=3.9" 1084 | groups = ["main"] 1085 | files = [ 1086 | {file = "streamlit-1.44.0-py3-none-any.whl", hash = "sha256:98510d03e53622bba8f0e9f2fd4f1191b3b55e5c7e55abbbaa0289cb9e21cdea"}, 1087 | {file = "streamlit-1.44.0.tar.gz", hash = "sha256:da75933bae94595167f43822dea43fcdde0d747433f7d04989266d78967951bb"}, 1088 | ] 1089 | 1090 | [package.dependencies] 1091 | altair = ">=4.0,<6" 1092 | blinker = ">=1.0.0,<2" 1093 | cachetools = ">=4.0,<6" 1094 | click = ">=7.0,<9" 1095 | gitpython = ">=3.0.7,<3.1.19 || >3.1.19,<4" 1096 | numpy = ">=1.23,<3" 1097 | packaging = ">=20,<25" 1098 | pandas = ">=1.4.0,<3" 1099 | pillow = ">=7.1.0,<12" 1100 | protobuf = ">=3.20,<6" 1101 | pyarrow = ">=7.0" 1102 | pydeck = ">=0.8.0b4,<1" 1103 | requests = ">=2.27,<3" 1104 | tenacity = ">=8.1.0,<10" 1105 | toml = ">=0.10.1,<2" 1106 | tornado = ">=6.0.3,<7" 1107 | typing-extensions = ">=4.4.0,<5" 1108 | watchdog = {version = ">=2.1.5,<7", markers = "platform_system != \"Darwin\""} 1109 | 1110 | [package.extras] 1111 | snowflake = ["snowflake-connector-python (>=3.3.0) ; python_version < \"3.12\"", "snowflake-snowpark-python[modin] (>=1.17.0) ; python_version < \"3.12\""] 1112 | 1113 | [[package]] 1114 | name = "streamlit-aggrid" 1115 | version = "1.1.2" 1116 | description = "Streamlit component implementation of ag-grid" 1117 | optional = false 1118 | python-versions = ">=3.10" 1119 | groups = ["main"] 1120 | files = [ 1121 | {file = "streamlit_aggrid-1.1.2-py3-none-any.whl", hash = "sha256:4603c673bb210ae3f60d9615217f8c57a26f37429596d464f8c2cf3d57125e8c"}, 1122 | {file = "streamlit_aggrid-1.1.2.tar.gz", hash = "sha256:23036ede967dc210565c67f6ff7fcba9545706499c542e59b9807c988f2881e0"}, 1123 | ] 1124 | 1125 | [package.dependencies] 1126 | pandas = ">=1.4.0" 1127 | python-decouple = "*" 1128 | streamlit = ">=1.2" 1129 | 1130 | [[package]] 1131 | name = "tenacity" 1132 | version = "9.0.0" 1133 | description = "Retry code until it succeeds" 1134 | optional = false 1135 | python-versions = ">=3.8" 1136 | groups = ["main"] 1137 | files = [ 1138 | {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, 1139 | {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, 1140 | ] 1141 | 1142 | [package.extras] 1143 | doc = ["reno", "sphinx"] 1144 | test = ["pytest", "tornado (>=4.5)", "typeguard"] 1145 | 1146 | [[package]] 1147 | name = "toml" 1148 | version = "0.10.2" 1149 | description = "Python Library for Tom's Obvious, Minimal Language" 1150 | optional = false 1151 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 1152 | groups = ["main"] 1153 | files = [ 1154 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 1155 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 1156 | ] 1157 | 1158 | [[package]] 1159 | name = "tornado" 1160 | version = "6.4.2" 1161 | description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." 1162 | optional = false 1163 | python-versions = ">=3.8" 1164 | groups = ["main"] 1165 | files = [ 1166 | {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, 1167 | {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, 1168 | {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, 1169 | {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, 1170 | {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, 1171 | {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, 1172 | {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, 1173 | {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, 1174 | {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, 1175 | {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, 1176 | {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, 1177 | ] 1178 | 1179 | [[package]] 1180 | name = "typing-extensions" 1181 | version = "4.13.0" 1182 | description = "Backported and Experimental Type Hints for Python 3.8+" 1183 | optional = false 1184 | python-versions = ">=3.8" 1185 | groups = ["main"] 1186 | files = [ 1187 | {file = "typing_extensions-4.13.0-py3-none-any.whl", hash = "sha256:c8dd92cc0d6425a97c18fbb9d1954e5ff92c1ca881a309c45f06ebc0b79058e5"}, 1188 | {file = "typing_extensions-4.13.0.tar.gz", hash = "sha256:0a4ac55a5820789d87e297727d229866c9650f6521b64206413c4fbada24d95b"}, 1189 | ] 1190 | 1191 | [[package]] 1192 | name = "tzdata" 1193 | version = "2025.2" 1194 | description = "Provider of IANA time zone data" 1195 | optional = false 1196 | python-versions = ">=2" 1197 | groups = ["main"] 1198 | files = [ 1199 | {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, 1200 | {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, 1201 | ] 1202 | 1203 | [[package]] 1204 | name = "urllib3" 1205 | version = "2.3.0" 1206 | description = "HTTP library with thread-safe connection pooling, file post, and more." 1207 | optional = false 1208 | python-versions = ">=3.9" 1209 | groups = ["main"] 1210 | files = [ 1211 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 1212 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 1213 | ] 1214 | 1215 | [package.extras] 1216 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 1217 | h2 = ["h2 (>=4,<5)"] 1218 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 1219 | zstd = ["zstandard (>=0.18.0)"] 1220 | 1221 | [[package]] 1222 | name = "watchdog" 1223 | version = "6.0.0" 1224 | description = "Filesystem events monitoring" 1225 | optional = false 1226 | python-versions = ">=3.9" 1227 | groups = ["main"] 1228 | markers = "platform_system != \"Darwin\"" 1229 | files = [ 1230 | {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, 1231 | {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, 1232 | {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, 1233 | {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, 1234 | {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, 1235 | {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, 1236 | {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, 1237 | {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, 1238 | {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, 1239 | {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, 1240 | {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, 1241 | {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, 1242 | {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, 1243 | {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, 1244 | {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, 1245 | {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, 1246 | {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, 1247 | {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, 1248 | {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, 1249 | {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, 1250 | {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, 1251 | {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, 1252 | {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, 1253 | {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, 1254 | {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, 1255 | {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, 1256 | {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, 1257 | {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, 1258 | {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, 1259 | {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, 1260 | ] 1261 | 1262 | [package.extras] 1263 | watchmedo = ["PyYAML (>=3.10)"] 1264 | 1265 | [metadata] 1266 | lock-version = "2.1" 1267 | python-versions = "^3.12" 1268 | content-hash = "0ae5134672c06204ad817148aed18db29ec8232f255ce2a7f6a791a4d90183d1" 1269 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "excel-to-markdown" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Devin Liu "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.12" 10 | pandas = "^2.2.2" 11 | openpyxl = "^3.1.5" 12 | streamlit = "^1.38.0" 13 | streamlit-aggrid = "^1.0.5" 14 | 15 | [tool.poetry.scripts] 16 | excel-to-markdown = "excel_to_markdown.main:main" 17 | app = "run_streamlit:main" 18 | 19 | 20 | [build-system] 21 | requires = ["poetry-core"] 22 | build-backend = "poetry.core.masonry.api" 23 | 24 | [tool.poetry.dev-dependencies] 25 | pytest = "^7.0" 26 | pytest-mock = "^3.0" 27 | 28 | -------------------------------------------------------------------------------- /run_streamlit.py: -------------------------------------------------------------------------------- 1 | # src/run_streamlit.py 2 | import os 3 | 4 | def main(): 5 | os.system("streamlit run src/app.py") 6 | -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import os 3 | from pathlib import Path 4 | from components.inputs_files_selector import input_files_selector 5 | 6 | 7 | input_files_selector() 8 | 9 | # Get the list of files in the data/output directory 10 | output_dir = Path("data/output") 11 | output_files = [f for f in os.listdir(output_dir) if f.endswith('.md')] 12 | 13 | # Display the list of output files using Streamlit 14 | st.header("Markdown Files in data/output") 15 | if output_files: 16 | for file in output_files: 17 | with open(output_dir / file, 'r') as f: 18 | content = f.read() 19 | st.write(f"- {file}") 20 | else: 21 | st.write("No Markdown files found in the output directory.") 22 | 23 | -------------------------------------------------------------------------------- /src/components/inputs_files_selector.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from pathlib import Path 3 | import os 4 | from urllib.parse import urlencode 5 | 6 | 7 | def input_files_selector(mode="link"): 8 | # Get the list of files in the data/input directory 9 | input_dir = Path("data/input") 10 | input_files = [f for f in os.listdir( 11 | input_dir) if f.endswith(('.xlsx', '.xls'))] 12 | 13 | # Get the default file from query params 14 | default_file = st.query_params.get("file") 15 | 16 | # Display the list of files using Streamlit 17 | st.header("Excel Files in data/input") 18 | if input_files: 19 | for file in input_files: 20 | if mode == "link": 21 | # Create a query string with both file and sheet parameters 22 | query_params = {"file": file} 23 | if "sheet" in st.query_params: 24 | query_params["sheet"] = st.query_params["sheet"] 25 | query_string = urlencode(query_params) 26 | 27 | st.link_button(f"Preview {file}", f"/preview?{query_string}") 28 | elif mode == "select": 29 | # If this file is the default, use a success message instead of a button 30 | if file == default_file: 31 | st.success(f"Selected file: {file}") 32 | else: 33 | if st.button(f"Select {file}"): 34 | st.query_params["file"] = file 35 | st.rerun() # Rerun the app to reflect the change 36 | else: 37 | st.write("No Excel files found in the input directory.") 38 | -------------------------------------------------------------------------------- /src/components/sheet_selector.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from pathlib import Path 3 | import pandas as pd 4 | 5 | 6 | def sheet_selector(): 7 | if "file" in st.query_params: 8 | file_name = st.query_params["file"] 9 | file_path = Path("data/input") / file_name 10 | if file_path.exists(): 11 | wb = pd.read_excel(file_path, sheet_name=None) 12 | sheet_names = list(wb.keys()) 13 | 14 | # Get the default sheet from query params 15 | default_sheet = st.query_params.get("sheet") 16 | 17 | # Find the index of the default sheet 18 | default_index = 0 19 | if default_sheet in sheet_names: 20 | default_index = sheet_names.index(default_sheet) 21 | 22 | selected_sheet = st.selectbox( 23 | "Select a sheet", sheet_names, index=default_index) 24 | 25 | if selected_sheet: 26 | st.query_params["sheet"] = selected_sheet 27 | st.success(f"Selected sheet: {selected_sheet}") 28 | else: 29 | st.warning( 30 | "No file selected. Please provide a file parameter in the URL.") 31 | 32 | # Usage example (can be placed in your main Streamlit app file): 33 | # sheet_selector() 34 | -------------------------------------------------------------------------------- /src/pages/column_to_doc.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from utils import load_excel_file 3 | from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode, DataReturnMode 4 | import pandas as pd 5 | from components.inputs_files_selector import input_files_selector 6 | from components.sheet_selector import sheet_selector 7 | import io 8 | import os 9 | import re 10 | from pathlib import Path 11 | 12 | 13 | def column_to_doc(): 14 | file_name = st.query_params.get("file") 15 | sheet_name = st.query_params.get("sheet") 16 | 17 | wb = load_excel_file(file_name) 18 | if wb is None: 19 | st.error("File not found.") 20 | return 21 | 22 | if sheet_name not in wb: 23 | st.error(f"Sheet '{sheet_name}' not found in the file.") 24 | return 25 | 26 | df = wb[sheet_name] 27 | st.subheader(f"Column to Document - {sheet_name}") 28 | 29 | # Display the grid 30 | gb = GridOptionsBuilder.from_dataframe(df) 31 | gb.configure_default_column(editable=True) # Make all columns editable 32 | grid_options = gb.build() 33 | 34 | grid_response = AgGrid( 35 | df, 36 | gridOptions=grid_options, 37 | data_return_mode=DataReturnMode.FILTERED_AND_SORTED, 38 | update_mode=GridUpdateMode.MODEL_CHANGED, 39 | fit_columns_on_grid_load=True, 40 | allow_unsafe_jscode=True, 41 | reload_data=False, 42 | key=f"{sheet_name}_aggrid", 43 | editable=True, # Enable editing for the entire grid 44 | ) 45 | 46 | # Update the dataframe with edited values 47 | df = grid_response['data'] 48 | 49 | # row selection 50 | rows = df.index.to_list() 51 | row_selector = st.selectbox( 52 | "Select the row to start generating the markdown from", rows) 53 | 54 | # Column selection 55 | columns = df.columns.tolist() 56 | question_column = st.selectbox( 57 | "Select the question column", columns) 58 | answer_column = st.selectbox( 59 | "Select the answer column", columns) 60 | 61 | # Handle both integer and label-based indices 62 | if isinstance(df.index, pd.RangeIndex): 63 | # If index is a default RangeIndex, use the selected value directly 64 | row_index = row_selector 65 | else: 66 | # If index is not a default RangeIndex, find the integer location 67 | row_index = df.index.get_loc(row_selector) 68 | 69 | first_answer_column_value = df.iloc[row_index][answer_column] 70 | default_file_name = f"{first_answer_column_value}_qa" 71 | 72 | file_name_input = st.text_input( 73 | "Enter the file name for the markdown document", value=default_file_name) 74 | markdown_file_name = file_name_input + ".md" 75 | 76 | # make a smaller dataframe that removes the columns before the answer column 77 | answer_columns = df.columns[df.columns.get_loc(answer_column):] 78 | 79 | # create a button to iterate through the columns and generate a markdown file for each column 80 | 81 | if st.button(f"Generate {len(answer_columns)} markdown files"): 82 | if question_column == answer_column: 83 | st.error("Please select different columns for questions and answers.") 84 | else: 85 | for column in answer_columns: 86 | # get the value of the row at the row_index for the current column 87 | file_name = df.iloc[row_index][column] 88 | file_name = f"{file_name}_qa.md" 89 | markdown = generate_markdown( 90 | # Convert row_index to int 91 | df, question_column, column, row_index) 92 | 93 | # download the markdown file 94 | 95 | output_dir = "./data/output/" 96 | 97 | # sanitize the file name to remove special characters using a library 98 | file_name = sanitize_filename(file_name) 99 | 100 | full_file_name = output_dir + file_name 101 | 102 | # check if directory exists 103 | if not os.path.exists(output_dir): 104 | os.makedirs(output_dir) 105 | 106 | # check if file exists 107 | # create the file if it doesn't exist 108 | if not os.path.exists(full_file_name): 109 | with open(full_file_name, "w") as f: 110 | f.write(markdown) 111 | else: 112 | st.error(f"File {file_name} already exists.") 113 | 114 | 115 | if st.button("Generate Markdown Preview"): 116 | if question_column == answer_column: 117 | st.error("Please select different columns for questions and answers.") 118 | else: 119 | 120 | # fill NA values in the answer column with empty strings 121 | df[answer_column] = df[answer_column].fillna("") 122 | 123 | markdown = generate_markdown( 124 | df, question_column, answer_column, row_selector) 125 | st.download_button( 126 | label="Download Markdown", 127 | data=markdown, 128 | file_name=markdown_file_name, 129 | mime="text/markdown" 130 | ) 131 | st.markdown("### Markdown Preview") 132 | st.markdown(markdown) 133 | 134 | 135 | def generate_markdown(df, question_column, answer_column, row_selector): 136 | markdown = "" 137 | for _, row in df.iterrows(): 138 | if int(_) < int(row_selector): 139 | continue 140 | question = row[question_column] 141 | answer = row[answer_column] 142 | if pd.notna(question) and pd.notna(answer): 143 | markdown += f"## {question}\n\n{answer}\n\n" 144 | return markdown 145 | 146 | 147 | def sanitize_filename(filename): 148 | # Remove or replace special characters 149 | sanitized = re.sub(r'[^\w\-_\. ]', '', filename) 150 | # Replace spaces with underscores 151 | sanitized = sanitized.replace(' ', '_') 152 | # Ensure the filename is not empty after sanitization 153 | return sanitized or 'untitled' 154 | 155 | 156 | if __name__ == "__main__": 157 | input_files_selector(mode="select") 158 | if "file" in st.query_params: 159 | sheet_selector() 160 | if "file" in st.query_params and "sheet" in st.query_params: 161 | column_to_doc() 162 | -------------------------------------------------------------------------------- /src/pages/edit.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from utils import load_excel_file 3 | from excel_to_markdown.markdown_generator import dataframe_to_markdown 4 | from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode, DataReturnMode 5 | import pandas as pd 6 | 7 | 8 | def edit_excel(): 9 | file_name = st.query_params.get("file") 10 | sheet_name = st.query_params.get("sheet") 11 | 12 | wb = load_excel_file(file_name) 13 | if wb is None: 14 | st.error("File not found.") 15 | return 16 | 17 | for current_sheet, df in wb.items(): 18 | if sheet_name and current_sheet != sheet_name: 19 | continue 20 | 21 | st.subheader(f"{current_sheet}") 22 | 23 | # Initialize session state keys 24 | start_row_key = f"{current_sheet}_start_row" 25 | end_row_key = f"{current_sheet}_end_row" 26 | start_col_key = f"{current_sheet}_start_col" 27 | end_col_key = f"{current_sheet}_end_col" 28 | 29 | # Initialize selection keys if not present 30 | if start_row_key not in st.session_state: 31 | st.session_state[start_row_key] = 0 32 | if end_row_key not in st.session_state: 33 | st.session_state[end_row_key] = len(df) - 1 34 | if start_col_key not in st.session_state: 35 | st.session_state[start_col_key] = 0 36 | if end_col_key not in st.session_state: 37 | st.session_state[end_col_key] = len(df.columns) - 1 38 | 39 | # Build grid options with selection 40 | gb = GridOptionsBuilder.from_dataframe(df) 41 | gb.configure_selection(selection_mode='multiple', use_checkbox=True) 42 | grid_options = gb.build() 43 | 44 | # Display the grid 45 | grid_response = AgGrid( 46 | df, 47 | gridOptions=grid_options, 48 | data_return_mode=DataReturnMode.FILTERED_AND_SORTED, 49 | update_mode=GridUpdateMode.SELECTION_CHANGED | GridUpdateMode.VALUE_CHANGED, 50 | fit_columns_on_grid_load=True, 51 | allow_unsafe_jscode=True, 52 | reload_data=False, 53 | key=f"{current_sheet}_aggrid_{st.session_state.get('aggrid_key', 0)}" 54 | ) 55 | 56 | selected_rows = grid_response['selected_rows'] 57 | 58 | # Initialize selected_indices 59 | selected_indices = [] 60 | 61 | if isinstance(selected_rows, pd.DataFrame): 62 | if not selected_rows.empty: 63 | selected_indices = selected_rows.index.tolist() 64 | elif isinstance(selected_rows, list): 65 | if len(selected_rows) > 0: 66 | selected_indices = [ 67 | int(row['_selectedRowNodeInfo']['nodeRowIndex']) for row in selected_rows] 68 | else: 69 | selected_indices = [] 70 | 71 | # Synchronize selections to session state 72 | selection_changed = False 73 | if selected_indices: 74 | new_start_row = min(selected_indices) 75 | new_end_row = max(selected_indices) 76 | if (new_start_row != st.session_state[start_row_key] or 77 | new_end_row != st.session_state[end_row_key]): 78 | st.session_state[start_row_key] = new_start_row 79 | st.session_state[end_row_key] = new_end_row 80 | selection_changed = True 81 | else: 82 | if st.session_state[start_row_key] != 0 or st.session_state[end_row_key] != len(df) - 1: 83 | st.session_state[start_row_key] = 0 84 | st.session_state[end_row_key] = len(df) - 1 85 | selection_changed = True 86 | 87 | # Retrieve current values from session state 88 | start_row = st.session_state[start_row_key] 89 | end_row = st.session_state[end_row_key] 90 | start_col = st.session_state[start_col_key] 91 | end_col = st.session_state[end_col_key] 92 | 93 | # Display the current selection 94 | st.write(f"Selected range: Rows {start_row} to {end_row}, Columns {start_col} to {end_col}") 95 | 96 | col1, col2 = st.columns(2) 97 | with col1: 98 | start_row_input = st.number_input( 99 | f"Start Row for {current_sheet}", 100 | min_value=0, 101 | max_value=len(df)-1, 102 | value=int(start_row), 103 | key=f"{start_row_key}_input" 104 | ) 105 | end_row_input = st.number_input( 106 | f"End Row for {current_sheet}", 107 | min_value=int(start_row_input), 108 | max_value=len(df)-1, 109 | value=int(end_row), 110 | key=f"{end_row_key}_input" 111 | ) 112 | with col2: 113 | start_col_input = st.number_input( 114 | f"Start Column for {current_sheet}", 115 | min_value=0, 116 | max_value=len(df.columns)-1, 117 | value=int(start_col), 118 | key=f"{start_col_key}_input" 119 | ) 120 | end_col_input = st.number_input( 121 | f"End Column for {current_sheet}", 122 | min_value=int(start_col_input), 123 | max_value=len(df.columns)-1, 124 | value=int(end_col), 125 | key=f"{end_col_key}_input" 126 | ) 127 | 128 | # # Update session state if number inputs changed 129 | # inputs_changed = False 130 | # if (start_row_input != st.session_state[start_row_key] or 131 | # end_row_input != st.session_state[end_row_key]): 132 | # st.session_state[start_row_key] = start_row_input 133 | # st.session_state[end_row_key] = end_row_input 134 | # inputs_changed = True 135 | 136 | # if inputs_changed or selection_changed: 137 | # pre_selected_indices = list(range( 138 | # int(st.session_state[start_row_key]), int(st.session_state[end_row_key]) + 1)) 139 | 140 | # gb = GridOptionsBuilder.from_dataframe(df) 141 | # gb.configure_selection( 142 | # selection_mode='multiple', 143 | # use_checkbox=True, 144 | # pre_selected_rows=pre_selected_indices 145 | # ) 146 | # grid_options = gb.build() 147 | 148 | # st.session_state['aggrid_key'] = st.session_state.get('aggrid_key', 0) + 1 149 | 150 | # # Re-render grid with updated selection 151 | # grid_response = AgGrid( 152 | # df, 153 | # gridOptions=grid_options, 154 | # data_return_mode=DataReturnMode.FILTERED_AND_SORTED, 155 | # update_mode=GridUpdateMode.SELECTION_CHANGED | GridUpdateMode.VALUE_CHANGED, 156 | # fit_columns_on_grid_load=True, 157 | # allow_unsafe_jscode=True, 158 | # reload_data=True, 159 | # key=f"{current_sheet}_aggrid_{st.session_state['aggrid_key']}" 160 | # ) 161 | 162 | if st.button(f"View markdown preview of {current_sheet}"): 163 | selected_df = df.iloc[ 164 | int(st.session_state[start_row_key]):int(st.session_state[end_row_key])+1, 165 | int(st.session_state[start_col_key]):int(st.session_state[end_col_key])+1 166 | ] 167 | markdown = dataframe_to_markdown(selected_df) 168 | st.markdown(markdown) 169 | st.download_button( 170 | label="Download Markdown", 171 | data=markdown, 172 | file_name=f"{current_sheet}_selected.md", 173 | mime="text/markdown" 174 | ) 175 | 176 | if sheet_name: 177 | break 178 | 179 | 180 | if __name__ == "__main__": 181 | edit_excel() 182 | -------------------------------------------------------------------------------- /src/pages/preview.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from pathlib import Path 3 | import pandas as pd 4 | from excel_to_markdown.markdown_generator import dataframe_to_markdown 5 | 6 | st.set_page_config(layout="wide") 7 | 8 | 9 | def preview_excel(): 10 | file_name = st.query_params.get("file") 11 | if file_name: 12 | file_path = Path("data/input") / file_name 13 | if file_path.exists(): 14 | wb = pd.read_excel(file_path, sheet_name=None) 15 | 16 | for sheet_name, df in wb.items(): 17 | st.subheader(f"{sheet_name}") 18 | 19 | # Add row and column range selectors 20 | col1, col2 = st.columns(2) 21 | with col1: 22 | start_row = st.number_input( 23 | f"Start Row for {sheet_name}", 24 | min_value=0, 25 | max_value=len(df) - 1, 26 | value=0, 27 | ) 28 | end_row = st.number_input( 29 | f"End Row for {sheet_name}", 30 | min_value=start_row, 31 | max_value=len(df) - 1, 32 | value=len(df) - 1, 33 | ) 34 | with col2: 35 | start_col = st.number_input( 36 | f"Start Column for {sheet_name}", 37 | min_value=0, 38 | max_value=len(df.columns) - 1, 39 | value=0, 40 | ) 41 | end_col = st.number_input( 42 | f"End Column for {sheet_name}", 43 | min_value=start_col, 44 | max_value=len(df.columns) - 1, 45 | value=len(df.columns) - 1, 46 | ) 47 | 48 | # Add button to edit selected range 49 | # if st.button(f"Edit selected range of {sheet_name}"): 50 | # edit_url = f"/edit?file={file_name}&sheet={sheet_name}&start_row={start_row}&end_row={end_row}&start_col={start_col}&end_col={end_col}" 51 | # st.switch_page("pages/edit.py") 52 | edit_url = f"/edit?file={file_name}&sheet={sheet_name}&start_row={start_row}&end_row={end_row}&start_col={start_col}&end_col={end_col}" 53 | st.link_button(f"Edit selected range of {sheet_name}", edit_url) 54 | 55 | # Display the entire DataFrame 56 | st.dataframe(df, use_container_width=True) 57 | 58 | else: 59 | st.error("File not found.") 60 | else: 61 | st.error("No file specified.") 62 | 63 | 64 | if __name__ == "__main__": 65 | preview_excel() 66 | -------------------------------------------------------------------------------- /src/pages/row_to_doc.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from utils import get_file_and_sheet, sanitize_filename 3 | from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode, DataReturnMode 4 | import pandas as pd 5 | from components.inputs_files_selector import input_files_selector 6 | from components.sheet_selector import sheet_selector 7 | import io 8 | import os 9 | from pathlib import Path 10 | 11 | 12 | def create_aggrid(df, sheet_name, selection_mode='multiple'): 13 | gb = GridOptionsBuilder.from_dataframe(df) 14 | gb.configure_default_column(editable=True) # Make all columns editable 15 | gb.configure_selection(selection_mode=selection_mode, use_checkbox=True) 16 | 17 | grid_options = gb.build() 18 | 19 | grid_response = AgGrid( 20 | df, 21 | gridOptions=grid_options, 22 | data_return_mode=DataReturnMode.FILTERED_AND_SORTED, 23 | update_mode=GridUpdateMode.MODEL_CHANGED, 24 | fit_columns_on_grid_load=True, 25 | allow_unsafe_jscode=True, 26 | reload_data=False, 27 | key=f"{sheet_name}_aggrid", 28 | editable=True, # Enable editing for the entire grid 29 | ) 30 | 31 | return grid_response 32 | 33 | 34 | def row_to_doc(): 35 | df, sheet_name = get_file_and_sheet() 36 | st.subheader(f"Row to Document - {sheet_name}") 37 | 38 | grid_response = create_aggrid(df, sheet_name) 39 | 40 | df = grid_response['data'] 41 | 42 | selected_rows_df = None 43 | 44 | # if a row is selected, is not None, and is not empty, show the row 45 | if grid_response['selected_rows'] is not None and not pd.DataFrame(grid_response['selected_rows']).empty: 46 | # make a df from the selected rows, with the first row as the header 47 | # the first column as the index 48 | selected_rows_df = pd.DataFrame(grid_response['selected_rows']) 49 | selected_rows_df.set_index(selected_rows_df.columns[0], inplace=True) 50 | 51 | pinned_columns = [ 52 | column for column in grid_response['columns_state'] if column.get('pinned') is not None] 53 | 54 | if pinned_columns: # Check if there are pinned columns 55 | # Get the ID of the first pinned column 56 | first_pinned_column = pinned_columns[0]['colId'] 57 | 58 | # get the location of the first pinned column 59 | first_index = selected_rows_df.columns.get_loc(first_pinned_column) 60 | 61 | # Keep columns from the first pinned column onward 62 | selected_rows_df = selected_rows_df.iloc[:, first_index:] 63 | 64 | st.write(f"First pinned column: {first_pinned_column}") 65 | else: 66 | st.warning("Please pin at least one column.") 67 | return 68 | 69 | st.write(selected_rows_df) 70 | 71 | else: 72 | st.warning("Please select at least one row and pin at least one column.") 73 | return 74 | 75 | # Column selection 76 | columns = selected_rows_df.columns.tolist() 77 | 78 | # Select the starting column for answers 79 | start_column = st.selectbox( 80 | "Select the starting column for answers", columns) 81 | 82 | # Determine valid answer columns based on the selected start column 83 | start_index = columns.index(start_column) 84 | answer_columns = columns[start_index:] 85 | 86 | # Row selection for answer labels 87 | answer_rows = selected_rows_df.index.tolist() 88 | answer_row_selector = st.selectbox( 89 | "Select the row for the answer labels", answer_rows) 90 | 91 | answer_row_index = selected_rows_df.index.get_loc(answer_row_selector) 92 | 93 | default_file_name = f"{sheet_name}_document" 94 | 95 | file_name_input = st.text_input( 96 | "Enter the file name for the markdown document", value=default_file_name) 97 | markdown_file_name = file_name_input + ".md" 98 | 99 | if st.button("Generate Markdown Preview"): 100 | markdown = "" 101 | for idx in range(answer_row_index + 1, len(selected_rows_df)): 102 | row = selected_rows_df.iloc[idx] 103 | question = row[first_pinned_column] 104 | answers = row[answer_columns] 105 | answer_labels = selected_rows_df.loc[answer_row_selector, 106 | answer_columns] 107 | markdown += generate_markdown(question, answers, answer_labels) 108 | 109 | st.download_button( 110 | label="Download Markdown", 111 | data=markdown, 112 | file_name=sanitize_filename(markdown_file_name), 113 | mime="text/markdown" 114 | ) 115 | st.markdown("### Markdown Preview") 116 | st.markdown(markdown) 117 | 118 | 119 | def generate_markdown(question, answers, answer_labels): 120 | markdown = f"## {question}\n\n" 121 | for label, answer in zip(answer_labels, answers): 122 | if pd.notna(answer): 123 | markdown += f"**{label}**: {answer}\n\n" 124 | return markdown 125 | 126 | 127 | if __name__ == "__main__": 128 | input_files_selector(mode="select") 129 | if "file" in st.query_params: 130 | sheet_selector() 131 | if "file" in st.query_params and "sheet" in st.query_params: 132 | row_to_doc() 133 | -------------------------------------------------------------------------------- /src/pages/select_range_to_markdown.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode, DataReturnMode 3 | import pandas as pd 4 | from components.inputs_files_selector import input_files_selector 5 | from components.sheet_selector import sheet_selector 6 | from utils import get_file_and_sheet, sanitize_filename 7 | 8 | 9 | def select_range_to_markdown(): 10 | st.title("Select Range to Markdown") 11 | 12 | st.text("Select rows where values exist") 13 | 14 | df, sheet_name = get_file_and_sheet() 15 | 16 | # Use the first row as values 17 | df = df.reset_index(drop=True) 18 | # Convert column names to strings 19 | df.columns = [str(i) for i in range(len(df.columns))] 20 | 21 | st.subheader(f"Range to Document - {sheet_name}") 22 | 23 | grid_response = create_aggrid(df, sheet_name) 24 | 25 | selected_rows = grid_response['selected_rows'] 26 | 27 | default_file_name = f"{sheet_name}_document" 28 | 29 | file_name_input = st.text_input( 30 | "Enter the file name for the markdown document", value=default_file_name) 31 | 32 | if selected_rows is not None and len(selected_rows) > 0: 33 | selected_rows_df = pd.DataFrame(selected_rows) 34 | st.write("Selected Rows:") 35 | st.dataframe(selected_rows_df) 36 | 37 | if st.button("Generate Markdown"): 38 | 39 | markdown_file_name = file_name_input + ".md" 40 | markdown = generate_markdown(selected_rows_df, file_name_input) 41 | st.markdown("### Markdown Preview") 42 | st.markdown(markdown) 43 | 44 | st.download_button( 45 | label="Download Markdown", 46 | data=markdown, 47 | file_name=sanitize_filename(markdown_file_name), 48 | mime="text/markdown" 49 | ) 50 | else: 51 | st.info("Please select at least one row to generate Markdown.") 52 | 53 | 54 | def generate_markdown(selected_rows_df, title=''): 55 | markdown = "" 56 | if title: 57 | markdown += f"## {title}\n\n" 58 | for index, row in selected_rows_df.iterrows(): 59 | for col_name, value in row.items(): 60 | if pd.notna(value): 61 | markdown += f"{value}\n\n" 62 | return markdown 63 | 64 | 65 | def create_aggrid(df, sheet_name, selection_mode='multiple'): 66 | # Ensure column names are strings 67 | df.columns = df.columns.astype(str) 68 | 69 | gb = GridOptionsBuilder.from_dataframe(df) 70 | gb.configure_default_column(editable=True) 71 | gb.configure_selection(selection_mode=selection_mode, use_checkbox=True) 72 | 73 | grid_options = gb.build() 74 | 75 | grid_response = AgGrid( 76 | df, # Include all rows in the grid 77 | gridOptions=grid_options, 78 | data_return_mode=DataReturnMode.FILTERED_AND_SORTED, 79 | update_mode=GridUpdateMode.MODEL_CHANGED, 80 | fit_columns_on_grid_load=True, 81 | # Use sheet_name in the key to make it unique 82 | key=f"aggrid_{sheet_name}", 83 | editable=True, 84 | ) 85 | 86 | return grid_response 87 | 88 | 89 | if __name__ == "__main__": 90 | input_files_selector(mode="select") 91 | if "file" in st.query_params: 92 | sheet_selector() 93 | if "file" in st.query_params and "sheet" in st.query_params: 94 | select_range_to_markdown() 95 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | import pandas as pd 3 | from pathlib import Path 4 | import re 5 | 6 | 7 | def load_excel_file(file_name): 8 | file_path = Path("data/input") / file_name 9 | if file_path.exists(): 10 | return pd.read_excel(file_path, sheet_name=None) 11 | return None 12 | 13 | 14 | def get_selected_range(df, sheet_name, selected_rows, selected_columns): 15 | start_row = min(selected_rows) if selected_rows else 0 16 | end_row = max(selected_rows) if selected_rows else len(df) - 1 17 | start_col = min(selected_columns) if selected_columns else 0 18 | end_col = max(selected_columns) if selected_columns else len( 19 | df.columns) - 1 20 | return start_row, end_row, start_col, end_col 21 | 22 | 23 | def get_file_and_sheet(): 24 | file_name = st.query_params.get("file") 25 | sheet_name = st.query_params.get("sheet") 26 | 27 | wb = load_excel_file(file_name) 28 | if wb is None: 29 | st.error("File not found.") 30 | return None, None 31 | 32 | if sheet_name not in wb: 33 | st.error(f"Sheet '{sheet_name}' not found in the file.") 34 | return None, None 35 | 36 | df = wb[sheet_name] 37 | return df, sheet_name 38 | 39 | 40 | def sanitize_filename(filename): 41 | # Remove or replace special characters 42 | sanitized = re.sub(r'[^\w\-_\. ]', '', filename) 43 | # Replace spaces with underscores 44 | sanitized = sanitized.replace(' ', '_') 45 | # Ensure the filename is not empty after sanitization 46 | return sanitized or 'untitled' 47 | -------------------------------------------------------------------------------- /tests/test_detector.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pandas as pd 3 | from excel_to_markdown.detector import detect_table_start 4 | 5 | 6 | class TestDetector(unittest.TestCase): 7 | def test_detect_table_start_success(self): 8 | data = { 9 | 'A': [None, 'Header1', 'Data1'], 10 | 'B': [None, 'Header2', 'Data2'], 11 | 'C': [None, 'Header3', 'Data3'] 12 | } 13 | df = pd.DataFrame(data) 14 | expected_row = 1 15 | result = detect_table_start(df) 16 | self.assertEqual(result, expected_row) 17 | 18 | def test_detect_table_start_failure(self): 19 | data = { 20 | 'A': [None, None, None], 21 | 'B': [None, None, None], 22 | 'C': [None, None, None], 23 | } 24 | df = pd.DataFrame(data) 25 | result = detect_table_start(df) 26 | self.assertIsNone(result) 27 | 28 | def test_detect_table_start_partial_fill(self): 29 | data = { 30 | 'A': [None, None, None], 31 | 'B': [None, 'Header1', 'Row1'], 32 | 'C': [None, 'Header2', 'Row2'], 33 | 'D': [None, 'Header3', 'Row3'], 34 | 'E': [None, None, None] 35 | } 36 | expected_row = 1 37 | df = pd.DataFrame(data) 38 | result = detect_table_start(df) 39 | self.assertEqual(result, expected_row) 40 | 41 | def test_detect_table_start_with_none_first_row(self): 42 | data = { 43 | 'A': [None, 'Name', 'Alice', 'Bob'], 44 | 'B': [None, 'Age', 30, 25], 45 | 'C': [None, 'City', 'New York', 'Los Angeles'] 46 | } 47 | df = pd.DataFrame(data) 48 | expected_row = 1 49 | result = detect_table_start(df) 50 | self.assertEqual(result, expected_row) 51 | 52 | def test_detect_table_start_with_second_row_start(self): 53 | data = { 54 | 'A': [None, None, 'Name', 'Alice', 'Bob'], 55 | 'B': [None, None, 'Age', 30, 25], 56 | 'C': [None, None, 'City', 'New York', 'Los Angeles'] 57 | } 58 | df = pd.DataFrame(data) 59 | expected_row = 2 60 | result = detect_table_start(df) 61 | self.assertEqual(result, expected_row) 62 | 63 | # test for this case 64 | 65 | # # Create sample data for two sheets 66 | # sheet1_data = { 67 | # 'A': [None, 'Name', 'Alice', 'Bob'], 68 | # 'B': [None, 'Age', 30, 25], 69 | # 'C': [None, 'City', 'New York', 'Los Angeles'] 70 | # } 71 | 72 | # finds Name, Age, City 73 | 74 | 75 | if __name__ == '__main__': 76 | unittest.main() 77 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pandas as pd 3 | import tempfile 4 | from pathlib import Path 5 | from unittest import mock 6 | from excel_to_markdown.main import process_file, excel_to_markdown 7 | from excel_to_markdown.utils import create_output_filename 8 | 9 | 10 | @pytest.fixture 11 | def sample_excel_file(): 12 | # Create a temporary Excel file with multiple sheets 13 | with tempfile.TemporaryDirectory() as tmpdir: 14 | tmpdir_path = Path(tmpdir) 15 | excel_path = tmpdir_path / "test_excel.xlsx" 16 | 17 | # Create sample data for two sheets 18 | sheet1_data = { 19 | 'A': [None, 'Name', 'Alice', 'Bob'], 20 | 'B': [None, 'Age', 30, 25], 21 | 'C': [None, 'City', 'New York', 'Los Angeles'] 22 | } 23 | sheet2_data = { 24 | 'A': [None, None, 'Product', 'Item1'], 25 | 'B': [None, None, 'Price', 99.99], 26 | 'C': [None, None, 'Stock', 50] 27 | } 28 | 29 | # Create DataFrames 30 | df1 = pd.DataFrame(sheet1_data) 31 | df2 = pd.DataFrame(sheet2_data) 32 | 33 | # Write to Excel with two sheets 34 | with pd.ExcelWriter(excel_path, engine='openpyxl') as writer: 35 | df1.to_excel(writer, sheet_name='Sheet1', 36 | index=False, header=False) 37 | df2.to_excel(writer, sheet_name='Sheet2', 38 | index=False, header=False) 39 | 40 | yield excel_path # Provide the path to the test 41 | 42 | 43 | @pytest.fixture 44 | def output_directory(): 45 | # Create a temporary directory for output 46 | with tempfile.TemporaryDirectory() as tmpdir: 47 | yield Path(tmpdir) 48 | 49 | 50 | def test_process_file_auto_detect(sample_excel_file, output_directory, mocker): 51 | # Mock the input function to avoid interactive prompts 52 | # Since automatic detection should succeed for Sheet1 53 | mocker.patch('builtins.input') 54 | 55 | # Run the process_file function 56 | process_file(sample_excel_file, output_directory) 57 | 58 | # Check if Markdown files are created 59 | expected_files = [ 60 | output_directory / "test_excel_Sheet1.md", 61 | output_directory / "test_excel_Sheet2.md" 62 | ] 63 | 64 | for file in expected_files: 65 | assert file.exists() 66 | 67 | # Verify content of Sheet1's Markdown file 68 | sheet1_md = output_directory / "test_excel_Sheet1.md" 69 | with open(sheet1_md, 'r', encoding='utf-8') as f: 70 | content = f.read() 71 | 72 | expected_content_sheet1 = ( 73 | "| Name | Age | City |\n" 74 | "| --- | --- | --- |\n" 75 | "| Alice | 30 | New York |\n" 76 | "| Bob | 25 | Los Angeles |\n" 77 | ) 78 | 79 | assert content == expected_content_sheet1 80 | 81 | # Since Sheet2 lacks a fully populated header row, it should prompt for input 82 | # However, since we mocked 'input' without side effects, it may result in incomplete processing 83 | # Adjust the test accordingly or split into separate tests 84 | 85 | 86 | def test_excel_to_markdown_manual_input(sample_excel_file, mocker): 87 | # Mock user input for Sheet2 88 | inputs = iter(['3', 'A:C']) # Header row 3, columns A to C 89 | mocker.patch('builtins.input', lambda _: next(inputs)) 90 | 91 | # Process Sheet2 manually 92 | markdown = excel_to_markdown(sample_excel_file, 'Sheet2') 93 | 94 | expected_markdown = ( 95 | "| Product | Price | Stock |\n" 96 | "| --- | --- | --- |\n" 97 | "| Item1 | 99.99 | 50 |\n" 98 | ) 99 | 100 | assert markdown == expected_markdown 101 | 102 | 103 | def test_create_output_filename(): 104 | # Test the utility function for creating output filenames 105 | from excel_to_markdown.utils import sanitize_sheet_name, create_output_filename 106 | 107 | input_file = Path("/path/to/report.xlsx") 108 | sheet_name = "Sales Data" 109 | output_dir = Path("/path/to/output") 110 | 111 | expected_filename = output_dir / "report_Sales_Data.md" 112 | result = create_output_filename(input_file, sheet_name, output_dir) 113 | assert result == expected_filename 114 | 115 | 116 | def test_main_function(sample_excel_file, output_directory, mocker): 117 | # Mock command-line arguments and user inputs 118 | mocker.patch('sys.argv', ['excel_to_markdown.main', str( 119 | sample_excel_file.parent), str(output_directory)]) 120 | mocker.patch('builtins.input', side_effect=['3', 'A:C']) # For Sheet2 121 | 122 | # Run the main function 123 | with mock.patch('excel_to_markdown.main.process_file') as mock_process_file: 124 | from excel_to_markdown.main import main 125 | main() 126 | 127 | # Ensure process_file was called for the Excel file 128 | mock_process_file.assert_called_once_with( 129 | sample_excel_file, output_directory) 130 | -------------------------------------------------------------------------------- /tests/test_markdown_generator.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pandas as pd 3 | from excel_to_markdown.markdown_generator import dataframe_to_markdown 4 | 5 | 6 | class TestMarkdownGenerator(unittest.TestCase): 7 | def test_dataframe_to_markdown_basic(self): 8 | # Test with a basic DataFrame 9 | data = { 10 | 'Name': ['Alice', 'Bob'], 11 | 'Age': [30, 25], 12 | 'City': ['New York', 'Los Angeles'] 13 | } 14 | df = pd.DataFrame(data) 15 | expected_markdown = ( 16 | "| Name | Age | City |\n" 17 | "| --- | --- | --- |\n" 18 | "| Alice | 30 | New York |\n" 19 | "| Bob | 25 | Los Angeles |\n" 20 | ) 21 | result = dataframe_to_markdown(df) 22 | self.assertEqual(result, expected_markdown) 23 | 24 | def test_dataframe_to_markdown_with_missing_values(self): 25 | # Test DataFrame with missing values 26 | data = { 27 | 'Name': ['Alice', 'Bob', 'Charlie'], 28 | 'Age': [30, None, 35], 29 | 'City': ['New York', 'Los Angeles', None] 30 | } 31 | df = pd.DataFrame(data) 32 | expected_markdown = ( 33 | "| Name | Age | City |\n" 34 | "| --- | --- | --- |\n" 35 | "| Alice | 30.0 | New York |\n" 36 | "| Bob | | Los Angeles |\n" 37 | "| Charlie | 35.0 | |\n" 38 | ) 39 | result = dataframe_to_markdown(df) 40 | self.assertEqual(result, expected_markdown) 41 | 42 | def test_dataframe_to_markdown_empty_dataframe(self): 43 | # Test with an empty DataFrame 44 | df = pd.DataFrame() 45 | expected_markdown = "" 46 | result = dataframe_to_markdown(df) 47 | self.assertEqual(result, expected_markdown) 48 | 49 | def test_dataframe_to_markdown_no_columns(self): 50 | # Test DataFrame with no columns but with rows 51 | df = pd.DataFrame([[] for _ in range(3)]) 52 | expected_markdown = "" 53 | result = dataframe_to_markdown(df) 54 | self.assertEqual(result, expected_markdown) 55 | 56 | def test_dataframe_to_markdown_single_row(self): 57 | # Test DataFrame with a single row 58 | data = { 59 | 'Product': ['Laptop'], 60 | 'Price': [999.99], 61 | 'Stock': [50] 62 | } 63 | df = pd.DataFrame(data) 64 | expected_markdown = ( 65 | "| Product | Price | Stock |\n" 66 | "| --- | --- | --- |\n" 67 | "| Laptop | 999.99 | 50 |\n" 68 | ) 69 | result = dataframe_to_markdown(df) 70 | self.assertEqual(result, expected_markdown) 71 | 72 | def test_dataframe_to_markdown_single_column(self): 73 | # Test DataFrame with a single column 74 | data = { 75 | 'Username': ['user1', 'user2', 'user3'] 76 | } 77 | df = pd.DataFrame(data) 78 | expected_markdown = ( 79 | "| Username |\n" 80 | "| --- |\n" 81 | "| user1 |\n" 82 | "| user2 |\n" 83 | "| user3 |\n" 84 | ) 85 | result = dataframe_to_markdown(df) 86 | self.assertEqual(result, expected_markdown) 87 | 88 | def test_dataframe_to_markdown_numeric_data(self): 89 | # Test DataFrame with numeric data 90 | data = { 91 | 'ID': [1, 2, 3], 92 | 'Score': [85.5, 92.0, 78.25] 93 | } 94 | df = pd.DataFrame(data) 95 | expected_markdown = ( 96 | "| ID | Score |\n" 97 | "| --- | --- |\n" 98 | "| 1.0 | 85.5 |\n" 99 | "| 2.0 | 92.0 |\n" 100 | "| 3.0 | 78.25 |\n" 101 | ) 102 | result = dataframe_to_markdown(df) 103 | print(result) 104 | self.assertEqual(result, expected_markdown) 105 | 106 | 107 | if __name__ == '__main__': 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pandas as pd 3 | from excel_to_markdown.parser import parse_columns 4 | from excel_to_markdown.utils import column_letter_to_index 5 | 6 | class TestParser(unittest.TestCase): 7 | def setUp(self): 8 | # Create a sample DataFrame with 10 columns labeled A to J 9 | columns = [chr(i) for i in range(65, 75)] # ['A', 'B', ..., 'J'] 10 | data = [[f'Data{i}{j}' for j in range(10)] for i in range(5)] 11 | self.df = pd.DataFrame(data, columns=columns) 12 | 13 | def test_parse_columns_letter_range(self): 14 | # Test parsing a letter-based column range 15 | cols_input = "A:D" 16 | expected = ['A', 'B', 'C', 'D'] 17 | result = parse_columns(cols_input, self.df) 18 | self.assertEqual(result, expected) 19 | 20 | def test_parse_columns_number_range(self): 21 | # Test parsing a number-based column range 22 | cols_input = "1-4" 23 | expected = ['A', 'B', 'C', 'D'] 24 | result = parse_columns(cols_input, self.df) 25 | self.assertEqual(result, expected) 26 | 27 | def test_parse_columns_single_letter(self): 28 | # Test parsing a single letter 29 | cols_input = "E" 30 | expected = ['E'] 31 | result = parse_columns(cols_input, self.df) 32 | self.assertEqual(result, expected) 33 | 34 | def test_parse_columns_single_number(self): 35 | # Test parsing a single number 36 | cols_input = "5" 37 | expected = ['E'] 38 | result = parse_columns(cols_input, self.df) 39 | self.assertEqual(result, expected) 40 | 41 | def test_parse_columns_mixed_letters(self): 42 | # Test parsing a mixed letter-based range 43 | cols_input = "G:I" 44 | expected = ['G', 'H', 'I'] 45 | result = parse_columns(cols_input, self.df) 46 | self.assertEqual(result, expected) 47 | 48 | def test_parse_columns_start_after_end_letters(self): 49 | # Test when start letter comes after end letter 50 | cols_input = "D:A" 51 | with self.assertRaises(ValueError): 52 | parse_columns(cols_input, self.df) 53 | 54 | def test_parse_columns_start_after_end_numbers(self): 55 | # Test when start number comes after end number 56 | cols_input = "4-1" 57 | with self.assertRaises(ValueError): 58 | parse_columns(cols_input, self.df) 59 | 60 | def test_parse_columns_invalid_format(self): 61 | # Test invalid format input 62 | cols_input = "A-4" 63 | with self.assertRaises(ValueError): 64 | parse_columns(cols_input, self.df) 65 | 66 | def test_parse_columns_non_alphanumeric(self): 67 | # Test input with non-alphanumeric characters 68 | cols_input = "A$-D#" 69 | with self.assertRaises(ValueError): 70 | parse_columns(cols_input, self.df) 71 | 72 | if __name__ == '__main__': 73 | unittest.main() 74 | --------------------------------------------------------------------------------