├── .gitignore ├── Dockerfile ├── LICENSE.txt ├── README.rst ├── docker-compose.yaml ├── pandaprint ├── __init__.py └── server.py ├── poetry.lock ├── printers.yaml ├── pyproject.toml ├── tests ├── __init__.py └── test_server.py └── tools ├── Dockerfile-ftp ├── docker-compose.yaml ├── mosquitto_conf ├── mosquitto.conf └── passwords ├── test-setup.sh └── vsftpd_conf └── vsftpd.conf /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .stestr/ 3 | tools/ca/ 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker.io/opendevorg/python-base:3.12-bookworm as builder 2 | 3 | ENV DEBIAN_FRONTEND=noninteractive 4 | ENV POETRY_HOME=/opt/poetry 5 | ENV POETRY_NO_INTERACTION=1 6 | ENV POETRY_VIRTUALENVS_IN_PROJECT=1 7 | ENV POETRY_VIRTUALENVS_CREATE=1 8 | ENV PYTHONDONTWRITEBYTECODE=1 9 | ENV PYTHONUNBUFFERED=1 10 | ENV POETRY_CACHE_DIR=/opt/.cache 11 | 12 | RUN apt-get update \ 13 | && apt-get install -y git \ 14 | && apt-get clean \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | RUN python3 -m venv /poetry-env &&\ 18 | /poetry-env/bin/pip install poetry poetry-dynamic-versioning 19 | 20 | COPY . /app 21 | WORKDIR /app 22 | 23 | RUN /poetry-env/bin/poetry install 24 | 25 | FROM docker.io/opendevorg/python-base:3.12-bookworm as app 26 | 27 | COPY --from=builder /app /app 28 | ENV PATH="/app/.venv/bin:$PATH" 29 | RUN mkdir /config 30 | 31 | CMD ["pandaprint", "/config/printers.yaml"] 32 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | PandaPrint 3 | ============ 4 | 5 | This is a server that enables uploading from OrcaSlicer to Bambu Lab 6 | printers in LAN mode without the use of the Bambu Lab network plugin. 7 | 8 | Users concerned about security or software freedom may be reluctant to 9 | use either the Bambu Lab cloud service or the proprietary Bambu Lab 10 | network plugin. PandaPrint does not replace all of the functionality 11 | in the network plugin, but it does allow sending a project to a 12 | printer directly from OrcaSlicer. 13 | 14 | Configuration 15 | ============= 16 | 17 | Create a ``printers.yaml`` file like this example: 18 | 19 | .. code-block:: yaml 20 | 21 | printers: 22 | - name: bambu 23 | host: bambu.lan 24 | serial: 123456789012345 25 | key: 12345678 26 | 27 | The fields are as follows: 28 | 29 | **name**: A friendly name of your choosing. This will appear in the 30 | url supplied to OrcaSlicer later. If you have more than one printer, 31 | ensure this is unique. 32 | 33 | **host**: The hostname or IP address of the printer. 34 | 35 | **serial**: The serial number of the printer. 36 | 37 | **key**: The access key for the printer. Get this from the LAN mode 38 | screen. 39 | 40 | The following fields are optional and are used to configure printing 41 | options when "Upload and Print" is used from the slicer. They have no 42 | effect for files that are merely uploaded. 43 | 44 | **timelapse**: Enable timelapse video (true/false). 45 | 46 | **bed_levelling**: Perform bed levelling (true/false). 47 | 48 | **flow_cali**: Perform flow calibration (true/false). 49 | 50 | **vibration_cali**: Perform vibration calibration (true/false). 51 | 52 | **layer_inspect**: Enable layer inspection (true/false). 53 | 54 | **use_ams**: Use the AMS instead of the external spool (true/false). 55 | No AMS mapping is performed, so be sure that the spool numbers in the 56 | slicer match the actual contents of the AMS when using "Upload and 57 | Print". 58 | 59 | Running 60 | ======= 61 | 62 | The easiest way to run this is from a container image. 63 | 64 | Running the Container Image 65 | --------------------------- 66 | 67 | A sample docker-compose file is included. 68 | 69 | .. code-block:: shell 70 | 71 | docker compose up -d 72 | 73 | Running from Source 74 | ------------------- 75 | 76 | To run this directly from the source repo: 77 | 78 | .. code-block:: shell 79 | 80 | poetry install 81 | poetry run pandaprint ./printers.yaml 82 | 83 | Configuring OrcaSlicer 84 | ====================== 85 | 86 | 1. Edit the printer. 87 | 2. Enable the `Advanced` toggle. 88 | 3. Under `Basic information`, `Advanced`, check ``Use 3rd-party print host`` 89 | 4. Close the printer edit dialog. 90 | 5. Click the `Connection` button (wifi icon) next to the printer. 91 | 6. Under `Print Host uplod` set the following: 92 | **Host Type**: ``Octo/Klipper`` 93 | **Hostname, IP or URL**: ``http://localhost:8080/bambu`` (if you chose something other than ``bambu`` as the name in ``printers.yaml``, use that here instead) 94 | 7. Press the `Test` button to ensure everything is working. 95 | 8. Press "OK" 96 | 97 | At this point, you should be able to use the `Print Plate` button to send projects to the printer. 98 | 99 | License 100 | ======= 101 | 102 | This software is licensed under the AGPLv3. See ``LICENSE.txt`` for details. 103 | 104 | Contributing 105 | ============ 106 | 107 | Pull requests are welcome. 108 | 109 | Running Tests 110 | ------------- 111 | 112 | .. code-block:: shell 113 | 114 | cd tools 115 | ./test-setup.sh 116 | cd .. 117 | poetry run stestr init 118 | poetry run stestr run 119 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | pandaprint: 3 | image: "quay.io/pandaprint/pandaprint:latest" 4 | volumes: 5 | - "./printers.yaml:/config/printers.yaml" 6 | ports: 7 | - "8080:8080" 8 | -------------------------------------------------------------------------------- /pandaprint/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaprint-dev/pandaprint/a4a78cc47ccb3deb11d2f314886b889ab8ebb4c9/pandaprint/__init__.py -------------------------------------------------------------------------------- /pandaprint/server.py: -------------------------------------------------------------------------------- 1 | # PandaPrint 2 | # Copyright (C) 2024 James E. Blair 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import argparse 18 | import json 19 | import logging 20 | import random 21 | import shutil 22 | import ssl 23 | import string 24 | import tempfile 25 | import zipfile 26 | 27 | import cherrypy 28 | import ftplib 29 | import yaml 30 | import paho.mqtt.client as mqtt 31 | 32 | # MQTT API reference: 33 | # https://github.com/Doridian/OpenBambuAPI/blob/main/mqtt.md 34 | 35 | def noop(): 36 | pass 37 | 38 | 39 | # This class is based on: 40 | # https://stackoverflow.com/questions/12164470/python-ftp-implicit-tls-connection-issue 41 | # and 42 | # https://gist.github.com/hoogenm/de42e2ef85b38179297a0bba8d60778b 43 | class FTPS(ftplib.FTP_TLS): 44 | # Implicit SSL for FTP 45 | 46 | def __init__(self, *args, **kwargs): 47 | super().__init__(*args, **kwargs) 48 | self._sock = None 49 | 50 | @property 51 | def sock(self): 52 | return self._sock 53 | 54 | @sock.setter 55 | def sock(self, value): 56 | # When modifying the socket, ensure that it is ssl wrapped 57 | if value is not None and not isinstance(value, ssl.SSLSocket): 58 | value = self.context.wrap_socket(value) 59 | self._sock = value 60 | 61 | def ntransfercmd(self, cmd, rest=None): 62 | # Override the ntransfercmd method to wrap the socket 63 | conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest) 64 | conn = self.sock.context.wrap_socket( 65 | conn, server_hostname=self.host, session=self.sock.session 66 | ) 67 | # Superclass will try to unwrap the socket after transfer 68 | conn.unwrap = noop 69 | return conn, size 70 | 71 | def makepasv(self): 72 | # Ignore the host value returned by PASV 73 | host, port = super().makepasv() 74 | return self.host, port 75 | 76 | 77 | class MQTT: 78 | def __init__(self, hostname, username, password, port=8883): 79 | self.hostname = hostname 80 | self.port = port 81 | self.subs = {} 82 | self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) 83 | self.client.tls_set(cert_reqs=ssl.CERT_NONE) 84 | self.client.tls_insecure_set(True) 85 | self.client.username_pw_set(username, password) 86 | self.client.connect(hostname, port, 60) 87 | self.client.loop_start() 88 | 89 | def send_json(self, topic, data): 90 | msg = json.dumps(data) 91 | self.client.publish(topic, msg) 92 | 93 | def stop(self): 94 | self.client.disconnect() 95 | self.client.loop_stop() 96 | 97 | 98 | class Printer: 99 | def __init__(self, printer): 100 | self.name = str(printer['name']) 101 | self.host = str(printer['host']) 102 | self.serial = str(printer['serial']) 103 | self.key = str(printer['key']) 104 | self._mqtt = None 105 | 106 | self.print_options = {} 107 | for k in ( 108 | 'timelapse', 109 | 'bed_levelling', 110 | 'flow_cali', 111 | 'vibration_cali', 112 | 'layer_inspect', 113 | 'use_ams', 114 | ): 115 | if k in printer: 116 | self.print_options[k] = bool(printer[k]) 117 | 118 | @property 119 | def mqtt(self): 120 | if not self._mqtt: 121 | self._mqtt = MQTT(self.host, 'bblp', self.key) 122 | return self._mqtt 123 | 124 | 125 | class PrintAPI: 126 | def __init__(self, config): 127 | self.printers = {p['name']: Printer(p) for p in config.printers} 128 | 129 | def stop(self): 130 | for p in self.printers.values(): 131 | p.mqtt.stop() 132 | 133 | @cherrypy.expose 134 | @cherrypy.tools.json_out() 135 | def version(self, pname): 136 | printer = self.printers[pname] 137 | # Make sure we can contact the mqtt server 138 | printer.mqtt 139 | return { 140 | 'api': '1.1.0', 141 | 'server': '1.1.0', 142 | 'text': 'OctoPrint 1.1.0 (PandaPrint 1.0)', 143 | } 144 | 145 | def _parse_file(self, fp): 146 | filename = fp.filename 147 | basename = filename.rsplit('.', 1)[0] 148 | with tempfile.TemporaryFile() as f: 149 | shutil.copyfileobj(fp.file, f) 150 | with zipfile.ZipFile(f) as zf: 151 | gcode_files = [x for x in zf.namelist() if x.startswith('Metadata/') and x.endswith('.gcode')] 152 | if len(gcode_files) == 1: 153 | # There's only one plate, send the original file 154 | f.seek(0) 155 | yield filename, f 156 | return 157 | for plate_no in range(1, len(gcode_files)+1): 158 | # Split the plate into multiple files 159 | plate = [] 160 | for fn in zf.namelist(): 161 | if fn.startswith('Metadata/'): 162 | base, ext = fn.split('.', 1) 163 | if str(plate_no) in base: 164 | plate.append(fn) 165 | else: 166 | # This file is not plate-specific 167 | plate.append(fn) 168 | # Make a new zipfile 169 | with tempfile.TemporaryFile() as outf: 170 | with zipfile.ZipFile(outf, mode='w') as outzf: 171 | for fn in plate: 172 | base, ext = fn.split('.', 1) 173 | # Rename every plate "plate_1". Only 174 | # the base, not the extension (.md5 175 | # may be present). 176 | base = base.replace(str(plate_no), '1') 177 | outfn = f'{base}.{ext}' 178 | outzf.writestr(outfn, zf.read(fn)) 179 | outf.seek(0) 180 | yield f'{basename}-{plate_no}.3mf', outf 181 | 182 | @cherrypy.expose 183 | def upload(self, pname, location, **kw): 184 | # https://docs.octoprint.org/en/master/api/files.html#upload-file-or-create-folder 185 | printer = self.printers[pname] 186 | do_print = str(kw.get('print', False)).lower() == 'true' 187 | fp = kw['file'] 188 | 189 | 190 | # ftps upload 191 | first_filename = None 192 | with FTPS() as ftp: 193 | ftp.connect(host=printer.host, port=990, timeout=30) 194 | ftp.login('bblp', printer.key) 195 | ftp.prot_p() 196 | for newfilename, newfp in self._parse_file(fp): 197 | if first_filename is None: 198 | first_filename = newfilename 199 | ftp.storbinary(f'STOR /model/{newfilename}', newfp) 200 | 201 | if do_print: 202 | print_data = { 203 | "sequence_id": "0", 204 | "command": "project_file", 205 | "param": "Metadata/plate_1.gcode", 206 | "project_id": "0", 207 | "profile_id": "0", 208 | "task_id": "0", 209 | "subtask_id": "0", 210 | "subtask_name": "", 211 | 212 | #"file": filename, 213 | "url": f"file:///sdcard/model/{first_filename}", 214 | #"md5": "", 215 | 216 | "bed_type": "auto", 217 | #"ams_mapping": "", 218 | } 219 | print_data.update(printer.print_options) 220 | 221 | printer.mqtt.send_json( 222 | f'device/{printer.serial}/request', 223 | { 224 | "print": print_data 225 | } 226 | ) 227 | cherrypy.response.status = "201 Resource Created" 228 | 229 | 230 | class PandaConfig: 231 | def __init__(self): 232 | self.listen_address = '::' 233 | self.listen_port = 8080 234 | self.printers = [] 235 | 236 | def load(self, data): 237 | self.listen_address = data.get('listen-address', self.listen_address) 238 | self.listen_port = data.get('listen-port', self.listen_port) 239 | self.printers = data.get('printers', self.printers) 240 | 241 | def load_from_file(self, path): 242 | with open(path) as f: 243 | self.load(yaml.safe_load(f)) 244 | 245 | 246 | class PandaServer: 247 | def __init__(self, config): 248 | self.api = PrintAPI(config) 249 | 250 | mapper = cherrypy.dispatch.RoutesDispatcher() 251 | mapper.connect('api', "/{pname}/api/version", 252 | controller=self.api, action='version') 253 | mapper.connect('api', "/{pname}/api/files/{location}", 254 | conditions=dict(method=['POST']), 255 | controller=self.api, action='upload') 256 | cherrypy.config.update({ 257 | 'global': { 258 | 'environment': 'production', 259 | 'server.socket_host': config.listen_address, 260 | 'server.socket_port': config.listen_port, 261 | } 262 | }) 263 | conf = { 264 | '/': { 265 | 'request.dispatch': mapper, 266 | } 267 | } 268 | app=cherrypy.tree.mount(root=None, config=conf) 269 | 270 | def start(self): 271 | cherrypy.engine.start() 272 | 273 | def stop(self): 274 | cherrypy.engine.exit() 275 | cherrypy.server.httpserver = None 276 | self.api.stop() 277 | 278 | def main(): 279 | parser = argparse.ArgumentParser( 280 | description='Relay for Bambu Lab printers') 281 | parser.add_argument('config_file', 282 | help='Config file') 283 | args = parser.parse_args() 284 | 285 | logging.basicConfig(level=logging.DEBUG) 286 | 287 | config = PandaConfig() 288 | config.load_from_file(args.config_file) 289 | server = PandaServer(config) 290 | server.start() 291 | 292 | if __name__ == '__main__': 293 | main() 294 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "autocommand" 5 | version = "2.2.2" 6 | description = "A library to create a command-line program from a function" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "autocommand-2.2.2-py3-none-any.whl", hash = "sha256:710afe251075e038e19e815e25f8155cabe02196cfb545b2185e0d9c8b2b0459"}, 11 | {file = "autocommand-2.2.2.tar.gz", hash = "sha256:878de9423c5596491167225c2a455043c3130fb5b7286ac83443d45e74955f34"}, 12 | ] 13 | 14 | [[package]] 15 | name = "autopage" 16 | version = "0.5.2" 17 | description = "A library to provide automatic paging for console output" 18 | optional = false 19 | python-versions = ">=3.6" 20 | files = [ 21 | {file = "autopage-0.5.2-py3-none-any.whl", hash = "sha256:f5eae54dd20ccc8b1ff611263fc87bc46608a9cde749bbcfc93339713a429c55"}, 22 | {file = "autopage-0.5.2.tar.gz", hash = "sha256:826996d74c5aa9f4b6916195547312ac6384bac3810b8517063f293248257b72"}, 23 | ] 24 | 25 | [[package]] 26 | name = "certifi" 27 | version = "2024.8.30" 28 | description = "Python package for providing Mozilla's CA Bundle." 29 | optional = false 30 | python-versions = ">=3.6" 31 | files = [ 32 | {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, 33 | {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, 34 | ] 35 | 36 | [[package]] 37 | name = "charset-normalizer" 38 | version = "3.4.0" 39 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 40 | optional = false 41 | python-versions = ">=3.7.0" 42 | files = [ 43 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, 44 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, 45 | {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, 46 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, 47 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, 48 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, 49 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, 50 | {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, 51 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, 52 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, 53 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, 54 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, 55 | {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, 56 | {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, 57 | {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, 58 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, 59 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, 60 | {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, 61 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, 62 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, 63 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, 64 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, 65 | {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, 66 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, 67 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, 68 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, 69 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, 70 | {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, 71 | {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, 72 | {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, 73 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, 74 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, 75 | {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, 76 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, 77 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, 78 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, 79 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, 80 | {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, 81 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, 82 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, 83 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, 84 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, 85 | {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, 86 | {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, 87 | {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, 88 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, 89 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, 90 | {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, 91 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, 92 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, 93 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, 94 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, 95 | {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, 96 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, 97 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, 98 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, 99 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, 100 | {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, 101 | {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, 102 | {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, 103 | {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, 104 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, 105 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, 106 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, 107 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, 108 | {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, 109 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, 110 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, 111 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, 112 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, 113 | {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, 114 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, 115 | {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, 116 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, 117 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, 118 | {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, 119 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, 120 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, 121 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, 122 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, 123 | {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, 124 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, 125 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, 126 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, 127 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, 128 | {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, 129 | {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, 130 | {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, 131 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, 132 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, 133 | {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, 134 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, 135 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, 136 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, 137 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, 138 | {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, 139 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, 140 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, 141 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, 142 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, 143 | {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, 144 | {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, 145 | {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, 146 | {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, 147 | {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, 148 | ] 149 | 150 | [[package]] 151 | name = "cheroot" 152 | version = "10.0.1" 153 | description = "Highly-optimized, pure-python HTTP server" 154 | optional = false 155 | python-versions = ">=3.6" 156 | files = [ 157 | {file = "cheroot-10.0.1-py3-none-any.whl", hash = "sha256:6ea332f20bfcede14e66174d112b30e9807492320d737ca628badc924d997595"}, 158 | {file = "cheroot-10.0.1.tar.gz", hash = "sha256:e0b82f797658d26b8613ec8eb563c3b08e6bd6a7921e9d5089bd1175ad1b1740"}, 159 | ] 160 | 161 | [package.dependencies] 162 | "jaraco.functools" = "*" 163 | more-itertools = ">=2.6" 164 | 165 | [package.extras] 166 | docs = ["furo", "jaraco.packaging (>=3.2)", "python-dateutil", "sphinx (>=1.8.2)", "sphinx-tabs (>=1.1.0)", "sphinxcontrib-apidoc (>=0.3.0)"] 167 | 168 | [[package]] 169 | name = "cherrypy" 170 | version = "18.10.0" 171 | description = "Object-Oriented HTTP framework" 172 | optional = false 173 | python-versions = ">=3.6" 174 | files = [ 175 | {file = "CherryPy-18.10.0-py3-none-any.whl", hash = "sha256:129e444b9a63cea4e765481b156376f1cfe319e64caaaec2485636532373b298"}, 176 | {file = "cherrypy-18.10.0.tar.gz", hash = "sha256:6c70e78ee11300e8b21c0767c542ae6b102a49cac5cfd4e3e313d7bb907c5891"}, 177 | ] 178 | 179 | [package.dependencies] 180 | cheroot = ">=8.2.1" 181 | "jaraco.collections" = "*" 182 | more-itertools = "*" 183 | portend = ">=2.1.1" 184 | "zc.lockfile" = "*" 185 | 186 | [package.extras] 187 | docs = ["alabaster", "docutils", "jaraco.packaging (>=3.2)", "rst.linker (>=1.11)", "sphinx", "sphinxcontrib-apidoc (>=0.3.0)"] 188 | json = ["simplejson"] 189 | memcached-session = ["python-memcached (>=1.58)"] 190 | routes-dispatcher = ["routes (>=2.3.1)"] 191 | ssl = ["pyOpenSSL"] 192 | testing = ["objgraph", "path.py", "pytest (>=5.3.5)", "pytest-cov", "pytest-forked", "pytest-services (>=2)", "pytest-sugar", "requests-toolbelt", "setuptools"] 193 | xcgi = ["flup"] 194 | 195 | [[package]] 196 | name = "cliff" 197 | version = "4.8.0" 198 | description = "Command Line Interface Formulation Framework" 199 | optional = false 200 | python-versions = ">=3.9" 201 | files = [ 202 | {file = "cliff-4.8.0-py3-none-any.whl", hash = "sha256:31d761e73920f3260a40f52ba629d7beef6a631b9ad2d039dd4b9fc738760de4"}, 203 | {file = "cliff-4.8.0.tar.gz", hash = "sha256:23eff502e603cf0aa841eaea6662a42cd3064169162b3e596b20226400e34dfd"}, 204 | ] 205 | 206 | [package.dependencies] 207 | autopage = ">=0.4.0" 208 | cmd2 = ">=1.0.0" 209 | PrettyTable = ">=0.7.2" 210 | PyYAML = ">=3.12" 211 | stevedore = ">=2.0.1" 212 | 213 | [[package]] 214 | name = "cmd2" 215 | version = "2.5.7" 216 | description = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python" 217 | optional = false 218 | python-versions = ">=3.8" 219 | files = [ 220 | {file = "cmd2-2.5.7-py3-none-any.whl", hash = "sha256:7e5856fd1a75716288d4638e68946f9697404f377dfdeeddc19045c7012de9b7"}, 221 | {file = "cmd2-2.5.7.tar.gz", hash = "sha256:0219e2bb75075fa16deffb88edf86efdd2a87439d1fa7b94fdea4b929a3dc914"}, 222 | ] 223 | 224 | [package.dependencies] 225 | gnureadline = {version = "*", markers = "platform_system == \"Darwin\""} 226 | pyperclip = "*" 227 | pyreadline3 = {version = "*", markers = "platform_system == \"Windows\""} 228 | wcwidth = "*" 229 | 230 | [package.extras] 231 | build = ["build", "setuptools", "setuptools-scm"] 232 | dev = ["codecov", "doc8", "invoke", "mypy", "pytest", "pytest-cov", "pytest-mock", "ruff", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme", "twine"] 233 | docs = ["setuptools", "setuptools_scm", "sphinx", "sphinx-autobuild", "sphinx-rtd-theme"] 234 | test = ["codecov", "coverage", "pytest", "pytest-cov", "pytest-mock"] 235 | validate = ["mypy", "ruff", "types-setuptools"] 236 | 237 | [[package]] 238 | name = "extras" 239 | version = "1.0.0" 240 | description = "Useful extra bits for Python - things that shold be in the standard library" 241 | optional = false 242 | python-versions = "*" 243 | files = [ 244 | {file = "extras-1.0.0-py2.py3-none-any.whl", hash = "sha256:f689f08df47e2decf76aa6208c081306e7bd472630eb1ec8a875c67de2366e87"}, 245 | {file = "extras-1.0.0.tar.gz", hash = "sha256:132e36de10b9c91d5d4cc620160a476e0468a88f16c9431817a6729611a81b4e"}, 246 | ] 247 | 248 | [[package]] 249 | name = "fixtures" 250 | version = "4.1.0" 251 | description = "Fixtures, reusable state for writing clean tests and more." 252 | optional = false 253 | python-versions = ">=3.7" 254 | files = [ 255 | {file = "fixtures-4.1.0-py3-none-any.whl", hash = "sha256:a43a55da406c37651aa86dd1ba6c3983a09d36d60fe5f72242872c8a4eeeb710"}, 256 | {file = "fixtures-4.1.0.tar.gz", hash = "sha256:82b1c5e69f615526ef6c067188a1e6c6067df7f88332509c99f8b8fdbb9776f3"}, 257 | ] 258 | 259 | [package.dependencies] 260 | pbr = ">=5.7.0" 261 | 262 | [package.extras] 263 | docs = ["docutils"] 264 | streams = ["testtools"] 265 | test = ["mock", "testtools"] 266 | 267 | [[package]] 268 | name = "gnureadline" 269 | version = "8.2.13" 270 | description = "The standard Python readline extension statically linked against the GNU readline library." 271 | optional = false 272 | python-versions = "*" 273 | files = [ 274 | {file = "gnureadline-8.2.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0ca03501ce0939d7ecf9d075860d6f6ceb2f49f30331b4e96e4678ce03687bab"}, 275 | {file = "gnureadline-8.2.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c28e33bfc56d4204693f213abeab927f65c505ce91f668a039720bc7c46b0353"}, 276 | {file = "gnureadline-8.2.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6472e3a780087eecd67c03e5455aecb209de51bcae74583222976f6b816f6192"}, 277 | {file = "gnureadline-8.2.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94b143ea5d22b0c1ca4a591265afe135272c69b7757e968e34fbb47a7858d1ce"}, 278 | {file = "gnureadline-8.2.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:561a60b12f74ea7234036cc4fe558f3b46023be0dac5ed73541ece58cba2f88a"}, 279 | {file = "gnureadline-8.2.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:daa405028b9fe92bfbb93624e13e0674a242a1c5434b70ef61a04294502fdb65"}, 280 | {file = "gnureadline-8.2.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:576dac060887adc6067ee9d23fb2f0031fb2b3e560e07a6c9e666e05f0473af7"}, 281 | {file = "gnureadline-8.2.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10fcaf561bc4ed6ab7075ab3ead188a18faaf4e6e92d916f81a09c0a670ce906"}, 282 | {file = "gnureadline-8.2.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c152a82613fa012ab4331bb9a0ffddb415e37561d376b910bf9e7d535607faf"}, 283 | {file = "gnureadline-8.2.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85e362d2d0e85e45f0affae7bbfaf998b00167c55a78d31ee0f214de9ff429d2"}, 284 | {file = "gnureadline-8.2.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b69e6608cc94e110018b721a11718d480a6330e0b62cbab65a22880e84011205"}, 285 | {file = "gnureadline-8.2.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cc77fc9c8a8fcf10e0a554e49ee763219683386b8f906b7e6ef07c9e40e8420"}, 286 | {file = "gnureadline-8.2.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d3e33d2e0dd694d623a2ca1fae6990b52f1d25955504b7293a9350fb9912940"}, 287 | {file = "gnureadline-8.2.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c550d08c4d2882a83293a724b14a262ee5078fd4fa7acdc78aa59cab26ae343"}, 288 | {file = "gnureadline-8.2.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7d6e3f5d9fd0cf8a84fb382d4e3ad2914331be4d929f17d50da01f1571c4b03"}, 289 | {file = "gnureadline-8.2.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59275168cae1b02ca1ec7586a9804bb04ce427df92f8582a80d16e96c846b78"}, 290 | {file = "gnureadline-8.2.13-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:59c5505026646da6d5ced6a5316d6d191d011e8be422cba4abce71730ef37dc6"}, 291 | {file = "gnureadline-8.2.13-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f1050ecf789f34d0ab0aacdb605f177725009a864e0038e70380614af92dc0d"}, 292 | {file = "gnureadline-8.2.13-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23b43c8e9e2e6566cb3094749826181a86dba1d94b1e023b5f9923dc26e37876"}, 293 | {file = "gnureadline-8.2.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4f5fc90af56a1ae6f88c9c7122fc76141c395b6c342a63800abed8c813f48b85"}, 294 | {file = "gnureadline-8.2.13-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d708e8f655d3b556a138f13e9fcb2d8a10a6901e3125c04cad5ef7c883191fe8"}, 295 | {file = "gnureadline-8.2.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:265bcf6ef7082e130160fb34b9664284affb216a22c5bffcd518b35d02bcc4e9"}, 296 | {file = "gnureadline-8.2.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07231f8191adb7f204010a86a91df9df9a80944981a16576a471f59304ad6a16"}, 297 | {file = "gnureadline-8.2.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30cc1b6cb11d94554815cb91eb1dfa6a11887185aae50f253adaa393e91c6a86"}, 298 | {file = "gnureadline-8.2.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50c40bfffffa82d4fcb0fde4940d4ff128ba2f876c1da09bae9d6d9ff770095e"}, 299 | {file = "gnureadline-8.2.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcfa601d95c00aa670ec5e4bf791caf6ba0bcf266de940fb54d44c278bd302fe"}, 300 | {file = "gnureadline-8.2.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c7b8d3f2a2c9b7e6feaf1f20bdb6ebb8210e207b8c5360ffe407a47efeeb3fb8"}, 301 | {file = "gnureadline-8.2.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811d85a70ac97cddeb1755282915e8a93c279dcf89513426f28617b8feff5aec"}, 302 | {file = "gnureadline-8.2.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f57a3aa97c3379b2513c8bfbac0de2dfb41f695623c0b2ad337babb646b51a7"}, 303 | {file = "gnureadline-8.2.13.tar.gz", hash = "sha256:c9b9e1e7ba99a80bb50c12027d6ce692574f77a65bf57bc97041cf81c0f49bd1"}, 304 | ] 305 | 306 | [[package]] 307 | name = "idna" 308 | version = "3.10" 309 | description = "Internationalized Domain Names in Applications (IDNA)" 310 | optional = false 311 | python-versions = ">=3.6" 312 | files = [ 313 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 314 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 315 | ] 316 | 317 | [package.extras] 318 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 319 | 320 | [[package]] 321 | name = "iso8601" 322 | version = "2.1.0" 323 | description = "Simple module to parse ISO 8601 dates" 324 | optional = false 325 | python-versions = ">=3.7,<4.0" 326 | files = [ 327 | {file = "iso8601-2.1.0-py3-none-any.whl", hash = "sha256:aac4145c4dcb66ad8b648a02830f5e2ff6c24af20f4f482689be402db2429242"}, 328 | {file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"}, 329 | ] 330 | 331 | [[package]] 332 | name = "jaraco-collections" 333 | version = "5.1.0" 334 | description = "Collection objects similar to those in stdlib by jaraco" 335 | optional = false 336 | python-versions = ">=3.8" 337 | files = [ 338 | {file = "jaraco.collections-5.1.0-py3-none-any.whl", hash = "sha256:a9480be7fe741d34639b3c32049066d7634b520746552d1a5d0fcda07ada1020"}, 339 | {file = "jaraco_collections-5.1.0.tar.gz", hash = "sha256:0e4829409d39ad18a40aa6754fee2767f4d9730c4ba66dc9df89f1d2756994c2"}, 340 | ] 341 | 342 | [package.dependencies] 343 | "jaraco.text" = "*" 344 | 345 | [package.extras] 346 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 347 | cover = ["pytest-cov"] 348 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 349 | enabler = ["pytest-enabler (>=2.2)"] 350 | test = ["pytest (>=6,!=8.1.*)"] 351 | type = ["pytest-mypy"] 352 | 353 | [[package]] 354 | name = "jaraco-context" 355 | version = "6.0.1" 356 | description = "Useful decorators and context managers" 357 | optional = false 358 | python-versions = ">=3.8" 359 | files = [ 360 | {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, 361 | {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, 362 | ] 363 | 364 | [package.extras] 365 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 366 | test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 367 | 368 | [[package]] 369 | name = "jaraco-functools" 370 | version = "4.1.0" 371 | description = "Functools like those found in stdlib" 372 | optional = false 373 | python-versions = ">=3.8" 374 | files = [ 375 | {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, 376 | {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, 377 | ] 378 | 379 | [package.dependencies] 380 | more-itertools = "*" 381 | 382 | [package.extras] 383 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] 384 | cover = ["pytest-cov"] 385 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 386 | enabler = ["pytest-enabler (>=2.2)"] 387 | test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] 388 | type = ["pytest-mypy"] 389 | 390 | [[package]] 391 | name = "jaraco-text" 392 | version = "4.0.0" 393 | description = "Module for text manipulation" 394 | optional = false 395 | python-versions = ">=3.8" 396 | files = [ 397 | {file = "jaraco.text-4.0.0-py3-none-any.whl", hash = "sha256:08de508939b5e681b14cdac2f1f73036cd97f6f8d7b25e96b8911a9a428ca0d1"}, 398 | {file = "jaraco_text-4.0.0.tar.gz", hash = "sha256:5b71fecea69ab6f939d4c906c04fee1eda76500d1641117df6ec45b865f10db0"}, 399 | ] 400 | 401 | [package.dependencies] 402 | autocommand = "*" 403 | "jaraco.context" = ">=4.1" 404 | "jaraco.functools" = "*" 405 | more-itertools = "*" 406 | 407 | [package.extras] 408 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 409 | inflect = ["inflect"] 410 | test = ["pathlib2", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 411 | 412 | [[package]] 413 | name = "more-itertools" 414 | version = "10.5.0" 415 | description = "More routines for operating on iterables, beyond itertools" 416 | optional = false 417 | python-versions = ">=3.8" 418 | files = [ 419 | {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, 420 | {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, 421 | ] 422 | 423 | [[package]] 424 | name = "paho-mqtt" 425 | version = "2.1.0" 426 | description = "MQTT version 5.0/3.1.1 client class" 427 | optional = false 428 | python-versions = ">=3.7" 429 | files = [ 430 | {file = "paho_mqtt-2.1.0-py3-none-any.whl", hash = "sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee"}, 431 | {file = "paho_mqtt-2.1.0.tar.gz", hash = "sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834"}, 432 | ] 433 | 434 | [package.extras] 435 | proxy = ["pysocks"] 436 | 437 | [[package]] 438 | name = "pbr" 439 | version = "6.1.0" 440 | description = "Python Build Reasonableness" 441 | optional = false 442 | python-versions = ">=2.6" 443 | files = [ 444 | {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, 445 | {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, 446 | ] 447 | 448 | [[package]] 449 | name = "portend" 450 | version = "3.2.0" 451 | description = "TCP port monitoring and discovery" 452 | optional = false 453 | python-versions = ">=3.8" 454 | files = [ 455 | {file = "portend-3.2.0-py3-none-any.whl", hash = "sha256:8b3fe3f78779df906559a21d9eaa6e21c8fa5a7a8cc76362cbbe1e16777399cf"}, 456 | {file = "portend-3.2.0.tar.gz", hash = "sha256:5250a352c19c959d767cac878b829d93e5dc7625a5143399a2a00dc6628ffb72"}, 457 | ] 458 | 459 | [package.dependencies] 460 | tempora = ">=1.8" 461 | 462 | [package.extras] 463 | docs = ["furo", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 464 | testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"] 465 | 466 | [[package]] 467 | name = "prettytable" 468 | version = "3.12.0" 469 | description = "A simple Python library for easily displaying tabular data in a visually appealing ASCII table format" 470 | optional = false 471 | python-versions = ">=3.9" 472 | files = [ 473 | {file = "prettytable-3.12.0-py3-none-any.whl", hash = "sha256:77ca0ad1c435b6e363d7e8623d7cc4fcf2cf15513bf77a1c1b2e814930ac57cc"}, 474 | {file = "prettytable-3.12.0.tar.gz", hash = "sha256:f04b3e1ba35747ac86e96ec33e3bb9748ce08e254dc2a1c6253945901beec804"}, 475 | ] 476 | 477 | [package.dependencies] 478 | wcwidth = "*" 479 | 480 | [package.extras] 481 | tests = ["pytest", "pytest-cov", "pytest-lazy-fixtures"] 482 | 483 | [[package]] 484 | name = "pyperclip" 485 | version = "1.9.0" 486 | description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" 487 | optional = false 488 | python-versions = "*" 489 | files = [ 490 | {file = "pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310"}, 491 | ] 492 | 493 | [[package]] 494 | name = "pyreadline3" 495 | version = "3.5.4" 496 | description = "A python implementation of GNU readline." 497 | optional = false 498 | python-versions = ">=3.8" 499 | files = [ 500 | {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, 501 | {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, 502 | ] 503 | 504 | [package.extras] 505 | dev = ["build", "flake8", "mypy", "pytest", "twine"] 506 | 507 | [[package]] 508 | name = "python-dateutil" 509 | version = "2.9.0.post0" 510 | description = "Extensions to the standard Python datetime module" 511 | optional = false 512 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 513 | files = [ 514 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 515 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 516 | ] 517 | 518 | [package.dependencies] 519 | six = ">=1.5" 520 | 521 | [[package]] 522 | name = "python-subunit" 523 | version = "1.4.4" 524 | description = "Python implementation of subunit test streaming protocol" 525 | optional = false 526 | python-versions = ">=3.7" 527 | files = [ 528 | {file = "python-subunit-1.4.4.tar.gz", hash = "sha256:1079363131aa1d3f45259237265bc2e61a77e35f20edfb6e3d1d2558a2cdea34"}, 529 | {file = "python_subunit-1.4.4-py3-none-any.whl", hash = "sha256:27b27909cfb20c3aa59add6ff97471afd869daa3c9035ac7ef5eed8dc394f7a5"}, 530 | ] 531 | 532 | [package.dependencies] 533 | iso8601 = "*" 534 | testtools = ">=0.9.34" 535 | 536 | [package.extras] 537 | docs = ["docutils"] 538 | test = ["fixtures", "hypothesis", "testscenarios"] 539 | 540 | [[package]] 541 | name = "pyyaml" 542 | version = "6.0.2" 543 | description = "YAML parser and emitter for Python" 544 | optional = false 545 | python-versions = ">=3.8" 546 | files = [ 547 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 548 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 549 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 550 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 551 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 552 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 553 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 554 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 555 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 556 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 557 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 558 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 559 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 560 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 561 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 562 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 563 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 564 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 565 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 566 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 567 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 568 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 569 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 570 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 571 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 572 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 573 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 574 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 575 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 576 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 577 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 578 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 579 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 580 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 581 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 582 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 583 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 584 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 585 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 586 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 587 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 588 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 589 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 590 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 591 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 592 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 593 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 594 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 595 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 596 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 597 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 598 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 599 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 600 | ] 601 | 602 | [[package]] 603 | name = "repoze-lru" 604 | version = "0.7" 605 | description = "A tiny LRU cache implementation and decorator" 606 | optional = false 607 | python-versions = "*" 608 | files = [ 609 | {file = "repoze.lru-0.7-py3-none-any.whl", hash = "sha256:f77bf0e1096ea445beadd35f3479c5cff2aa1efe604a133e67150bc8630a62ea"}, 610 | {file = "repoze.lru-0.7.tar.gz", hash = "sha256:0429a75e19380e4ed50c0694e26ac8819b4ea7851ee1fc7583c8572db80aff77"}, 611 | ] 612 | 613 | [package.extras] 614 | docs = ["Sphinx"] 615 | testing = ["coverage", "nose"] 616 | 617 | [[package]] 618 | name = "requests" 619 | version = "2.32.3" 620 | description = "Python HTTP for Humans." 621 | optional = false 622 | python-versions = ">=3.8" 623 | files = [ 624 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 625 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 626 | ] 627 | 628 | [package.dependencies] 629 | certifi = ">=2017.4.17" 630 | charset-normalizer = ">=2,<4" 631 | idna = ">=2.5,<4" 632 | urllib3 = ">=1.21.1,<3" 633 | 634 | [package.extras] 635 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 636 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 637 | 638 | [[package]] 639 | name = "routes" 640 | version = "2.5.1" 641 | description = "Routing Recognition and Generation Tools" 642 | optional = false 643 | python-versions = "*" 644 | files = [ 645 | {file = "Routes-2.5.1-py2.py3-none-any.whl", hash = "sha256:fab5a042a3a87778eb271d053ca2723cadf43c95b471532a191a48539cb606ea"}, 646 | {file = "Routes-2.5.1.tar.gz", hash = "sha256:b6346459a15f0cbab01a45a90c3d25caf980d4733d628b4cc1952b865125d053"}, 647 | ] 648 | 649 | [package.dependencies] 650 | "repoze.lru" = ">=0.3" 651 | six = "*" 652 | 653 | [package.extras] 654 | docs = ["Sphinx", "webob"] 655 | middleware = ["webob"] 656 | 657 | [[package]] 658 | name = "setuptools" 659 | version = "75.6.0" 660 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 661 | optional = false 662 | python-versions = ">=3.9" 663 | files = [ 664 | {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, 665 | {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, 666 | ] 667 | 668 | [package.extras] 669 | check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] 670 | core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] 671 | cover = ["pytest-cov"] 672 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 673 | enabler = ["pytest-enabler (>=2.2)"] 674 | test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] 675 | type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] 676 | 677 | [[package]] 678 | name = "six" 679 | version = "1.16.0" 680 | description = "Python 2 and 3 compatibility utilities" 681 | optional = false 682 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 683 | files = [ 684 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 685 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 686 | ] 687 | 688 | [[package]] 689 | name = "stestr" 690 | version = "4.1.0" 691 | description = "A parallel Python test runner built around subunit" 692 | optional = false 693 | python-versions = ">=3.6" 694 | files = [ 695 | {file = "stestr-4.1.0-py3-none-any.whl", hash = "sha256:f319326588c39528b9a69083f0be58ab68d040de2c5dbfdc784b2de028d7d293"}, 696 | {file = "stestr-4.1.0.tar.gz", hash = "sha256:5f61c369eece63c292d13599e12aa158af7685990643f24dd6fa7fabfe34e98a"}, 697 | ] 698 | 699 | [package.dependencies] 700 | cliff = ">=2.8.0" 701 | extras = ">=1.0.0" 702 | fixtures = ">=3.0.0" 703 | pbr = ">=2.0.0,<2.1.0 || >2.1.0,<4.0.0 || >4.0.0,<4.0.1 || >4.0.1,<4.0.2 || >4.0.2,<4.0.3 || >4.0.3" 704 | python-subunit = ">=1.4.0" 705 | PyYAML = ">=3.10.0" 706 | testtools = ">=2.2.0" 707 | tomlkit = ">=0.11.6" 708 | voluptuous = ">=0.8.9" 709 | 710 | [package.extras] 711 | sql = ["subunit2sql (>=1.8.0)"] 712 | test = ["black (>=22.0,<23.0)", "coverage (>=4.0)", "ddt (>=1.0.1)", "doc8 (>=0.8.0)", "hacking (>=3.1.0,<3.2.0)", "iso8601", "sphinx (>2.1.0)"] 713 | 714 | [[package]] 715 | name = "stevedore" 716 | version = "5.4.0" 717 | description = "Manage dynamic plugins for Python applications" 718 | optional = false 719 | python-versions = ">=3.9" 720 | files = [ 721 | {file = "stevedore-5.4.0-py3-none-any.whl", hash = "sha256:b0be3c4748b3ea7b854b265dcb4caa891015e442416422be16f8b31756107857"}, 722 | {file = "stevedore-5.4.0.tar.gz", hash = "sha256:79e92235ecb828fe952b6b8b0c6c87863248631922c8e8e0fa5b17b232c4514d"}, 723 | ] 724 | 725 | [package.dependencies] 726 | pbr = ">=2.0.0" 727 | 728 | [[package]] 729 | name = "tempora" 730 | version = "5.7.0" 731 | description = "Objects and routines pertaining to date and time (tempora)" 732 | optional = false 733 | python-versions = ">=3.8" 734 | files = [ 735 | {file = "tempora-5.7.0-py3-none-any.whl", hash = "sha256:93dac0d33825e66c8314d7bd206b9ecb959075c8728bb05b9b050b2726d0442a"}, 736 | {file = "tempora-5.7.0.tar.gz", hash = "sha256:888190a2dbe3255ff26dfa9fcecb25f4d38434c0f1943cd61de98bb41c410c50"}, 737 | ] 738 | 739 | [package.dependencies] 740 | "jaraco.functools" = ">=1.20" 741 | python-dateutil = "*" 742 | 743 | [package.extras] 744 | doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 745 | test = ["backports.zoneinfo", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-freezer", "pytest-mypy", "pytest-ruff (>=0.2.1)", "types-python-dateutil", "tzdata"] 746 | 747 | [[package]] 748 | name = "testtools" 749 | version = "2.7.2" 750 | description = "Extensions to the Python standard library unit testing framework" 751 | optional = false 752 | python-versions = ">=3.8" 753 | files = [ 754 | {file = "testtools-2.7.2-py3-none-any.whl", hash = "sha256:11712e29cebbe92187c3ad47ace5c32f91e1bb7a9f1ac5e8684c2b01eaa6fd2d"}, 755 | {file = "testtools-2.7.2.tar.gz", hash = "sha256:5be5bbc1f0fa0f8b60aca6ceec07845d41d0c475cf445bfadb4d2c45ec397ea3"}, 756 | ] 757 | 758 | [package.dependencies] 759 | setuptools = {version = "*", markers = "python_version >= \"3.12\""} 760 | 761 | [package.extras] 762 | dev = ["ruff (==0.4.8)"] 763 | test = ["testresources", "testscenarios"] 764 | twisted = ["fixtures", "twisted"] 765 | 766 | [[package]] 767 | name = "tomlkit" 768 | version = "0.13.2" 769 | description = "Style preserving TOML library" 770 | optional = false 771 | python-versions = ">=3.8" 772 | files = [ 773 | {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, 774 | {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, 775 | ] 776 | 777 | [[package]] 778 | name = "urllib3" 779 | version = "2.2.3" 780 | description = "HTTP library with thread-safe connection pooling, file post, and more." 781 | optional = false 782 | python-versions = ">=3.8" 783 | files = [ 784 | {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, 785 | {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, 786 | ] 787 | 788 | [package.extras] 789 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 790 | h2 = ["h2 (>=4,<5)"] 791 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 792 | zstd = ["zstandard (>=0.18.0)"] 793 | 794 | [[package]] 795 | name = "voluptuous" 796 | version = "0.15.2" 797 | description = "Python data validation library" 798 | optional = false 799 | python-versions = ">=3.9" 800 | files = [ 801 | {file = "voluptuous-0.15.2-py3-none-any.whl", hash = "sha256:016348bc7788a9af9520b1764ebd4de0df41fe2138ebe9e06fa036bf86a65566"}, 802 | {file = "voluptuous-0.15.2.tar.gz", hash = "sha256:6ffcab32c4d3230b4d2af3a577c87e1908a714a11f6f95570456b1849b0279aa"}, 803 | ] 804 | 805 | [[package]] 806 | name = "wcwidth" 807 | version = "0.2.13" 808 | description = "Measures the displayed width of unicode strings in a terminal" 809 | optional = false 810 | python-versions = "*" 811 | files = [ 812 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 813 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 814 | ] 815 | 816 | [[package]] 817 | name = "zc-lockfile" 818 | version = "3.0.post1" 819 | description = "Basic inter-process locks" 820 | optional = false 821 | python-versions = ">=3.7" 822 | files = [ 823 | {file = "zc.lockfile-3.0.post1-py3-none-any.whl", hash = "sha256:ddb2d71088c061dc8a5edbaa346b637d742ca1e1564be75cb98e7dcae715de19"}, 824 | {file = "zc.lockfile-3.0.post1.tar.gz", hash = "sha256:adb2ee6d9e6a2333c91178dcb2c9b96a5744c78edb7712dc784a7d75648e81ec"}, 825 | ] 826 | 827 | [package.dependencies] 828 | setuptools = "*" 829 | 830 | [package.extras] 831 | test = ["zope.testing"] 832 | 833 | [metadata] 834 | lock-version = "2.0" 835 | python-versions = "^3.12" 836 | content-hash = "f1721e6ad23bfa2ea0f6e152d78b4ff36f469ec783fa6d2d6d3946406fb680bf" 837 | -------------------------------------------------------------------------------- /printers.yaml: -------------------------------------------------------------------------------- 1 | printers: 2 | - name: bambu 3 | host: bambu.lan 4 | serial: 'SERIAL NUMBER' 5 | key: 'ACCESS KEY' 6 | # timelapse: false 7 | # bed_levelling: true 8 | # flow_cali: true 9 | # vibration_cali: true 10 | # layer_inspect: true 11 | # use_ams: true 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pandaprint" 3 | version = "0.0.0" 4 | description = "" 5 | authors = ["James E. Blair "] 6 | readme = "README.rst" 7 | 8 | [tool.poetry.scripts] 9 | pandaprint = "pandaprint.server:main" 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.12" 13 | cherrypy = "^18.10.0" 14 | routes = "^2.5.1" 15 | pyyaml = "^6.0.2" 16 | paho-mqtt = "^2.1.0" 17 | pbr = "^6.1.0" 18 | 19 | [tool.poetry.group.test.dependencies] 20 | stestr = "^4.1.0" 21 | requests = "^2.32.3" 22 | 23 | [tool.poetry-dynamic-versioning] 24 | enable = true 25 | pattern = "default-unprefixed" 26 | 27 | [build-system] 28 | requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] 29 | build-backend = "poetry_dynamic_versioning.backend" 30 | 31 | [tool.stestr] 32 | test_path = "./tests" 33 | top_dir = "./" 34 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pandaprint-dev/pandaprint/a4a78cc47ccb3deb11d2f314886b889ab8ebb4c9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | # PandaPrint 2 | # Copyright (C) 2024 James E. Blair 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU Affero General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Affero General Public License 15 | # along with this program. If not, see . 16 | 17 | import cherrypy 18 | import collections 19 | import fixtures 20 | import ftplib 21 | import io 22 | import json 23 | import logging 24 | import paho.mqtt.client as mqtt 25 | import pandaprint.server 26 | import requests 27 | import socket 28 | import ssl 29 | import testtools 30 | import threading 31 | import uuid 32 | import zipfile 33 | 34 | 35 | def get_config(): 36 | name = uuid.uuid4().hex 37 | serial = uuid.uuid4().hex 38 | return { 39 | 'printers': [ 40 | { 41 | 'name': name, 42 | 'host': 'localhost', 43 | 'serial': serial, 44 | 'key': '5678', 45 | 'use_ams': True, 46 | } 47 | ], 48 | 'listen-port': 0, 49 | } 50 | 51 | 52 | class PandaServerFixture(fixtures.Fixture): 53 | def __init__(self, config): 54 | self._config = config 55 | super().__init__() 56 | 57 | def _setUp(self): 58 | config = pandaprint.server.PandaConfig() 59 | config.load(self._config) 60 | self.server = pandaprint.server.PandaServer(config) 61 | self.server.start() 62 | self.addCleanup(self.stop) 63 | 64 | while True: 65 | self.port = cherrypy.server.bound_addr[1] 66 | try: 67 | with socket.create_connection(('localhost', self.port)): 68 | break 69 | except ConnectionRefusedError: 70 | pass 71 | 72 | def stop(self): 73 | self.server.stop() 74 | 75 | 76 | class MQTTFixture(fixtures.Fixture): 77 | def _setUp(self): 78 | self.event = threading.Event() 79 | self.topic_messages = collections.defaultdict(list) 80 | self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) 81 | self.client.tls_set(cert_reqs=ssl.CERT_NONE) 82 | self.client.tls_insecure_set(True) 83 | self.client.username_pw_set('bblp', '5678') 84 | self.client.on_connect = self.on_connect 85 | self.client.on_message = self.on_message 86 | self.client.connect('localhost', 8883, 60) 87 | self.client.loop_start() 88 | self.event.wait() 89 | 90 | def on_connect(self, client, userdata, flags, reason_code, properties): 91 | self.client.subscribe('#') 92 | self.event.set() 93 | 94 | def on_message(self, client, userdata, msg): 95 | self.topic_messages[msg.topic].append(msg.payload) 96 | 97 | def get_messages(self, topic): 98 | return self.topic_messages[topic] 99 | 100 | def stop(self): 101 | self.client.disconnect() 102 | self.client.loop_stop() 103 | 104 | 105 | def get_ftp_file(path): 106 | with pandaprint.server.FTPS() as ftp: 107 | ftp.connect(host='localhost', port=990, timeout=30) 108 | ftp.login('bblp', '5678') 109 | ftp.prot_p() 110 | with io.BytesIO() as f: 111 | try: 112 | ftp.retrbinary(f'RETR {path}', f.write) 113 | except ftplib.error_perm: 114 | # No file 115 | return None 116 | return f.getvalue() 117 | 118 | 119 | def make_zip_file(plates=1): 120 | with io.BytesIO() as f: 121 | with zipfile.ZipFile(f, mode='w') as zf: 122 | for pno in range(1, plates+1): 123 | zf.writestr(f'Metadata/plate_{pno}.png', b'PNG') 124 | zf.writestr(f'Metadata/plate_{pno}.gcode', f'G0X{pno}\n'.encode('utf8')) 125 | zf.writestr('3D/3dmodel.model', b'') 126 | zf.writestr('[Content_Types].xml', b'') 127 | zf.writestr('_rels/.rels', b'') 128 | return f.getvalue() 129 | 130 | 131 | class TestServer(testtools.TestCase): 132 | def test_server_version(self): 133 | config = get_config() 134 | name = config['printers'][0]['name'] 135 | server = self.useFixture(PandaServerFixture(config)) 136 | url = f'http://localhost:{server.port}/{name}/api/version' 137 | resp = requests.get(url) 138 | out = resp.json() 139 | self.assertEqual( 140 | out, 141 | { 142 | 'api': '1.1.0', 143 | 'server': '1.1.0', 144 | 'text': 'OctoPrint 1.1.0 (PandaPrint 1.0)' 145 | } 146 | ) 147 | 148 | def test_upload_only(self): 149 | filename = 'test_upload.3mf' 150 | config = get_config() 151 | name = config['printers'][0]['name'] 152 | server = self.useFixture(PandaServerFixture(config)) 153 | url = f'http://localhost:{server.port}/{name}/api/files/local' 154 | 155 | testfile = make_zip_file() 156 | files = {'file': (filename, testfile)} 157 | resp = requests.post(url, files=files) 158 | self.assertEqual(201, resp.status_code) 159 | data = get_ftp_file(f'/model/{filename}') 160 | self.assertEqual(testfile, data) 161 | 162 | def test_upload_multiple(self): 163 | filename = 'test_multiple.3mf' 164 | config = get_config() 165 | name = config['printers'][0]['name'] 166 | server = self.useFixture(PandaServerFixture(config)) 167 | url = f'http://localhost:{server.port}/{name}/api/files/local' 168 | 169 | testfile = make_zip_file(2) 170 | files = {'file': (filename, testfile)} 171 | resp = requests.post(url, files=files) 172 | self.assertEqual(201, resp.status_code) 173 | data = get_ftp_file(f'/model/{filename}') 174 | self.assertIsNone(data) 175 | data = get_ftp_file(f'/model/test_multiple-1.3mf') 176 | with io.BytesIO(data) as f: 177 | with zipfile.ZipFile(f) as zf: 178 | names = zf.namelist() 179 | self.assertIn('3D/3dmodel.model', names) 180 | self.assertIn('Metadata/plate_1.gcode', names) 181 | self.assertIn('Metadata/plate_1.png', names) 182 | self.assertNotIn('Metadata/plate_2.gcode', names) 183 | self.assertNotIn('Metadata/plate_2.png', names) 184 | plate = zf.read('Metadata/plate_1.gcode') 185 | self.assertEqual(b'G0X1\n', plate) 186 | data = get_ftp_file(f'/model/test_multiple-2.3mf') 187 | with io.BytesIO(data) as f: 188 | with zipfile.ZipFile(f) as zf: 189 | names = zf.namelist() 190 | self.assertIn('3D/3dmodel.model', names) 191 | self.assertIn('Metadata/plate_1.gcode', names) 192 | self.assertIn('Metadata/plate_1.png', names) 193 | self.assertNotIn('Metadata/plate_2.gcode', names) 194 | self.assertNotIn('Metadata/plate_2.png', names) 195 | plate = zf.read('Metadata/plate_1.gcode') 196 | self.assertEqual(b'G0X2\n', plate) 197 | 198 | def test_upload_and_print(self): 199 | filename = 'test_print.3mf' 200 | config = get_config() 201 | mqtt_fix = self.useFixture(MQTTFixture()) 202 | name = config['printers'][0]['name'] 203 | serial = config['printers'][0]['serial'] 204 | server = self.useFixture(PandaServerFixture(config)) 205 | url = f'http://localhost:{server.port}/{name}/api/files/local' 206 | 207 | testfile = make_zip_file() 208 | files = {'file': (filename, testfile)} 209 | resp = requests.post(url, files=files, data={'print': 'true'}) 210 | self.assertEqual(201, resp.status_code) 211 | data = get_ftp_file(f'/model/{filename}') 212 | self.assertEqual(testfile, data) 213 | msgs = [json.loads(x) for x in mqtt_fix.get_messages(f'device/{serial}/request')] 214 | self.assertEqual( 215 | [{ 216 | "print": { 217 | "sequence_id": "0", 218 | "command": "project_file", 219 | "param": "Metadata/plate_1.gcode", 220 | "project_id": "0", 221 | "profile_id": "0", 222 | "task_id": "0", 223 | "subtask_id": "0", 224 | "subtask_name": "", 225 | "url": f"file:///sdcard/model/{filename}", 226 | "bed_type": "auto", 227 | "use_ams": True, 228 | } 229 | }], 230 | msgs) 231 | -------------------------------------------------------------------------------- /tools/Dockerfile-ftp: -------------------------------------------------------------------------------- 1 | FROM docker.io/fauria/vsftpd@sha256:6d71d7c7f1b0ab2844ec7dc7999a30aef6d758b6d8179cf5967513f87c79c177 2 | 3 | ADD ./vsftpd_conf/vsftpd.conf /etc/vsftpd/vsftpd.conf 4 | RUN mkdir -p /home/vsftpd/bblp/model && chown ftp.ftp /home/vsftpd/bblp/model 5 | -------------------------------------------------------------------------------- /tools/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mqtt: 3 | image: "docker.io/eclipse-mosquitto:2.0.20" 4 | volumes: 5 | - "./ca:/ca" 6 | - "./mosquitto_conf/:/mosquitto/config/" 7 | ports: 8 | - "8883:8883" 9 | entrypoint: ["/usr/sbin/mosquitto", "-c", "/mosquitto/config/mosquitto.conf"] 10 | ftp: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile-ftp 14 | environment: 15 | FTP_USER: 'bblp' 16 | FTP_PASS: '5678' 17 | LOG_STDOUT: '1' 18 | PASV_MIN_PORT: 10100 19 | PASV_MAX_PORT: 10110 20 | volumes: 21 | - "./ca:/ca" 22 | ports: 23 | - "990:990" 24 | - "10100-10110:10100-10110" 25 | #entrypoint: ["sleep", "900"] 26 | -------------------------------------------------------------------------------- /tools/mosquitto_conf/mosquitto.conf: -------------------------------------------------------------------------------- 1 | listener 8883 2 | cafile /ca/ca.crt 3 | keyfile /ca/server.key 4 | certfile /ca/server.crt 5 | password_file /mosquitto/config/passwords 6 | -------------------------------------------------------------------------------- /tools/mosquitto_conf/passwords: -------------------------------------------------------------------------------- 1 | bblp:$7$101$573kj5iTIR6IP4Fd$+6h+hSUffWwuiJRV7iBN8iRtNV+dZsCC0DloZ2LaPxDk9TwtBaESUkksXAA0FToU8OXsAgEeRbNZfI1C1Wbqag== 2 | -------------------------------------------------------------------------------- /tools/test-setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir ca 4 | openssl req -new -nodes -x509 -days 3650 -extensions v3_ca -keyout ca/ca.key -out ca/ca.crt -subj "/CN=ca" 5 | openssl genrsa -out ca/server.key 2048 6 | openssl req -out ca/server.csr -key ca/server.key -new -nodes -subj "/CN=server" 7 | openssl x509 -req -in ca/server.csr -CA ca/ca.crt -CAkey ca/ca.key -CAcreateserial -out ca/server.crt -days 3650 8 | chmod -R a+r ca/* 9 | 10 | docker compose up -d 11 | -------------------------------------------------------------------------------- /tools/vsftpd_conf/vsftpd.conf: -------------------------------------------------------------------------------- 1 | # Run in the foreground to keep the container running: 2 | background=NO 3 | 4 | # Allow anonymous FTP? (Beware - allowed by default if you comment this out). 5 | anonymous_enable=NO 6 | 7 | # Uncomment this to allow local users to log in. 8 | local_enable=YES 9 | 10 | ## Enable virtual users 11 | guest_enable=YES 12 | 13 | ## Virtual users will use the same permissions as anonymous 14 | virtual_use_local_privs=YES 15 | 16 | # Uncomment this to enable any form of FTP write command. 17 | write_enable=YES 18 | 19 | ## PAM file name 20 | pam_service_name=vsftpd_virtual 21 | 22 | ## Home Directory for virtual users 23 | user_sub_token=$USER 24 | local_root=/home/vsftpd/$USER 25 | 26 | # You may specify an explicit list of local users to chroot() to their home 27 | # directory. If chroot_local_user is YES, then this list becomes a list of 28 | # users to NOT chroot(). 29 | chroot_local_user=YES 30 | 31 | # Workaround chroot check. 32 | # See https://www.benscobie.com/fixing-500-oops-vsftpd-refusing-to-run-with-writable-root-inside-chroot/ 33 | # and http://serverfault.com/questions/362619/why-is-the-chroot-local-user-of-vsftpd-insecure 34 | allow_writeable_chroot=YES 35 | 36 | ## Hide ids from user 37 | hide_ids=YES 38 | 39 | ## Enable logging 40 | xferlog_enable=YES 41 | xferlog_file=/var/log/vsftpd/vsftpd.log 42 | log_ftp_protocol=YES 43 | 44 | ## Enable active mode 45 | #port_enable=YES 46 | #connect_from_port_20=YES 47 | #ftp_data_port=20 48 | 49 | ## Disable seccomp filter sanboxing 50 | seccomp_sandbox=NO 51 | 52 | ## Implicit SSL 53 | ssl_enable=YES 54 | implicit_ssl=YES 55 | allow_anon_ssl=NO 56 | force_local_data_ssl=YES 57 | force_local_logins_ssl=YES 58 | rsa_cert_file=/ca/server.crt 59 | rsa_private_key_file=/ca/server.key 60 | listen_port=990 61 | debug_ssl=YES 62 | ssl_ciphers=HIGH 63 | 64 | ### Variables set at container runtime 65 | --------------------------------------------------------------------------------