├── .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 | 
4 | 
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 |
--------------------------------------------------------------------------------