├── .flake8 ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .mailmap ├── .pre-commit-config.yaml ├── LICENSE ├── README.rst ├── catalog-info.yaml ├── issue_model.py ├── jira.cfg ├── pyproject.toml ├── pytest_jira.py ├── tests ├── test_jira.py └── test_utils.py ├── tox.ini └── uv.lock /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 80 3 | extend-ignore = E203, W503 4 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: "Publish" 2 | 3 | on: 4 | release: 5 | types: ["published"] 6 | 7 | jobs: 8 | run: 9 | name: "Build and publish release" 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Install uv 16 | uses: astral-sh/setup-uv@v6 17 | with: 18 | enable-cache: true 19 | cache-dependency-glob: uv.lock 20 | 21 | - name: Set up Python 22 | run: uv python install 3.13 23 | 24 | - name: Build 25 | run: uv build 26 | 27 | - name: Publish 28 | run: uv publish -u ${{ secrets.PYPI_USERNAME }} -p ${{ secrets.PYPI_PASSWORD }} 29 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [main] 7 | 8 | jobs: 9 | test: 10 | name: Test with Python ${{ matrix.python }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python: ["3.9", "3.11", "3.13"] 15 | 16 | env: 17 | TEST_JIRA_TOKEN: ${{ secrets.TEST_JIRA_TOKEN }} 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | 23 | - name: Install uv 24 | uses: astral-sh/setup-uv@v6 25 | 26 | - name: Install tox-uv 27 | run: | 28 | uv tool install tox --with tox-uv 29 | 30 | - name: Run tests 31 | run: tox -e ${{ matrix.python }} 32 | 33 | - name: Upload coverage to Codecov 34 | uses: codecov/codecov-action@v5 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} # Not required for public repos 37 | files: coverage.xml 38 | flags: unittests 39 | name: codecov-${{ matrix.python }} 40 | fail_ci_if_error: true 41 | 42 | lint: 43 | name: Lint (pre-commit) 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - name: Check out code 48 | uses: actions/checkout@v4 49 | 50 | - name: Install uv 51 | uses: astral-sh/setup-uv@v6 52 | 53 | - name: Install tox-uv 54 | run: | 55 | uv tool install tox --with tox-uv 56 | 57 | - name: Run lint 58 | run: tox -e lint 59 | 60 | - name: Run pre-commit 61 | run: tox -e pre-commit 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | .eggs 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | ChangeLog 23 | AUTHORS 24 | .cache 25 | 26 | # Installer logs 27 | pip-log.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov 31 | .coverage 32 | .tox 33 | nosetests.xml 34 | 35 | # Translations 36 | *.mo 37 | 38 | # Mr Developer 39 | .mr.developer.cfg 40 | .project 41 | .pydevproject 42 | 43 | # PyTest results 44 | results 45 | coverage.xml 46 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | James Laska 2 | James Laska 3 | James Laska 4 | Lukas Bednar 5 | Vaclav Kondula vkondula 6 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: 24.3.0 4 | hooks: 5 | - id: black 6 | 7 | - repo: https://github.com/PyCQA/flake8 8 | rev: 6.1.0 9 | hooks: 10 | - id: flake8 11 | 12 | - repo: https://github.com/pre-commit/mirrors-isort 13 | rev: v5.10.1 14 | hooks: 15 | - id: isort 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Intro 2 | ===== 3 | 4 | A `pytest `__ plugin for JIRA integration. 5 | 6 | This plugin links tests with JIRA tickets. The plugin behaves similar to 7 | the `pytest-bugzilla `__ 8 | plugin. 9 | 10 | The plugin does not close JIRA tickets, or create them. It just allows 11 | you to link tests to existing tickets. 12 | 13 | Please feel free to contribute by forking and submitting pull requests 14 | or by submitting feature requests or issues to 15 | `issues `__. 16 | 17 | Test results 18 | ------------ 19 | - If the test **unresolved** ... 20 | 21 | - and the *run=False*, the test is **skipped** 22 | 23 | - and the *run=True* or not set, the test is executed and based on it 24 | the result is **xpassed** (e.g. unexpected pass) or **xfailed** (e.g. expected fail). 25 | Interpretation of **xpassed** result depends on the py.test ini-file **xfail_strict** value, 26 | i.e. with *xfail_strict=true* **xpassed** results will fail the test suite. 27 | More information about strict xfail available on the py.test `doc `__ 28 | 29 | - If the test **resolved** ... 30 | 31 | - the test is executed and based on it 32 | the result is **passed** or **failed** 33 | 34 | - If the **skipif** parameter is provided ... 35 | 36 | - with value *False* or *callable returning False-like value* jira marker line is **ignored** 37 | 38 | 39 | **NOTE:** You can set default value for ``run`` parameter globally in config 40 | file (option ``run_test_case``) or from CLI 41 | ``--jira-do-not-run-test-case``. Default value is ``run=True``. 42 | 43 | Marking tests 44 | ------------- 45 | You can specify jira issue ID in docstring or in pytest.mark.jira decorator. 46 | 47 | By default the regular expression pattern for matching jira issue ID is ``[A-Z]+-[0-9]+``, 48 | it can be changed by ``--jira-issue-regex=REGEX`` or in a config file by 49 | ``jira_regex=REGEX``. 50 | 51 | It's also possible to change behavior if issue ID was not found 52 | by setting ``--jira-marker-strategy=STRATEGY`` or in config file 53 | as ``marker_strategy=STRATEGY``. 54 | 55 | Strategies for dealing with issue IDs that were not found: 56 | 57 | - **open** - issue is considered as open (default) 58 | - **strict** - raise an exception 59 | - **ignore** - issue id is ignored 60 | - **warn** - write error message and ignore 61 | 62 | Issue ID in decorator 63 | ~~~~~~~~~~~~~~~~~~~~~ 64 | If you use decorator you can specify optional parameters ``run`` and ``skipif``. 65 | If ``run`` is false and issue is unresolved, the test will be skipped. 66 | If ``skipif`` is is false jira marker line will be ignored. 67 | 68 | .. code:: python 69 | 70 | @pytest.mark.jira("ORG-1382", run=False) 71 | def test_skip(): # will be skipped if unresolved 72 | assert False 73 | 74 | @pytest.mark.jira("ORG-1382") 75 | def test_xfail(): # will run and xfail if unresolved 76 | assert False 77 | 78 | @pytest.mark.jira("ORG-1382", skipif=False) 79 | def test_fail(): # will run and fail as jira marker is ignored 80 | assert False 81 | 82 | Using lambda value for skipif 83 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 84 | You can use lambda value for ``skipif`` parameter. Lambda function must take 85 | issue JSON as input value and return boolean-like value. If any JIRA ID 86 | gets False-like value marker for that issue will be ignored. 87 | 88 | .. code:: python 89 | 90 | @pytest.mark.jira("ORG-1382", skipif=lambda i: 'my component' in i['components']) 91 | def test_fail(): # Test will run if 'my component' is not present in Jira issue's components 92 | assert False 93 | 94 | @pytest.mark.jira("ORG-1382", "ORG-1412", skipif=lambda i: 'to do' == i['status']) 95 | def test_fail(): # Test will run if either of JIRA issue's status differs from 'to do' 96 | assert False 97 | 98 | 99 | Issue ID in docstring 100 | ~~~~~~~~~~~~~~~~~~~~~ 101 | 102 | You can disable searching for issue ID in doc string by using 103 | ``--jira-disable-docs-search`` parameter or by ``docs_search=False`` 104 | in ``jira.cfg``. 105 | 106 | .. code:: python 107 | 108 | def test_xpass(): # will run and xpass if unresolved 109 | """issue: ORG-1382""" 110 | assert True 111 | 112 | Status evaluation 113 | ----------------- 114 | 115 | Issues are considered as **resolved** if their status matches 116 | ``resolved_statuses``. By default it is ``Resolved`` or ``Closed``. 117 | 118 | You can set your own custom resolved statuses on command line 119 | ``--jira-resolved-statuses``, or in config file. 120 | 121 | If you specify components (in command line or jira.cfg), open issues will be considered 122 | **unresolved** only if they are also open for at least one used component. 123 | 124 | If you specify version, open issues will be **unresolved** only if they also affects your version. 125 | Even when the issue is closed, but your version was affected and it was not fixed for your version, 126 | the issue will be considered **unresolved**. 127 | 128 | If you specify fixed resolutions closed issues will be **unresolved** if they do not also have a **resolved** resolution. 129 | 130 | Fixture usage 131 | ------------- 132 | 133 | Besides a test marker, you can also use the added ``jira_issue`` fixture. This enables examining issue status mid test 134 | and not just at the beginning of a test. The fixture return a boolean representing the state of the issue. 135 | If the issue isn't found, or the jira plugin isn't loaded, it returns ``None``. 136 | 137 | .. code:: python 138 | 139 | NICE_ANIMALS = ["bird", "cat", "dog"] 140 | 141 | def test_stuff(jira_issue): 142 | animals = ["dog", "cat"] 143 | for animal in animals: 144 | if animal == "dog" and jira_issue("ORG-1382") is True: 145 | print("Issue is still open, cannot check for dogs!") 146 | continue 147 | assert animal in NICE_ANIMALS 148 | 149 | Requires 150 | ======== 151 | 152 | - pytest >= 2.2.3 153 | - requests >= 2.13.0 154 | - six 155 | - retry2>=0.9.5 156 | - marshmallow>=3.2.0 157 | 158 | Installation 159 | ============ 160 | 161 | ``pip install pytest_jira`` 162 | 163 | Usage 164 | ===== 165 | 166 | 167 | 1. Create a ``jira.cfg`` and put it at least in one of following places. 168 | 169 | * /etc/jira.cfg 170 | * ~/jira.cfg 171 | * tests\_root\_dir/jira.cfg 172 | * tests\_test\_dir/jira.cfg 173 | 174 | The configuration file is loaded in that order mentioned above. 175 | That means that first options from global configuration are loaded, 176 | and might be overwritten by options from user's home directory and 177 | finally these might be overwritten by options from test's root directory. 178 | 179 | See example bellow, you can use it as template, and update it according 180 | to your needs. 181 | 182 | .. code:: ini 183 | 184 | [DEFAULT] 185 | url = https://jira.atlassian.com 186 | username = USERNAME (or blank for no authentication) 187 | password = PASSWORD (or blank for no authentication) 188 | token = TOKEN (either use token or username and password) 189 | # ssl_verification = True/False 190 | # version = foo-1.0 191 | # components = com1,second component,com3 192 | # strategy = [open|strict|warn|ignore] (dealing with not found issues) 193 | # docs_search = False (disable searching for issue id in docs) 194 | # issue_regex = REGEX (replace default `[A-Z]+-[0-9]+` regular expression) 195 | # resolved_statuses = comma separated list of statuses (closed, resolved) 196 | # resolved_resolutions = comma separated list of resolutions (done, fixed) 197 | # run_test_case = True (default value for 'run' parameter) 198 | # error_strategy [strict|skip|ignore] Choose how to handle connection errors 199 | # return_jira_metadata = False (return Jira issue with metadata instead of boolean result) 200 | # connection_retry_total = 5 (number of retries) 201 | # connection_retry_backoff_factor = 0.2 ( connection retry backoff factor) 202 | 203 | Alternatively, you can set the url, password, username and token fields using relevant environment variables: 204 | 205 | .. code:: sh 206 | 207 | export PYTEST_JIRA_URL="https://..." 208 | export PYTEST_JIRA_PASSWORD="FOO" 209 | export PYTEST_JIRA_USERNAME="BAR" 210 | export PYTEST_JIRA_TOKEN="TOKEN" 211 | 212 | Configuration options can be overridden with command line options as well. 213 | For all available command line options run following command. 214 | 215 | .. code:: sh 216 | 217 | py.test --help 218 | 219 | 2. Mark your tests with jira marker and issue id. 220 | 221 | ``@pytest.mark.jira('issue_id')`` 222 | 223 | You can put Jira ID into doc string of test case as well. 224 | 225 | 3. Run py.test with jira option to enable the plugin. 226 | 227 | ``py.test --jira`` 228 | 229 | Tests 230 | ===== 231 | 232 | In order to execute tests run 233 | 234 | .. code:: sh 235 | 236 | $ tox 237 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: backstage.io/v1alpha1 3 | kind: Component 4 | metadata: 5 | name: pytest-jira 6 | title: PyTest Jira Plugin 7 | description: This plugin allows you to skip test cases based on the status of related Jira issue. 8 | links: 9 | - title: docs 10 | url: https://github.com/rhevm-qe-automation/pytest_jira/blob/master/README.rst 11 | icon: docs 12 | - title: code 13 | url: https://github.com/rhevm-qe-automation/pytest_jira 14 | icon: github 15 | - title: pypi 16 | url: https://pypi.org/project/pytest-jira 17 | tags: 18 | # Ecosystem 19 | - public 20 | - test-framework 21 | # Programming Languages 22 | - python 23 | # Tool / Service 24 | - jira 25 | - pytest 26 | namespace: quality-community 27 | annotations: 28 | # For GitHub repositories 29 | github.com/project-slug: rhevm-qe-automation/pytest_jira 30 | spec: 31 | type: library 32 | lifecycle: production 33 | owner: group:redhat/cnv-qe-infra 34 | -------------------------------------------------------------------------------- /issue_model.py: -------------------------------------------------------------------------------- 1 | from marshmallow import EXCLUDE, Schema, fields 2 | 3 | 4 | class Basic(Schema): 5 | id = fields.String() 6 | name = fields.String() 7 | 8 | 9 | class Components(Schema): 10 | name = fields.String() 11 | 12 | 13 | class Version(Schema): 14 | name = fields.String() 15 | 16 | 17 | class Priority(Basic): 18 | pass 19 | 20 | 21 | class Resolution(Basic): 22 | description = fields.String() 23 | 24 | 25 | class Status(Basic): 26 | description = fields.String() 27 | 28 | 29 | class Type(Basic): 30 | subtask = fields.Boolean() 31 | 32 | 33 | class User(Schema): 34 | key = fields.String() 35 | name = fields.String() 36 | displayName = fields.String() 37 | active = fields.Boolean() 38 | 39 | 40 | class JiraIssueSchema(Schema): 41 | class Meta: 42 | unknown = EXCLUDE # exclude unknown fields 43 | 44 | # Default set to None for fields that are not filled 45 | issuetype = fields.Nested(Type(), default=None) 46 | status = fields.Nested(Status(), default=None) 47 | priority = fields.Nested(Priority(), default=None) 48 | reporter = fields.Nested(User(), default=None) 49 | creator = fields.Nested(User(), default=None) 50 | versions = fields.List(fields.Nested(Version()), default=None) 51 | summary = fields.String(default=None) 52 | updated = fields.String(default=None) 53 | created = fields.String(default=None) 54 | resolutiondate = fields.String(default=None) 55 | duedate = fields.String(default=None) 56 | fixVersions = fields.List(fields.Nested(Version()), default=None) 57 | components = fields.List(fields.Nested(Components()), default=None) 58 | resolution = fields.Nested(Resolution(), default=None) 59 | assignee = fields.Nested(User(), default=None) 60 | labels = fields.List(fields.String()) 61 | 62 | 63 | class JiraIssue: 64 | def __init__(self, issue_id, **entries): 65 | self.__dict__.update(entries) 66 | self.issue_id = issue_id 67 | 68 | def __repr__(self): 69 | return "JiraIssue {}".format(self.issue_id) 70 | 71 | @property 72 | def components_list(self): 73 | return set(component["name"] for component in self.components) 74 | 75 | @property 76 | def fixed_versions(self): 77 | return set(version["name"] for version in self.fix_versions) 78 | 79 | @property 80 | def versions_list(self): 81 | return set(version["name"] for version in self.versions) 82 | -------------------------------------------------------------------------------- /jira.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | url = https://issues.jboss.org 3 | username = 4 | password = 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling", "uv-dynamic-versioning"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pytest-jira" 7 | dynamic = ["version"] 8 | description = "py.test JIRA integration plugin, using markers" 9 | readme = "README.rst" 10 | requires-python = ">=3.9" 11 | license = { text = "GPL-2.0-only" } 12 | authors = [ 13 | { name = "James Laska", email = "james.laska@gmail.com" }, 14 | { name = "Lukas Bednar", email = "lukyn17@gmail.com" } 15 | ] 16 | keywords = ["pytest", "jira", "plugin"] 17 | classifiers = [ 18 | "Development Status :: 4 - Beta", 19 | "Environment :: Plugins", 20 | "Framework :: Pytest", 21 | "Intended Audience :: Developers", 22 | "Operating System :: POSIX", 23 | "Programming Language :: Python :: 3", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | "Programming Language :: Python :: 3.11", 27 | "Programming Language :: Python :: 3.12", 28 | "Programming Language :: Python :: 3.13", 29 | "Topic :: Software Development :: Testing", 30 | "Topic :: Software Development :: Quality Assurance", 31 | "Topic :: Utilities" 32 | ] 33 | 34 | dependencies = [ 35 | "pytest>=2.2.4", 36 | "six", 37 | "requests>=2.13.0", 38 | "retry2>=0.9.5", 39 | "marshmallow==3.26.1", 40 | "packaging", 41 | ] 42 | 43 | [project.urls] 44 | Homepage = "https://github.com/rhevm-qe-automation/pytest_jira" 45 | 46 | [dependency-groups] 47 | tests = [ 48 | "pytest", 49 | "pytest-cov", 50 | "flake8", 51 | "coverage", 52 | "isort", 53 | "pre-commit", 54 | ] 55 | 56 | 57 | [project.entry-points."pytest11"] 58 | pytest_jira = "pytest_jira" 59 | 60 | 61 | [tool.black] 62 | line-length = 80 63 | [tool.isort] 64 | profile = "black" 65 | line_length = 80 66 | known_first_party = ["pytest_jira"] 67 | include_trailing_comma = true 68 | use_parentheses = true 69 | 70 | [tool.hatch.version] 71 | source = "uv-dynamic-versioning" 72 | 73 | [tool.hatch.build.targets.wheel] 74 | packages = ["pytest_jira*", "issue_model*"] 75 | -------------------------------------------------------------------------------- /pytest_jira.py: -------------------------------------------------------------------------------- 1 | """ 2 | This plugin integrates pytest with jira; allowing the tester to mark a test 3 | with a bug id. The test will then be skipped unless the issue status matches 4 | at least one of resolved statuses. 5 | 6 | You must set the url either at the command line or in jira.cfg. 7 | 8 | Author: James Laska 9 | """ 10 | 11 | import os 12 | import re 13 | import sys 14 | from json import JSONDecodeError 15 | 16 | import pytest 17 | import requests 18 | import six 19 | import urllib3 20 | from packaging.version import Version 21 | from retry import retry 22 | 23 | from issue_model import JiraIssue, JiraIssueSchema 24 | 25 | DEFAULT_RESOLVE_STATUSES = "closed", "resolved" 26 | DEFAULT_RUN_TEST_CASE = True 27 | CONNECTION_SKIP_MESSAGE = "Jira connection issue, skipping test: %s" 28 | CONNECTION_ERROR_FLAG_NAME = "--jira-connection-error-strategy" 29 | STRICT = "strict" 30 | SKIP = "skip" 31 | IGNORE = "ignore" 32 | PLUGIN_NAME = "jira_plugin" 33 | URL_ENV_VAR = "PYTEST_JIRA_URL" 34 | PASSWORD_ENV_VAR = "PYTEST_JIRA_PASSWORD" 35 | USERNAME_ENV_VAR = "PYTEST_JIRA_USERNAME" 36 | TOKEN_ENV_VAR = "PYTEST_JIRA_TOKEN" 37 | 38 | 39 | class JiraHooks(object): 40 | def __init__( 41 | self, 42 | connection, 43 | marker, 44 | version=None, 45 | components=None, 46 | resolved_statuses=None, 47 | resolved_resolutions=None, 48 | run_test_case=DEFAULT_RUN_TEST_CASE, 49 | strict_xfail=False, 50 | connection_error_strategy=None, 51 | return_jira_metadata=False, 52 | ): 53 | self.conn = connection 54 | self.mark = marker 55 | self.components = set(components) if components else None 56 | self.version = version 57 | if resolved_statuses: 58 | self.resolved_statuses = resolved_statuses 59 | else: 60 | self.resolved_statuses = DEFAULT_RESOLVE_STATUSES 61 | if resolved_resolutions: 62 | self.resolved_resolutions = resolved_resolutions 63 | else: 64 | self.resolved_resolutions = [] 65 | self.run_test_case = run_test_case 66 | self.connection_error_strategy = connection_error_strategy 67 | # Speed up JIRA lookups for duplicate issues 68 | self.issue_cache = dict() 69 | 70 | self.strict_xfail = strict_xfail 71 | self.return_jira_metadata = return_jira_metadata 72 | 73 | def is_issue_resolved(self, issue_id): 74 | """ 75 | Returns whether the provided issue ID is resolved (True|False). Will 76 | cache issues to speed up subsequent calls for the same issue. 77 | """ 78 | # Access Jira issue (may be cached) 79 | if issue_id not in self.issue_cache: 80 | try: 81 | self.issue_cache[issue_id] = self.conn.get_issue( 82 | issue_id, self.return_jira_metadata 83 | ) 84 | except requests.RequestException as e: 85 | if ( 86 | not hasattr(e.response, "status_code") 87 | or not e.response.status_code == 404 88 | ): 89 | raise 90 | self.issue_cache[issue_id] = self.mark.get_default(issue_id) 91 | if self.return_jira_metadata: 92 | issue = JiraIssueSchema().dump(self.issue_cache[issue_id]) 93 | return JiraIssue(issue_id, **issue) 94 | 95 | # Skip test if issue remains unresolved 96 | if self.issue_cache[issue_id] is None: 97 | return True 98 | if self.issue_cache[issue_id]["status"] in self.resolved_statuses and ( 99 | # Issue is resolved if resolutions are not specified 100 | # Or if the issue's resolution mathces a resolved_resolution 101 | len(self.resolved_resolutions) == 0 102 | or self.issue_cache[issue_id]["resolution"] 103 | in self.resolved_resolutions 104 | ): 105 | return self.fixed_in_version(issue_id) 106 | else: 107 | return not self.is_affected(issue_id) 108 | 109 | def get_marker(self, item): 110 | if Version(pytest.__version__) >= Version("3.6.0"): 111 | return item.get_closest_marker("jira") 112 | else: 113 | return item.keywords.get("jira") 114 | 115 | def pytest_collection_modifyitems(self, config, items): 116 | for item in items: 117 | try: 118 | jira_ids = self.mark.get_jira_issues(item) 119 | except Exception as exc: 120 | pytest.exit(exc) 121 | 122 | jira_run = self.run_test_case 123 | 124 | marker = self.get_marker(item) 125 | if marker: 126 | jira_run = marker.kwargs.get("run", jira_run) 127 | 128 | for issue_id, skipif in jira_ids: 129 | try: 130 | issue = self.is_issue_resolved(issue_id) 131 | if config.option.return_jira_metadata: 132 | # Get the resolution resolution status 133 | issue = issue.resolution in self.resolved_resolutions 134 | if not issue: 135 | if callable(skipif): 136 | if not skipif(self.issue_cache[issue_id]): 137 | continue 138 | else: 139 | if not skipif: 140 | continue 141 | reason = "%s/browse/%s" % ( 142 | self.conn.get_url(), 143 | issue_id, 144 | ) 145 | if jira_run: 146 | item.add_marker(pytest.mark.xfail(reason=reason)) 147 | else: 148 | item.add_marker(pytest.mark.skip(reason=reason)) 149 | except requests.RequestException as e: 150 | if self.connection_error_strategy == STRICT: 151 | raise 152 | elif self.connection_error_strategy == SKIP: 153 | item.add_marker( 154 | pytest.mark.skip(reason=CONNECTION_SKIP_MESSAGE % e) 155 | ) 156 | else: 157 | return 158 | 159 | def fixed_in_version(self, issue_id): 160 | """ 161 | Return True if: 162 | jira_product_version was not specified 163 | OR issue was fixed for jira_product_version 164 | else return False 165 | """ 166 | if not self.version: 167 | return True 168 | affected = self.issue_cache[issue_id].get("versions") 169 | fixed = self.issue_cache[issue_id].get("fixed_versions") 170 | return self.version not in (affected - fixed) 171 | 172 | def is_affected(self, issue_id): 173 | """ 174 | Return True if: 175 | at least one component affected (or not specified) 176 | version is affected (or not specified) 177 | else return False 178 | """ 179 | return self._affected_version(issue_id) and self._affected_components( 180 | issue_id 181 | ) 182 | 183 | def _affected_version(self, issue_id): 184 | affected = self.issue_cache[issue_id].get("versions") 185 | if not self.version or not affected: 186 | return True 187 | return self.version in affected 188 | 189 | def _affected_components(self, issue_id): 190 | affected = self.issue_cache[issue_id].get("components") 191 | if not self.components or not affected: 192 | return True 193 | return bool(self.components.intersection(affected)) 194 | 195 | 196 | class JiraSiteConnection(object): 197 | def __init__( 198 | self, 199 | url, 200 | username=None, 201 | password=None, 202 | verify=True, 203 | token=None, 204 | ): 205 | self.url = url 206 | self.username = username 207 | self.password = password 208 | self.verify = verify 209 | self.token = token 210 | 211 | self.is_connected = False 212 | 213 | if self.token: 214 | token_bearer = f"Bearer {self.token}" 215 | self.headers = {"Authorization": token_bearer} 216 | 217 | # Setup basic_auth 218 | elif self.username and self.password: 219 | self.basic_auth = (self.username, self.password) 220 | 221 | else: 222 | self.basic_auth = None 223 | 224 | self.session = requests.Session() 225 | 226 | def setup_retries(self, total, backoff_factor): 227 | retries = urllib3.Retry( 228 | total=total, 229 | backoff_factor=backoff_factor, 230 | respect_retry_after_header=True, # use retry-after header 231 | status_forcelist=urllib3.Retry.RETRY_AFTER_STATUS_CODES, 232 | allowed_methods={ 233 | "GET", 234 | }, 235 | ) 236 | self.session.mount( 237 | self.url, requests.adapters.HTTPAdapter(max_retries=retries) 238 | ) 239 | 240 | def _jira_request(self, url, method="get", **kwargs): 241 | if "verify" not in kwargs: 242 | kwargs["verify"] = self.verify 243 | 244 | if self.token: 245 | rsp = self.session.request( 246 | method, url, headers=self.headers, **kwargs 247 | ) 248 | 249 | elif self.basic_auth: 250 | rsp = self.session.request( 251 | method, url, auth=self.basic_auth, **kwargs 252 | ) 253 | 254 | else: 255 | rsp = self.session.request(method, url, **kwargs) 256 | rsp.raise_for_status() 257 | return rsp 258 | 259 | def check_connection(self): 260 | # This URL work for both anonymous and logged in users 261 | auth_url = "{url}/rest/api/2/mypermissions".format(url=self.url) 262 | r = self._jira_request( 263 | auth_url, params={"permissions": "BROWSE_PROJECTS"} 264 | ) 265 | 266 | # For some reason in case on invalid credentials the status is still 267 | # 200 but the body is empty 268 | if not r.text: 269 | raise Exception( 270 | "Could not connect to {url}. Invalid credentials".format( 271 | url=self.url 272 | ) 273 | ) 274 | 275 | # If the user does not have sufficient permissions to browse issues 276 | else: 277 | try: 278 | response = r.json() 279 | except JSONDecodeError: 280 | raise Exception( 281 | "Unable to determine permission for the user" 282 | ": {text}".format(text=r.text) 283 | ) 284 | try: 285 | if not response["permissions"]["BROWSE_PROJECTS"][ 286 | "havePermission" 287 | ]: 288 | raise Exception( 289 | "Current user does not have sufficient permissions" 290 | " to view issue" 291 | ) 292 | except KeyError: 293 | raise Exception( 294 | "Unable to determine permission for the user" 295 | ": {response}".format(response=response) 296 | ) 297 | self.is_connected = True 298 | return True 299 | 300 | @retry(JSONDecodeError, tries=3, delay=2) 301 | def get_issue(self, issue_id, return_jira_metadata): 302 | if not self.is_connected: 303 | self.check_connection() 304 | issue_url = "{url}/rest/api/2/issue/{issue_id}".format( 305 | url=self.url, issue_id=issue_id 306 | ) 307 | issue = self._jira_request(issue_url).json() 308 | field = issue["fields"] 309 | return ( 310 | field 311 | if return_jira_metadata 312 | else { 313 | "components": set( 314 | c["name"] for c in field.get("components", set()) 315 | ), 316 | "versions": set( 317 | v["name"] for v in field.get("versions", set()) 318 | ), 319 | "fixed_versions": set( 320 | v["name"] for v in field.get("fixVersions", set()) 321 | ), 322 | "status": field["status"]["name"].lower(), 323 | "resolution": ( 324 | field["resolution"]["name"].lower() 325 | if field["resolution"] 326 | else None 327 | ), 328 | } 329 | ) 330 | 331 | def get_url(self): 332 | return self.url 333 | 334 | 335 | class JiraMarkerReporter(object): 336 | issue_re = r"([A-Z]+-[0-9]+)" 337 | 338 | def __init__(self, strategy, docs, pattern): 339 | self.issue_pattern = re.compile(pattern or self.issue_re) 340 | self.docs = docs 341 | self.strategy = strategy.lower() 342 | 343 | def _get_marks(self, item): 344 | marks = [] 345 | if Version(pytest.__version__) >= Version("3.6.0"): 346 | for mark in item.iter_markers("jira"): 347 | marks.append(mark) 348 | else: 349 | if "jira" in item.keywords: 350 | marker = item.keywords["jira"] 351 | # process markers independently 352 | if not isinstance(marker, (list, tuple)): 353 | marker = [marker] 354 | for mark in marker: 355 | marks.append(mark) 356 | return marks 357 | 358 | def get_jira_issues(self, item): 359 | jira_ids = [] 360 | for mark in self._get_marks(item): 361 | skip_if = mark.kwargs.get("skipif", True) 362 | 363 | if len(mark.args) == 0: 364 | raise TypeError("JIRA marker requires one, or more, arguments") 365 | 366 | for arg in mark.args: 367 | jira_ids.append((arg, skip_if)) 368 | 369 | # Was a jira issue referenced in the docstr? 370 | if self.docs and item.function.__doc__: 371 | jira_ids.extend( 372 | [ 373 | (m.group(0), True) 374 | for m in self.issue_pattern.finditer(item.function.__doc__) 375 | ] 376 | ) 377 | 378 | # Filter valid issues, and return unique issues 379 | for jid, _ in set(jira_ids): 380 | if not self.issue_pattern.match(jid): 381 | raise ValueError( 382 | "JIRA marker argument `%s` does not match pattern" % jid 383 | ) 384 | return list(set(jira_ids)) 385 | 386 | def get_default(self, jid): 387 | if self.strategy == "open": 388 | return {"status": "open"} 389 | if self.strategy == "strict": 390 | raise ValueError("JIRA marker argument `%s` was not found" % jid) 391 | if self.strategy == "warn": 392 | sys.stderr.write("JIRA marker argument `%s` was not found" % jid) 393 | return None 394 | 395 | 396 | def _get_value(config, section, name, default=None): 397 | if config.has_option(section, name): 398 | return config.get(section, name) 399 | return default 400 | 401 | 402 | def _get_bool(config, section, name, default=False): 403 | if config.has_option(section, name): 404 | return config.getboolean(section, name) 405 | return default 406 | 407 | 408 | def pytest_addoption(parser): 409 | """ 410 | Add a options section to py.test --help for jira integration. 411 | Parse configuration file, jira.cfg and / or the command line options 412 | passed. 413 | 414 | :param parser: Command line options. 415 | """ 416 | group = parser.getgroup("JIRA integration") 417 | group.addoption( 418 | "--jira", 419 | action="store_true", 420 | default=False, 421 | dest="jira", 422 | help="Enable JIRA integration.", 423 | ) 424 | 425 | # FIXME - Change to a credentials.yaml ? 426 | config = six.moves.configparser.ConfigParser() 427 | config.read( 428 | [ 429 | os.path.join("/", "etc", "jira.cfg"), 430 | os.path.join(str(parser.extra_info["rootdir"]), "jira.cfg"), 431 | os.path.expanduser(os.path.join("~", "jira.cfg")), 432 | "jira.cfg", 433 | ] 434 | ) 435 | 436 | group.addoption( 437 | "--jira-url", 438 | action="store", 439 | dest="jira_url", 440 | default=_get_value(config, "DEFAULT", "url"), 441 | metavar="url", 442 | help="JIRA url (default: %(default)s)", 443 | ) 444 | group.addoption( 445 | "--jira-user", 446 | action="store", 447 | dest="jira_username", 448 | default=_get_value(config, "DEFAULT", "username"), 449 | metavar="username", 450 | help="JIRA username (default: %(default)s)", 451 | ) 452 | group.addoption( 453 | "--jira-password", 454 | action="store", 455 | dest="jira_password", 456 | default=_get_value(config, "DEFAULT", "password"), 457 | metavar="password", 458 | help="JIRA password.", 459 | ) 460 | group.addoption( 461 | "--jira-token", 462 | action="store", 463 | dest="jira_token", 464 | default=_get_value(config, "DEFAULT", "token"), 465 | metavar="token", 466 | help="JIRA token.", 467 | ) 468 | group.addoption( 469 | "--jira-no-ssl-verify", 470 | action="store_false", 471 | dest="jira_verify", 472 | default=_get_bool( 473 | config, 474 | "DEFAULT", 475 | "ssl_verification", 476 | True, 477 | ), 478 | help="Disable SSL verification to Jira", 479 | ) 480 | group.addoption( 481 | "--jira-components", 482 | action="store", 483 | nargs="+", 484 | dest="jira_components", 485 | default=_get_value(config, "DEFAULT", "components", ""), 486 | help="Used components", 487 | ) 488 | group.addoption( 489 | "--jira-product-version", 490 | action="store", 491 | dest="jira_product_version", 492 | default=_get_value(config, "DEFAULT", "version"), 493 | help="Used version", 494 | ) 495 | group.addoption( 496 | "--jira-marker-strategy", 497 | action="store", 498 | dest="jira_marker_strategy", 499 | default=_get_value(config, "DEFAULT", "marker_strategy", "open"), 500 | choices=["open", "strict", "ignore", "warn"], 501 | help="""Action if issue ID was not found 502 | open - issue is considered as open (default) 503 | strict - raise an exception 504 | ignore - issue id is ignored 505 | warn - write error message and ignore 506 | """, 507 | ) 508 | group.addoption( 509 | "--jira-disable-docs-search", 510 | action="store_false", 511 | dest="jira_docs", 512 | default=_get_bool(config, "DEFAULT", "docs_search", True), 513 | help="Issue ID in doc strings will be ignored", 514 | ) 515 | group.addoption( 516 | "--jira-issue-regex", 517 | action="store", 518 | dest="jira_regex", 519 | default=_get_value(config, "DEFAULT", "issue_regex"), 520 | help="Replace default `[A-Z]+-[0-9]+` regular expression", 521 | ) 522 | group.addoption( 523 | "--jira-resolved-statuses", 524 | action="store", 525 | dest="jira_resolved_statuses", 526 | default=_get_value( 527 | config, 528 | "DEFAULT", 529 | "resolved_statuses", 530 | ",".join(DEFAULT_RESOLVE_STATUSES), 531 | ), 532 | help="Comma separated list of resolved statuses (closed, " "resolved)", 533 | ) 534 | group.addoption( 535 | "--jira-resolved-resolutions", 536 | action="store", 537 | dest="jira_resolved_resolutions", 538 | default=_get_value(config, "DEFAULT", "resolved_resolutions"), 539 | help="Comma separated list of resolved resolutions (done, " "fixed)", 540 | ) 541 | group.addoption( 542 | "--jira-do-not-run-test-case", 543 | action="store_false", 544 | dest="jira_run_test_case", 545 | default=_get_bool( 546 | config, 547 | "DEFAULT", 548 | "run_test_case", 549 | DEFAULT_RUN_TEST_CASE, 550 | ), 551 | help="If set and test is marked by Jira plugin, such " 552 | "test case is not executed.", 553 | ) 554 | group.addoption( 555 | CONNECTION_ERROR_FLAG_NAME, 556 | action="store", 557 | dest="jira_connection_error_strategy", 558 | default=_get_value(config, "DEFAULT", "error_strategy", "strict"), 559 | choices=[STRICT, SKIP, IGNORE], 560 | help="""Action if there is a connection issue 561 | strict - raise an exception 562 | ignore - marker is ignored 563 | skip - skip any test that has a marker 564 | """, 565 | ) 566 | group.addoption( 567 | "--jira-connection-retry-total", 568 | action="store", 569 | type=int, 570 | dest="jira_connection_retry_total", 571 | default=_get_value(config, "DEFAULT", "connection_retry_total", 5), 572 | help="Number of connection retries", 573 | ) 574 | group.addoption( 575 | "--jira-connection-retry-backoff-factor", 576 | action="store", 577 | type=float, 578 | dest="jira_connection_retry_backoff_factor", 579 | default=_get_value( 580 | config, "DEFAULT", "connection_retry_backoff_factor", 0.2 581 | ), 582 | help="Number of connection retries", 583 | ) 584 | group.addoption( 585 | "--jira-return-metadata", 586 | action="store_true", 587 | dest="return_jira_metadata", 588 | default=_get_value(config, "DEFAULT", "return_jira_metadata"), 589 | help="If set, will return Jira issue with ticket metadata", 590 | ) 591 | 592 | 593 | def pytest_configure(config): 594 | """ 595 | If jira is enabled, setup a session 596 | with jira_url. 597 | 598 | :param config: configuration object 599 | """ 600 | config.addinivalue_line( 601 | "markers", 602 | "jira([issue_id,...], run=True): xfail the test if the provided JIRA " 603 | "issue(s) remains unresolved. When 'run' is True, the test will be " 604 | "executed. If a failure occurs, the test will xfail. " 605 | "When 'run' is False, the test will be skipped prior to execution. " 606 | "See https://github.com/rhevm-qe-automation/pytest_jira", 607 | ) 608 | components = config.getvalue("jira_components") 609 | if isinstance(components, six.string_types): 610 | components = [c for c in components.split(",") if c] 611 | 612 | resolved_statuses = config.getvalue("jira_resolved_statuses") 613 | if isinstance(resolved_statuses, six.string_types): 614 | resolved_statuses = [ 615 | s.strip().lower() for s in resolved_statuses.split(",") if s.strip() 616 | ] 617 | if not resolved_statuses: 618 | resolved_statuses = list(DEFAULT_RESOLVE_STATUSES) 619 | 620 | resolved_resolutions = config.getvalue("jira_resolved_resolutions") 621 | if isinstance(resolved_resolutions, six.string_types): 622 | resolved_resolutions = [ 623 | s.strip().lower() 624 | for s in resolved_resolutions.split(",") 625 | if s.strip() 626 | ] 627 | if not resolved_resolutions: 628 | resolved_resolutions = [] 629 | 630 | if config.getvalue("jira") and ( 631 | os.getenv(URL_ENV_VAR) or config.getvalue("jira_url") 632 | ): 633 | jira_connection = JiraSiteConnection( 634 | os.getenv(URL_ENV_VAR) or config.getvalue("jira_url"), 635 | os.getenv(USERNAME_ENV_VAR) or config.getvalue("jira_username"), 636 | os.getenv(PASSWORD_ENV_VAR) or config.getvalue("jira_password"), 637 | config.getvalue("jira_verify"), 638 | os.getenv(TOKEN_ENV_VAR) or config.getvalue("jira_token"), 639 | ) 640 | jira_connection.setup_retries( 641 | config.getvalue("jira_connection_retry_total"), 642 | config.getvalue("jira_connection_retry_backoff_factor"), 643 | ) 644 | jira_marker = JiraMarkerReporter( 645 | config.getvalue("jira_marker_strategy"), 646 | config.getvalue("jira_docs"), 647 | config.getvalue("jira_regex"), 648 | ) 649 | 650 | jira_plugin = JiraHooks( 651 | jira_connection, 652 | jira_marker, 653 | config.getvalue("jira_product_version"), 654 | components, 655 | resolved_statuses, 656 | resolved_resolutions, 657 | config.getvalue("jira_run_test_case"), 658 | config.getini("xfail_strict"), 659 | config.getvalue("jira_connection_error_strategy"), 660 | config.getvalue("return_jira_metadata"), 661 | ) 662 | ok = config.pluginmanager.register(jira_plugin, PLUGIN_NAME) 663 | assert ok 664 | 665 | 666 | @pytest.fixture 667 | def jira_issue(request): 668 | """ 669 | Returns a bool representing the state of the issue, or None if no 670 | connection could be made. See 671 | https://github.com/rhevm-qe-automation/pytest_jira#fixture-usage 672 | for more details 673 | """ 674 | 675 | def wrapper_jira_issue(issue_id): 676 | jira_plugin = request.config.pluginmanager.getplugin(PLUGIN_NAME) 677 | if jira_plugin: 678 | try: 679 | result = jira_plugin.is_issue_resolved(issue_id) 680 | if request.config.option.return_jira_metadata: 681 | return result 682 | return not result # return boolean representing of issue state 683 | except requests.RequestException as e: 684 | strategy = request.config.getoption(CONNECTION_ERROR_FLAG_NAME) 685 | if strategy == SKIP: 686 | pytest.skip(CONNECTION_SKIP_MESSAGE % e) 687 | elif strategy == STRICT: 688 | raise 689 | 690 | return wrapper_jira_issue 691 | -------------------------------------------------------------------------------- /tests/test_jira.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import pytest 5 | from packaging.version import Version 6 | 7 | PUBLIC_JIRA_SERVER = "https://issues.redhat.com" 8 | 9 | CONFTEST = """ 10 | import pytest 11 | 12 | 13 | FAKE_ISSUES = { 14 | "ORG-1412": {"status": "closed"}, 15 | "ORG-1382": {"status": "open"}, 16 | "ORG-1510": { 17 | "components": set(["com1", "com2"]), 18 | "versions": set(), 19 | "fixed_versions": set(), 20 | "status": "open", 21 | }, 22 | "ORG-1511": { 23 | "components": set(["com1", "com2"]), 24 | "versions": set(["foo-0.1", "foo-0.2"]), 25 | "fixVersions": set(), 26 | "status": "open", 27 | }, 28 | "ORG-1512": { 29 | "components": set(), 30 | "versions": set(), 31 | "fixed_versions": set(), 32 | "status": "custom-status", 33 | }, 34 | "ORG-1501": { 35 | "components": set(), 36 | "versions": set(["foo-0.1", "foo-0.2"]), 37 | "fixed_versions": set(["foo-0.2"]), 38 | "status": "closed", 39 | }, 40 | "ORG-1513": { 41 | "components": set(['component1', 'component2']), 42 | "versions": set(), 43 | "fixed_versions": set(), 44 | "status": "custom-status", 45 | }, 46 | "ORG-1514": { 47 | "components": set(['component2', 'component3']), 48 | "versions": set(["foo-0.1", "foo-0.2"]), 49 | "fixed_versions": set(["foo-0.2"]), 50 | "status": "closed", 51 | }, 52 | "ORG-1515": { 53 | "components": set(['component2', 'component3']), 54 | "versions": set(["foo-0.1", "foo-0.2"]), 55 | "fixed_versions": set(["foo-0.2"]), 56 | "status": "closed", 57 | "resolution": "won't fix" 58 | }, 59 | "ORG-1516": { 60 | "components": set(['component2', 'component3']), 61 | "versions": set(["foo-0.1", "foo-0.2"]), 62 | "fixed_versions": set(["foo-0.2"]), 63 | "status": "closed", 64 | "resolution": "done" 65 | }, 66 | } 67 | 68 | 69 | @pytest.mark.tryfirst 70 | def pytest_collection_modifyitems(session, config, items): 71 | plug = config.pluginmanager.getplugin("jira_plugin") 72 | assert plug is not None 73 | plug.issue_cache.update(FAKE_ISSUES) 74 | """ 75 | 76 | PLUGIN_ARGS = ( 77 | "--jira", 78 | "--jira-url", 79 | PUBLIC_JIRA_SERVER, 80 | ) 81 | 82 | TOKEN = os.environ.get("TEST_JIRA_TOKEN", "").strip() 83 | MISSING_TOKEN_REASON = "Missing TEST_JIRA_TOKEN variable" 84 | 85 | 86 | def assert_outcomes( 87 | result, passed, skipped, failed, error=0, xpassed=0, xfailed=0 88 | ): 89 | outcomes = result.parseoutcomes() 90 | assert outcomes.get("passed", 0) == passed 91 | assert outcomes.get("skipped", 0) == skipped 92 | assert outcomes.get("failed", 0) == failed 93 | assert outcomes.get("error", 0) == error 94 | assert outcomes.get("xpassed", 0) == xpassed 95 | assert outcomes.get("xfailed", 0) == xfailed 96 | 97 | 98 | def test_jira_plugin_disabled(testdir): 99 | testdir.makepyfile( 100 | """ 101 | import pytest 102 | @pytest.mark.jira("ORG-1382", run=True) 103 | def test_pass(): 104 | assert True 105 | """ 106 | ) 107 | result = testdir.runpytest() 108 | assert_outcomes(result, 1, 0, 0) 109 | 110 | 111 | def test_jira_marker_no_args(testdir): 112 | testdir.makeconftest(CONFTEST) 113 | testdir.makepyfile( 114 | """ 115 | import pytest 116 | @pytest.mark.jira 117 | def test_pass(): 118 | assert True 119 | """ 120 | ) 121 | result = testdir.runpytest(*PLUGIN_ARGS) 122 | text = "JIRA marker requires one, or more, arguments" 123 | assert text in result.stdout.str() 124 | 125 | 126 | def test_jira_marker_bad_args(testdir): 127 | testdir.makeconftest(CONFTEST) 128 | testdir.makepyfile( 129 | """ 130 | import pytest 131 | 132 | @pytest.mark.jira("there is no issue here") 133 | def test_pass(): 134 | assert True 135 | """ 136 | ) 137 | result = testdir.runpytest(*PLUGIN_ARGS) 138 | text = ( 139 | "JIRA marker argument `there is no issue here` " 140 | "does not match pattern" 141 | ) 142 | assert text in result.stdout.str() 143 | 144 | 145 | def test_jira_marker_bad_args2(testdir): 146 | testdir.makeconftest(CONFTEST) 147 | testdir.makepyfile( 148 | """ 149 | import pytest 150 | 151 | @pytest.mark.jira(None) 152 | def test_pass(): 153 | assert True 154 | """ 155 | ) 156 | result = testdir.runpytest(*PLUGIN_ARGS) 157 | assert "expected string or " in result.stdout.str() 158 | 159 | 160 | def test_jira_marker_no_run(testdir): 161 | """Expected skip due to run=False""" 162 | testdir.makeconftest(CONFTEST) 163 | testdir.makepyfile( 164 | """ 165 | import pytest 166 | 167 | @pytest.mark.jira("ORG-1382", run=False) 168 | def test_pass(): 169 | assert True 170 | """ 171 | ) 172 | result = testdir.runpytest(*PLUGIN_ARGS) 173 | result.assert_outcomes(0, 1, 0) 174 | 175 | 176 | def test_open_jira_marker_pass(testdir): 177 | """Expected skip due to unresolved JIRA""" 178 | testdir.makeconftest(CONFTEST) 179 | testdir.makepyfile( 180 | """ 181 | import pytest 182 | 183 | @pytest.mark.jira("ORG-1382", run=True) 184 | def test_pass(): 185 | assert True 186 | """ 187 | ) 188 | result = testdir.runpytest(*PLUGIN_ARGS) 189 | assert_outcomes(result, 0, 0, 0, 0, 1) 190 | 191 | 192 | def test_open_jira_marker_with_skipif_pass(testdir): 193 | """Expected skip due to unresolved JIRA when skipif is True""" 194 | testdir.makeconftest(CONFTEST) 195 | testdir.makepyfile( 196 | """ 197 | import pytest 198 | 199 | @pytest.mark.jira("ORG-1382", skipif=True) 200 | def test_pass(): 201 | assert False 202 | """ 203 | ) 204 | result = testdir.runpytest(*PLUGIN_ARGS) 205 | assert_outcomes(result, 0, 0, 0, xfailed=1) 206 | 207 | 208 | def test_open_jira_marker_without_skipif_fail(testdir): 209 | """Expected test to fail as unresolved JIRA marker 210 | is parametrized with False skipif""" 211 | testdir.makeconftest(CONFTEST) 212 | testdir.makepyfile( 213 | """ 214 | import pytest 215 | 216 | @pytest.mark.jira("ORG-1382", skipif=False) 217 | def test_fail(): 218 | assert False 219 | """ 220 | ) 221 | result = testdir.runpytest(*PLUGIN_ARGS) 222 | result.assert_outcomes(0, 0, 1) 223 | 224 | 225 | def test_open_jira_marker_with_callable_skipif_pass(testdir): 226 | """ 227 | Expected skip as skipif value is a lambda returning True. Expected 228 | component 'component2' is present on both closed and open JIRA issue 229 | """ 230 | testdir.makeconftest(CONFTEST) 231 | testdir.makepyfile( 232 | """ 233 | import pytest 234 | 235 | @pytest.mark.jira("ORG-1513", "ORG-1514", 236 | skipif=lambda i: 'component2' in i.get('components')) 237 | def test_pass(): 238 | assert False 239 | """ 240 | ) 241 | result = testdir.runpytest(*PLUGIN_ARGS) 242 | assert_outcomes(result, 0, 0, 0, xfailed=1) 243 | 244 | 245 | def test_open_jira_marker_with_callable_skipif_fail(testdir): 246 | """ 247 | Expected fail as skipif value for open issue is a lambda returning False. 248 | Expected component 'component3' is present only on closed JIRA issue 249 | """ 250 | testdir.makeconftest(CONFTEST) 251 | testdir.makepyfile( 252 | """ 253 | import pytest 254 | 255 | @pytest.mark.jira("ORG-1513", "ORG-1514", 256 | skipif=lambda i: 'component3' in i.get('components')) 257 | def test_fail(): 258 | assert False 259 | """ 260 | ) 261 | result = testdir.runpytest(*PLUGIN_ARGS) 262 | result.assert_outcomes(0, 0, 1) 263 | 264 | 265 | def test_multiple_jira_markers_with_skipif_pass(testdir): 266 | """Expected test to skip due to multiple JIRA lines with skipif set""" 267 | testdir.makeconftest(CONFTEST) 268 | testdir.makepyfile( 269 | """ 270 | import pytest 271 | 272 | @pytest.mark.jira("ORG-1382", skipif=True) 273 | @pytest.mark.jira("ORG-1412", skipif=True) 274 | def test_pass(): 275 | assert False 276 | """ 277 | ) 278 | result = testdir.runpytest(*PLUGIN_ARGS) 279 | assert_outcomes(result, 0, 0, 0, xfailed=1) 280 | 281 | 282 | def test_multiple_jira_markers_open_without_skipif_fail(testdir): 283 | """Expected to fail as skipif for open JIRA is False""" 284 | testdir.makeconftest(CONFTEST) 285 | testdir.makepyfile( 286 | """ 287 | import pytest 288 | 289 | @pytest.mark.jira("ORG-1382", skipif=False) 290 | @pytest.mark.jira("ORG-1412", skipif=True) 291 | def test_fail(): 292 | assert False 293 | """ 294 | ) 295 | result = testdir.runpytest(*PLUGIN_ARGS) 296 | result.assert_outcomes(0, 0, 1) 297 | 298 | 299 | def test_multiple_jira_markers_without_skipif_fail(testdir): 300 | """Expected to fail as skipif is False""" 301 | testdir.makeconftest(CONFTEST) 302 | testdir.makepyfile( 303 | """ 304 | import pytest 305 | 306 | @pytest.mark.jira("ORG-1382", "ORG-1412", skipif=False) 307 | def test_fail(): 308 | assert False 309 | """ 310 | ) 311 | result = testdir.runpytest(*PLUGIN_ARGS) 312 | result.assert_outcomes(0, 0, 1) 313 | 314 | 315 | def test_multiple_jira_markers_with_one_skipif_pass(testdir): 316 | """Expected to skip as skipif for JIRA tickets is True""" 317 | testdir.makeconftest(CONFTEST) 318 | testdir.makepyfile( 319 | """ 320 | import pytest 321 | 322 | @pytest.mark.jira("ORG-1382", "ORG-1412", skipif=True) 323 | def test_pass(): 324 | assert False 325 | """ 326 | ) 327 | result = testdir.runpytest(*PLUGIN_ARGS) 328 | assert_outcomes(result, 0, 0, 0, xfailed=1) 329 | 330 | 331 | def test_open_jira_docstr_pass(testdir): 332 | """Expected skip due to unresolved JIRA Issue %s""" 333 | testdir.makeconftest(CONFTEST) 334 | testdir.makepyfile( 335 | """ 336 | def test_pass(): 337 | \"\"\" 338 | ORG-1382 339 | \"\"\" 340 | assert True 341 | """ 342 | ) 343 | result = testdir.runpytest(*PLUGIN_ARGS) 344 | assert_outcomes(result, 0, 0, 0, 0, 1) 345 | 346 | 347 | # Should increase code coverage around return-metadata 348 | def test_return_metadata(testdir): 349 | """Expected skip due to unresolved JIRA Issue %s""" 350 | testdir.makeconftest(CONFTEST) 351 | testdir.makepyfile( 352 | """ 353 | def test_xpassed(): 354 | \"\"\" 355 | ORG-1382 356 | \"\"\" 357 | assert True 358 | """ 359 | ) 360 | ARGS = PLUGIN_ARGS + ("--jira-return-metadata",) 361 | result = testdir.runpytest(*ARGS) 362 | assert_outcomes(result, 0, 0, 0, xpassed=1) 363 | 364 | 365 | def test_open_jira_marker_fail(testdir): 366 | """Expected skip due to unresolved JIRA""" 367 | testdir.makeconftest(CONFTEST) 368 | testdir.makepyfile( 369 | """ 370 | import pytest 371 | 372 | @pytest.mark.jira("ORG-1382", run=True) 373 | def test_fail(): 374 | assert False 375 | """ 376 | ) 377 | result = testdir.runpytest(*PLUGIN_ARGS) 378 | assert_outcomes(result, 0, 0, 0, xfailed=1) 379 | 380 | 381 | def test_open_jira_docstr_fail(testdir): 382 | """Expected skip due to unresolved JIRA Issue %s""" 383 | testdir.makeconftest(CONFTEST) 384 | testdir.makepyfile( 385 | """ 386 | def test_fail(): 387 | \"\"\" 388 | ORG-1382 389 | \"\"\" 390 | assert False 391 | """ 392 | ) 393 | result = testdir.runpytest(*PLUGIN_ARGS) 394 | assert_outcomes(result, 0, 0, 0, xfailed=1) 395 | 396 | 397 | def test_closed_jira_marker_pass(testdir): 398 | """Expected PASS due to resolved JIRA Issue""" 399 | testdir.makeconftest(CONFTEST) 400 | testdir.makepyfile( 401 | """ 402 | import pytest 403 | 404 | @pytest.mark.jira("ORG-1412", run=True) 405 | def test_pass(): 406 | assert True 407 | """ 408 | ) 409 | result = testdir.runpytest(*PLUGIN_ARGS) 410 | result.assert_outcomes(1, 0, 0) 411 | 412 | 413 | def test_closed_jira_docstr_pass(testdir): 414 | """Expected PASS due to resolved JIRA Issue %s""" 415 | testdir.makeconftest(CONFTEST) 416 | testdir.makepyfile( 417 | """ 418 | def test_fail(): 419 | \"\"\" 420 | ORG-1412 421 | \"\"\" 422 | assert True 423 | """ 424 | ) 425 | result = testdir.runpytest(*PLUGIN_ARGS) 426 | result.assert_outcomes(1, 0, 0) 427 | 428 | 429 | def test_closed_jira_marker_fail(testdir): 430 | testdir.makeconftest(CONFTEST) 431 | testdir.makepyfile( 432 | """ 433 | import pytest 434 | 435 | @pytest.mark.jira("ORG-1412", run=True) 436 | def test_fail(): 437 | assert False 438 | """ 439 | ) 440 | result = testdir.runpytest(*PLUGIN_ARGS) 441 | result.assert_outcomes(0, 0, 1) 442 | 443 | 444 | def test_closed_jira_docstr_fail(testdir): 445 | """Expected xfail due to resolved JIRA Issue %s""" 446 | testdir.makeconftest(CONFTEST) 447 | testdir.makepyfile( 448 | """ 449 | def test_fail(): 450 | \"\"\" 451 | ORG-1412 452 | \"\"\" 453 | assert False 454 | """ 455 | ) 456 | result = testdir.runpytest(*PLUGIN_ARGS) 457 | result.assert_outcomes(0, 0, 1) 458 | 459 | 460 | def test_pass_without_jira(testdir): 461 | testdir.makeconftest(CONFTEST) 462 | testdir.makepyfile( 463 | """ 464 | def test_pass(): 465 | \"\"\" 466 | some description 467 | \"\"\" 468 | assert True 469 | """ 470 | ) 471 | result = testdir.runpytest(*PLUGIN_ARGS) 472 | result.assert_outcomes(1, 0, 0) 473 | 474 | 475 | def test_fail_without_jira_marker(testdir): 476 | testdir.makeconftest(CONFTEST) 477 | testdir.makepyfile( 478 | """ 479 | def test_fail(): 480 | assert False 481 | """ 482 | ) 483 | result = testdir.runpytest(*PLUGIN_ARGS) 484 | result.assert_outcomes(0, 0, 1) 485 | 486 | 487 | def test_fail_without_jira_docstr(testdir): 488 | """docstring with no jira issue""" 489 | testdir.makeconftest(CONFTEST) 490 | testdir.makepyfile( 491 | """ 492 | def test_pass(): 493 | \"\"\" 494 | some description 495 | \"\"\" 496 | assert False 497 | """ 498 | ) 499 | result = testdir.runpytest(*PLUGIN_ARGS) 500 | result.assert_outcomes(0, 0, 1) 501 | 502 | 503 | def test_invalid_configuration_exception(testdir): 504 | """Invalid option in config file, exception should be rised""" 505 | testdir.makefile( 506 | ".cfg", 507 | jira="\n".join( 508 | [ 509 | "[DEFAULT]", 510 | "ssl_verification = something", 511 | ] 512 | ), 513 | ) 514 | testdir.makepyfile( 515 | """ 516 | import pytest 517 | 518 | def test_pass(): 519 | pass 520 | """ 521 | ) 522 | result = testdir.runpytest(*PLUGIN_ARGS) 523 | assert "ValueError: Not a boolean: something" in result.stderr.str() 524 | 525 | 526 | def test_invalid_authentication_exception(testdir): 527 | """Failed authentication, exception should be raised""" 528 | testdir.makepyfile( 529 | """ 530 | import pytest 531 | 532 | @pytest.mark.jira('FOO-1234') 533 | def test_pass(): 534 | pass 535 | """ 536 | ) 537 | ARGS = ( 538 | "--jira", 539 | "--jira-url", 540 | PUBLIC_JIRA_SERVER, 541 | "--jira-user", 542 | "user123", 543 | "--jira-password", 544 | "passwd123", 545 | ) 546 | result = testdir.runpytest(*ARGS) 547 | assert re.search("4(01|29) Client Error", result.stdout.str(), re.MULTILINE) 548 | 549 | 550 | def test_disabled_ssl_verification_pass(testdir): 551 | """Expected PASS due to resolved JIRA Issue""" 552 | testdir.makeconftest(CONFTEST) 553 | testdir.makefile( 554 | ".cfg", 555 | jira="\n".join( 556 | [ 557 | "[DEFAULT]", 558 | "url = " + PUBLIC_JIRA_SERVER, 559 | "ssl_verification = false", 560 | ] 561 | ), 562 | ) 563 | testdir.makepyfile( 564 | """ 565 | import pytest 566 | 567 | @pytest.mark.jira("ORG-1412", run=True) 568 | def test_pass(): 569 | assert True 570 | """ 571 | ) 572 | result = testdir.runpytest("--jira") 573 | result.assert_outcomes(1, 0, 0) 574 | 575 | 576 | def test_config_file_paths_xfail(testdir): 577 | """Jira url set in ~/jira.cfg""" 578 | testdir.makeconftest(CONFTEST) 579 | homedir = testdir.mkdir("home") 580 | os.environ["HOME"] = os.getcwd() + "/home" 581 | homedir.ensure("jira.cfg").write( 582 | "[DEFAULT]\nurl = " + PUBLIC_JIRA_SERVER, 583 | ) 584 | assert os.path.isfile(os.getcwd() + "/home/jira.cfg") 585 | testdir.makepyfile( 586 | """ 587 | import pytest 588 | 589 | @pytest.mark.jira("ORG-1382", run=True) 590 | def test_fail(): 591 | assert False 592 | """ 593 | ) 594 | result = testdir.runpytest("--jira") 595 | assert_outcomes(result, 0, 0, 0, xfailed=1) 596 | 597 | 598 | def test_closed_for_different_version_skipped(testdir): 599 | """Skiped, closed for different version""" 600 | testdir.makeconftest(CONFTEST) 601 | testdir.makefile( 602 | ".cfg", 603 | jira="\n".join( 604 | [ 605 | "[DEFAULT]", 606 | "components = com1,com3", 607 | "version = foo-0.1", 608 | ] 609 | ), 610 | ) 611 | testdir.makepyfile( 612 | """ 613 | import pytest 614 | 615 | @pytest.mark.jira("ORG-1501", run=False) 616 | def test_fail(): 617 | assert False 618 | """ 619 | ) 620 | result = testdir.runpytest(*PLUGIN_ARGS) 621 | assert_outcomes(result, 0, 1, 0) 622 | 623 | 624 | def test_open_for_different_version_failed(testdir): 625 | """Failed, open for different version""" 626 | testdir.makeconftest(CONFTEST) 627 | testdir.makefile( 628 | ".cfg", 629 | jira="\n".join( 630 | [ 631 | "[DEFAULT]", 632 | "components = com1,com3", 633 | "version = foo-1.1", 634 | ] 635 | ), 636 | ) 637 | testdir.makepyfile( 638 | """ 639 | import pytest 640 | 641 | @pytest.mark.jira("ORG-1511", run=False) 642 | def test_fail(): 643 | assert False 644 | """ 645 | ) 646 | result = testdir.runpytest(*PLUGIN_ARGS) 647 | assert_outcomes(result, 0, 0, 1) 648 | 649 | 650 | @pytest.mark.skipif(not TOKEN, reason=MISSING_TOKEN_REASON) 651 | def test_get_issue_info_from_remote_passed(testdir): 652 | testdir.makeconftest(CONFTEST) 653 | testdir.makepyfile( 654 | """ 655 | def test_pass(): 656 | \"\"\" 657 | XNIO-250 658 | \"\"\" 659 | assert True 660 | """ 661 | ) 662 | ARGS = PLUGIN_ARGS + ("--jira-token", TOKEN) 663 | result = testdir.runpytest(*ARGS) 664 | result.assert_outcomes(1, 0, 0) 665 | 666 | 667 | def test_affected_component_skiped(testdir): 668 | """Skiped, affected component""" 669 | testdir.makeconftest(CONFTEST) 670 | testdir.makepyfile( 671 | """ 672 | import pytest 673 | 674 | @pytest.mark.jira("ORG-1511", run=False) 675 | def test_pass(): 676 | assert True 677 | """ 678 | ) 679 | result = testdir.runpytest( 680 | "--jira", 681 | "--jira-url", 682 | PUBLIC_JIRA_SERVER, 683 | "--jira-components", 684 | "com3", 685 | "com1", 686 | ) 687 | assert_outcomes(result, 0, 1, 0) 688 | 689 | 690 | @pytest.mark.skipif(not TOKEN, reason=MISSING_TOKEN_REASON) 691 | def test_strategy_ignore_failed(testdir): 692 | """Invalid issue ID is ignored and test fails""" 693 | testdir.makeconftest(CONFTEST) 694 | testdir.makefile( 695 | ".cfg", 696 | jira="\n".join( 697 | [ 698 | "[DEFAULT]", 699 | "url = " + PUBLIC_JIRA_SERVER, 700 | "marker_strategy = ignore", 701 | "docs_search = False", 702 | ] 703 | ), 704 | ) 705 | testdir.makepyfile( 706 | """ 707 | import pytest 708 | 709 | @pytest.mark.jira("ORG-1412789456148865", run=True) 710 | def test_fail(): 711 | assert False 712 | """ 713 | ) 714 | result = testdir.runpytest("--jira", "--jira-token", TOKEN) 715 | result.assert_outcomes(0, 0, 1) 716 | 717 | 718 | @pytest.mark.skipif(not TOKEN, reason=MISSING_TOKEN_REASON) 719 | def test_strategy_strict_exception(testdir): 720 | """Invalid issue ID, exception is rised""" 721 | testdir.makeconftest(CONFTEST) 722 | testdir.makepyfile( 723 | """ 724 | import pytest 725 | 726 | def test_fail(): 727 | \"\"\" 728 | issue: 89745-1412789456148865 729 | \"\"\" 730 | assert False 731 | """ 732 | ) 733 | result = testdir.runpytest( 734 | "--jira", 735 | "--jira-url", 736 | PUBLIC_JIRA_SERVER, 737 | "--jira-token", 738 | TOKEN, 739 | "--jira-marker-strategy", 740 | "strict", 741 | "--jira-issue-regex", 742 | "[0-9]+-[0-9]+", 743 | ) 744 | assert "89745-1412789456148865" in result.stdout.str() 745 | 746 | 747 | @pytest.mark.skipif(not TOKEN, reason=MISSING_TOKEN_REASON) 748 | def test_strategy_warn_fail(testdir): 749 | """Invalid issue ID is ignored and warning is written""" 750 | testdir.makeconftest(CONFTEST) 751 | testdir.makefile( 752 | ".cfg", 753 | jira="\n".join( 754 | [ 755 | "[DEFAULT]", 756 | "url = " + PUBLIC_JIRA_SERVER, 757 | "marker_strategy = warn", 758 | ] 759 | ), 760 | ) 761 | testdir.makepyfile( 762 | """ 763 | import pytest 764 | 765 | @pytest.mark.jira("ORG-1511786754387", run=True) 766 | def test_fail(): 767 | assert False 768 | """ 769 | ) 770 | result = testdir.runpytest("--jira", "--jira-token", TOKEN) 771 | assert "ORG-1511786754387" in result.stderr.str() 772 | result.assert_outcomes(0, 0, 1) 773 | 774 | 775 | def test_ignored_docs_marker_fail(testdir): 776 | """Issue is open but docs is ignored""" 777 | testdir.makeconftest(CONFTEST) 778 | testdir.makepyfile( 779 | """ 780 | import pytest 781 | 782 | def test_fail(): 783 | \"\"\" 784 | open issue: ORG-1382 785 | ignored 786 | \"\"\" 787 | assert False 788 | """ 789 | ) 790 | result = testdir.runpytest( 791 | "--jira", 792 | "--jira-url", 793 | PUBLIC_JIRA_SERVER, 794 | "--jira-disable-docs-search", 795 | ) 796 | assert_outcomes(result, 0, 0, 1) 797 | 798 | 799 | @pytest.mark.skipif(not TOKEN, reason=MISSING_TOKEN_REASON) 800 | def test_issue_not_found_considered_open_xfailed(testdir): 801 | """Issue is open but docs is ignored""" 802 | testdir.makeconftest(CONFTEST) 803 | testdir.makepyfile( 804 | """ 805 | import pytest 806 | 807 | def test_fail(): 808 | \"\"\" 809 | not existing issue: ORG-13827864532876523 810 | considered open by default 811 | \"\"\" 812 | assert False 813 | """ 814 | ) 815 | ARGS = PLUGIN_ARGS + ("--jira-token", TOKEN) 816 | result = testdir.runpytest(*ARGS) 817 | assert_outcomes(result, 0, 0, 0, xfailed=1) 818 | 819 | 820 | def test_jira_marker_bad_args_due_to_changed_regex(testdir): 821 | """Issue ID in marker doesn't match due to changed regex""" 822 | testdir.makeconftest(CONFTEST) 823 | testdir.makepyfile( 824 | """ 825 | import pytest 826 | 827 | @pytest.mark.jira("ORG-1382", run=False) 828 | def test_fail(): 829 | assert False 830 | """ 831 | ) 832 | result = testdir.runpytest( 833 | "--jira", 834 | "--jira-url", 835 | PUBLIC_JIRA_SERVER, 836 | "--jira-issue-regex", 837 | "[0-9]+-[0-9]+", 838 | ) 839 | text = "JIRA marker argument `ORG-1382` does not match pattern" 840 | assert text in result.stdout.str() 841 | 842 | 843 | def test_invalid_jira_marker_strategy_parameter(testdir): 844 | """Invalid parameter for --jira-marker-strategy""" 845 | testdir.makeconftest(CONFTEST) 846 | testdir.makepyfile( 847 | """ 848 | import pytest 849 | 850 | @pytest.mark.jira("ORG-1382", run=False) 851 | def test_fail(): 852 | assert False 853 | """ 854 | ) 855 | result = testdir.runpytest( 856 | "--jira", 857 | "--jira-url", 858 | PUBLIC_JIRA_SERVER, 859 | "--jira-marker-strategy", 860 | "invalid", 861 | ) 862 | assert "invalid choice: 'invalid'" in result.stderr.str() 863 | 864 | 865 | def test_custom_resolve_status_fail(testdir): 866 | """ 867 | Test case matches custom status and do not skip it because it is considered 868 | as closed, in additional test fails because of some regression. 869 | """ 870 | testdir.makeconftest(CONFTEST) 871 | testdir.makepyfile( 872 | """ 873 | import pytest 874 | 875 | @pytest.mark.jira("ORG-1512", run=True) 876 | def test_fail(): 877 | assert False # some regression 878 | """ 879 | ) 880 | result = testdir.runpytest( 881 | "--jira", 882 | "--jira-url", 883 | PUBLIC_JIRA_SERVER, 884 | "--jira-resolved-statuses", 885 | "custom-status", 886 | ) 887 | assert_outcomes(result, 0, 0, 1) 888 | 889 | 890 | def test_custom_resolve_status_pass(testdir): 891 | """ 892 | Test case matches custom status and do not skip it because it is considered 893 | as closed, in additional test passes. 894 | """ 895 | testdir.makeconftest(CONFTEST) 896 | testdir.makepyfile( 897 | """ 898 | import pytest 899 | 900 | @pytest.mark.jira("ORG-1512", run=True) 901 | def test_pass(): 902 | assert True 903 | """ 904 | ) 905 | result = testdir.runpytest( 906 | "--jira", 907 | "--jira-url", 908 | PUBLIC_JIRA_SERVER, 909 | "--jira-resolved-statuses", 910 | "custom-status", 911 | ) 912 | assert_outcomes(result, 1, 0, 0) 913 | 914 | 915 | def test_custom_resolve_status_skipped_on_closed_status(testdir): 916 | """ 917 | Test case is marked by issue with status 'closed' which is one of defaults 918 | resolved statuses. But test-case gets skipped because custom resolved 919 | statuses are set. 920 | """ 921 | testdir.makeconftest(CONFTEST) 922 | testdir.makepyfile( 923 | """ 924 | import pytest 925 | 926 | @pytest.mark.jira("ORG-1501", run=False) 927 | def test_fail(): 928 | assert False 929 | """ 930 | ) 931 | result = testdir.runpytest( 932 | "--jira", 933 | "--jira-url", 934 | PUBLIC_JIRA_SERVER, 935 | "--jira-resolved-statuses", 936 | "custom-status,some-other", 937 | ) 938 | assert_outcomes(result, 0, 1, 0) 939 | 940 | 941 | def test_run_test_case_false1(testdir): 942 | """Test case shouldn't get executed""" 943 | testdir.makeconftest(CONFTEST) 944 | testdir.makefile( 945 | ".cfg", 946 | jira="\n".join( 947 | [ 948 | "[DEFAULT]", 949 | "run_test_case = False", 950 | ] 951 | ), 952 | ) 953 | testdir.makepyfile( 954 | """ 955 | import pytest 956 | 957 | @pytest.mark.jira("ORG-1382") 958 | def test_fail(): 959 | assert False 960 | """ 961 | ) 962 | result = testdir.runpytest(*PLUGIN_ARGS) 963 | assert_outcomes(result, passed=0, skipped=1, failed=0, error=0) 964 | 965 | 966 | def test_run_test_case_false2(testdir): 967 | """Test case shouldn't get executed""" 968 | testdir.makeconftest(CONFTEST) 969 | plugin_args = ( 970 | "--jira", 971 | "--jira-url", 972 | PUBLIC_JIRA_SERVER, 973 | "--jira-do-not-run-test-case", 974 | ) 975 | testdir.makepyfile( 976 | """ 977 | import pytest 978 | 979 | @pytest.mark.jira("ORG-1382") 980 | def test_fail(): 981 | assert False 982 | """ 983 | ) 984 | result = testdir.runpytest(*plugin_args) 985 | assert_outcomes(result, passed=0, skipped=1, failed=0, error=0) 986 | 987 | 988 | def test_run_test_case_true1(testdir): 989 | """Test case should get executed""" 990 | testdir.makeconftest(CONFTEST) 991 | testdir.makefile( 992 | ".cfg", 993 | jira="\n".join( 994 | [ 995 | "[DEFAULT]", 996 | "run_test_case = True", 997 | ] 998 | ), 999 | ) 1000 | testdir.makepyfile( 1001 | """ 1002 | import pytest 1003 | 1004 | @pytest.mark.jira("ORG-1382") 1005 | def test_fail(): 1006 | assert False 1007 | """ 1008 | ) 1009 | result = testdir.runpytest(*PLUGIN_ARGS) 1010 | assert_outcomes(result, passed=0, skipped=0, failed=0, error=0, xfailed=1) 1011 | 1012 | 1013 | def test_jira_fixture_plugin_disabled(testdir): 1014 | testdir.makepyfile( 1015 | """ 1016 | import pytest 1017 | 1018 | def test_pass(jira_issue): 1019 | assert jira_issue("ORG-1382") is None 1020 | """ 1021 | ) 1022 | result = testdir.runpytest() 1023 | assert_outcomes(result, 1, 0, 0) 1024 | 1025 | 1026 | def test_jira_fixture_run_positive(testdir): 1027 | testdir.makeconftest(CONFTEST) 1028 | testdir.makepyfile( 1029 | """ 1030 | import pytest 1031 | 1032 | def test_pass(jira_issue): 1033 | assert jira_issue("ORG-1382") 1034 | """ 1035 | ) 1036 | result = testdir.runpytest(*PLUGIN_ARGS) 1037 | result.assert_outcomes(1, 0, 0) 1038 | 1039 | 1040 | def test_jira_fixture_run_negative(testdir): 1041 | testdir.makeconftest(CONFTEST) 1042 | testdir.makepyfile( 1043 | """ 1044 | import pytest 1045 | 1046 | def test_pass(jira_issue): 1047 | assert not jira_issue("ORG-1382") 1048 | """ 1049 | ) 1050 | result = testdir.runpytest(*PLUGIN_ARGS) 1051 | result.assert_outcomes(0, 0, 1) 1052 | 1053 | 1054 | def test_run_false_for_resolved_issue(testdir): 1055 | testdir.makeconftest(CONFTEST) 1056 | testdir.makepyfile( 1057 | """ 1058 | import pytest 1059 | 1060 | @pytest.mark.jira("ORG-1412", run=False) 1061 | def test_pass(): 1062 | assert True 1063 | """ 1064 | ) 1065 | result = testdir.runpytest(*PLUGIN_ARGS) 1066 | result.assert_outcomes(1, 0, 0) 1067 | 1068 | 1069 | def test_xfail_strict(testdir): 1070 | testdir.makeconftest(CONFTEST) 1071 | testdir.makefile( 1072 | ".ini", 1073 | pytest="\n".join( 1074 | [ 1075 | "[pytest]", 1076 | "xfail_strict = True", 1077 | ] 1078 | ), 1079 | ) 1080 | testdir.makepyfile( 1081 | """ 1082 | import pytest 1083 | 1084 | @pytest.mark.jira("ORG-1382") 1085 | def test_pass(): 1086 | assert True 1087 | """ 1088 | ) 1089 | result = testdir.runpytest(*PLUGIN_ARGS) 1090 | assert_outcomes(result, passed=0, skipped=0, failed=1, error=0, xfailed=0) 1091 | 1092 | 1093 | @pytest.mark.skipif( 1094 | Version(pytest.__version__) < Version("3.0.0"), 1095 | reason="requires pytest-3 or higher", 1096 | ) 1097 | def test_jira_marker_with_parametrize_pytest3(testdir): 1098 | """""" 1099 | testdir.makeconftest(CONFTEST) 1100 | testdir.makepyfile( 1101 | """ 1102 | import pytest 1103 | 1104 | @pytest.mark.parametrize('arg', [ 1105 | pytest.param(1, marks=pytest.mark.jira("ORG-1382", run=True)), 1106 | 2, 1107 | ]) 1108 | def test_fail(arg): 1109 | assert False 1110 | """ 1111 | ) 1112 | result = testdir.runpytest(*PLUGIN_ARGS) 1113 | assert_outcomes(result, 0, 0, failed=1, xfailed=1) 1114 | 1115 | 1116 | @pytest.mark.skipif( 1117 | Version(pytest.__version__) >= Version("3.0.0"), 1118 | reason="requires pytest-2 or lower", 1119 | ) 1120 | def test_jira_marker_with_parametrize_pytest2(testdir): 1121 | """""" 1122 | testdir.makeconftest(CONFTEST) 1123 | testdir.makepyfile( 1124 | """ 1125 | import pytest 1126 | 1127 | @pytest.mark.parametrize('arg', [ 1128 | pytest.mark.jira("ORG-1382", run=True)(1), 1129 | 2, 1130 | ]) 1131 | def test_fail(arg): 1132 | assert False 1133 | """ 1134 | ) 1135 | result = testdir.runpytest(*PLUGIN_ARGS) 1136 | assert_outcomes(result, 0, 0, failed=1, xfailed=1) 1137 | 1138 | 1139 | @pytest.mark.parametrize( 1140 | "error_strategy, passed, skipped, failed, error", 1141 | [ 1142 | ("strict", 0, 0, 0, 0), 1143 | ("skip", 0, 1, 0, 0), 1144 | ("ignore", 1, 0, 0, 0), 1145 | ], 1146 | ) 1147 | def test_marker_error_strategy( 1148 | testdir, error_strategy, passed, skipped, failed, error 1149 | ): 1150 | """HTTP Error when trying to connect""" 1151 | testdir.makeconftest(CONFTEST) 1152 | testdir.makepyfile( 1153 | """ 1154 | import pytest 1155 | 1156 | @pytest.mark.jira("FOO-1234") 1157 | def test_pass(): 1158 | pass 1159 | """ 1160 | ) 1161 | ARGS = ( 1162 | "--jira", 1163 | "--jira-url", 1164 | "http://foo.bar.com", 1165 | "--jira-user", 1166 | "user123", 1167 | "--jira-password", 1168 | "passwd123", 1169 | "--jira-connection-error-strategy", 1170 | error_strategy, 1171 | "--jira-connection-retry-total", 1172 | 1, 1173 | "--jira-connection-retry-backoff-factor", 1174 | 0.2, 1175 | ) 1176 | result = testdir.runpytest(*ARGS) 1177 | assert_outcomes( 1178 | result, passed=passed, skipped=skipped, failed=failed, error=error 1179 | ) 1180 | 1181 | 1182 | @pytest.mark.skip("Annonymous access it broken") 1183 | @pytest.mark.parametrize( 1184 | "passed, skipped, failed, error", 1185 | [ 1186 | (1, 0, 1, 0), 1187 | ], 1188 | ) 1189 | def test_marker_anonymous_access(testdir, passed, skipped, failed, error): 1190 | """Anonymous access to closed, public issue""" 1191 | testdir.makeconftest(CONFTEST) 1192 | testdir.makepyfile( 1193 | """ 1194 | import pytest 1195 | 1196 | @pytest.mark.jira("CNV-21615") 1197 | def test_pass(): 1198 | assert True 1199 | 1200 | @pytest.mark.jira("CNV-21615") 1201 | def test_fail(): 1202 | assert False 1203 | """ 1204 | ) 1205 | ARGS = ("--jira", "--jira-url", PUBLIC_JIRA_SERVER) 1206 | result = testdir.runpytest(*ARGS) 1207 | assert_outcomes( 1208 | result, passed=passed, skipped=skipped, failed=failed, error=error 1209 | ) 1210 | 1211 | 1212 | @pytest.mark.parametrize( 1213 | "error_strategy, passed, skipped, failed, error", 1214 | [ 1215 | ("strict", 0, 0, 1, 0), 1216 | ], 1217 | ) 1218 | def test_jira_fixture_request_exception( 1219 | testdir, error_strategy, passed, skipped, failed, error 1220 | ): 1221 | testdir.makeconftest(CONFTEST) 1222 | testdir.makepyfile( 1223 | """ 1224 | import pytest 1225 | 1226 | def test_pass(jira_issue): 1227 | assert jira_issue("FOO-1234") 1228 | """ 1229 | ) 1230 | ARGS = ( 1231 | "--jira", 1232 | "--jira-url", 1233 | "http://foo.bar.com", 1234 | "--jira-user", 1235 | "user123", 1236 | "--jira-password", 1237 | "passwd123", 1238 | "--jira-connection-error-strategy", 1239 | error_strategy, 1240 | "--jira-connection-retry-total", 1241 | 2, 1242 | "--jira-connection-retry-backoff-factor", 1243 | 0.1, 1244 | ) 1245 | result = testdir.runpytest(*ARGS) 1246 | assert_outcomes( 1247 | result, passed=passed, skipped=skipped, failed=failed, error=error 1248 | ) 1249 | 1250 | 1251 | @pytest.mark.skipif(not TOKEN, reason=MISSING_TOKEN_REASON) 1252 | @pytest.mark.parametrize("ticket", ["ORG-1382", "Foo-Bar"]) 1253 | @pytest.mark.parametrize( 1254 | "return_method, _type", 1255 | [ 1256 | ("--jira-return-metadata", "JiraIssue"), 1257 | ("", "bool"), 1258 | ], 1259 | ) 1260 | def test_jira_fixture_return_metadata(testdir, return_method, _type, ticket): 1261 | testdir.makepyfile( 1262 | """ 1263 | import pytest 1264 | from issue_model import JiraIssue 1265 | 1266 | def test_pass(jira_issue): 1267 | issue = jira_issue('%s') 1268 | assert isinstance(issue, %s) 1269 | """ 1270 | % (ticket, _type) 1271 | ) 1272 | ARGS = ( 1273 | "--jira", 1274 | "--jira-url", 1275 | PUBLIC_JIRA_SERVER, 1276 | "--jira-token", 1277 | TOKEN, 1278 | return_method, 1279 | ) 1280 | result = testdir.runpytest(*ARGS) 1281 | result.assert_outcomes(1, 0, 0) 1282 | 1283 | 1284 | def test_closed_nofix_nooption(testdir): 1285 | testdir.makeconftest(CONFTEST) 1286 | testdir.makepyfile( 1287 | """ 1288 | import pytest 1289 | 1290 | @pytest.mark.jira("ORG-1515", run=False) 1291 | def test_pass(): 1292 | assert False 1293 | """ 1294 | ) 1295 | result = testdir.runpytest(*PLUGIN_ARGS) 1296 | result.assert_outcomes(0, 0, 1) 1297 | 1298 | 1299 | def test_closed_nofix_option(testdir): 1300 | testdir.makeconftest(CONFTEST) 1301 | testdir.makepyfile( 1302 | """ 1303 | import pytest 1304 | 1305 | @pytest.mark.jira("ORG-1515", run=False) 1306 | def test_pass(): 1307 | assert False 1308 | """ 1309 | ) 1310 | ARGS = ( 1311 | "--jira", 1312 | "--jira-url", 1313 | PUBLIC_JIRA_SERVER, 1314 | "--jira-resolved-resolutions", 1315 | "done,fixed,completed", 1316 | ) 1317 | result = testdir.runpytest(*ARGS) 1318 | result.assert_outcomes(0, 1, 0) 1319 | 1320 | 1321 | def test_closed_fixed_nooption(testdir): 1322 | testdir.makeconftest(CONFTEST) 1323 | testdir.makepyfile( 1324 | """ 1325 | import pytest 1326 | 1327 | @pytest.mark.jira("ORG-1516", run=False) 1328 | def test_pass(): 1329 | assert True 1330 | """ 1331 | ) 1332 | result = testdir.runpytest(*PLUGIN_ARGS) 1333 | result.assert_outcomes(1, 0, 0) 1334 | 1335 | 1336 | def test_closed_fixed_option(testdir): 1337 | testdir.makeconftest(CONFTEST) 1338 | testdir.makepyfile( 1339 | """ 1340 | import pytest 1341 | 1342 | @pytest.mark.jira("ORG-1516", run=False) 1343 | def test_pass(): 1344 | assert True 1345 | """ 1346 | ) 1347 | ARGS = ( 1348 | "--jira", 1349 | "--jira-url", 1350 | PUBLIC_JIRA_SERVER, 1351 | "--jira-resolved-resolutions", 1352 | "done,fixed,completed", 1353 | ) 1354 | result = testdir.runpytest(*ARGS) 1355 | result.assert_outcomes(1, 0, 0) 1356 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from pytest_jira import _get_value 4 | 5 | 6 | def init_config_parser(): 7 | c = six.moves.configparser.ConfigParser() 8 | c.set("DEFAULT", "key", "value") 9 | return c 10 | 11 | 12 | def test_get_value1(): 13 | c = init_config_parser() 14 | assert _get_value(c, "DEFAULT", "key") == "value" 15 | 16 | 17 | def test_get_value2(): 18 | c = init_config_parser() 19 | assert _get_value(c, "DEFAULT", "nokey") is None 20 | 21 | 22 | def test_get_value3(): 23 | c = init_config_parser() 24 | assert _get_value(c, "DEFAULT", "nokey", "one") == "one" 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3.9, 3.1{0,1,2,3}, lint, pre-commit 3 | 4 | [testenv] 5 | setenv = 6 | PYTEST_ADDOPTS=-rxXs -p pytester --basetemp={envtmpdir} 7 | passenv = 8 | TEST_JIRA_TOKEN 9 | deps = uv 10 | commands = 11 | uv python pin python{envname} 12 | uv sync --locked --all-extras --dev --group tests 13 | uv run coverage run --source=pytest_jira,issue_model -m pytest 14 | uv run coverage xml 15 | uv run coverage html 16 | 17 | [testenv:lint] 18 | deps=uvx 19 | commands = uvx flake8 pytest_jira.py issue_model.py tests 20 | 21 | [testenv:pre-commit] 22 | deps=uvx 23 | commands = uvx pre-commit run --all-files 24 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 2 3 | requires-python = ">=3.9" 4 | 5 | [[package]] 6 | name = "certifi" 7 | version = "2025.4.26" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, 12 | ] 13 | 14 | [[package]] 15 | name = "cfgv" 16 | version = "3.4.0" 17 | source = { registry = "https://pypi.org/simple" } 18 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } 19 | wheels = [ 20 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, 21 | ] 22 | 23 | [[package]] 24 | name = "charset-normalizer" 25 | version = "3.4.2" 26 | source = { registry = "https://pypi.org/simple" } 27 | sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } 28 | wheels = [ 29 | { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, 30 | { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, 31 | { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, 32 | { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, 33 | { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, 34 | { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, 35 | { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, 36 | { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, 37 | { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, 38 | { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, 39 | { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, 40 | { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, 41 | { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, 42 | { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, 43 | { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, 44 | { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, 45 | { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, 46 | { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, 47 | { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, 48 | { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, 49 | { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, 50 | { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, 51 | { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, 52 | { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, 53 | { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, 54 | { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, 55 | { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, 56 | { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, 57 | { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, 58 | { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, 59 | { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, 60 | { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, 61 | { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, 62 | { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, 63 | { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, 64 | { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, 65 | { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, 66 | { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, 67 | { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, 68 | { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, 69 | { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, 70 | { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, 71 | { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, 72 | { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, 73 | { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, 74 | { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, 75 | { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, 76 | { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, 77 | { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, 78 | { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, 79 | { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, 80 | { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, 81 | { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, 82 | { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, 83 | { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, 84 | { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, 85 | { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, 86 | { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, 87 | { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, 88 | { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, 89 | { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, 90 | { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, 91 | { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, 92 | { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, 93 | { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, 94 | { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, 95 | ] 96 | 97 | [[package]] 98 | name = "colorama" 99 | version = "0.4.6" 100 | source = { registry = "https://pypi.org/simple" } 101 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } 102 | wheels = [ 103 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, 104 | ] 105 | 106 | [[package]] 107 | name = "coverage" 108 | version = "7.6.1" 109 | source = { registry = "https://pypi.org/simple" } 110 | sdist = { url = "https://files.pythonhosted.org/packages/f7/08/7e37f82e4d1aead42a7443ff06a1e406aabf7302c4f00a546e4b320b994c/coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", size = 798791, upload-time = "2024-08-04T19:45:30.9Z" } 111 | wheels = [ 112 | { url = "https://files.pythonhosted.org/packages/7e/61/eb7ce5ed62bacf21beca4937a90fe32545c91a3c8a42a30c6616d48fc70d/coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", size = 206690, upload-time = "2024-08-04T19:43:07.695Z" }, 113 | { url = "https://files.pythonhosted.org/packages/7d/73/041928e434442bd3afde5584bdc3f932fb4562b1597629f537387cec6f3d/coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", size = 207127, upload-time = "2024-08-04T19:43:10.15Z" }, 114 | { url = "https://files.pythonhosted.org/packages/c7/c8/6ca52b5147828e45ad0242388477fdb90df2c6cbb9a441701a12b3c71bc8/coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", size = 235654, upload-time = "2024-08-04T19:43:12.405Z" }, 115 | { url = "https://files.pythonhosted.org/packages/d5/da/9ac2b62557f4340270942011d6efeab9833648380109e897d48ab7c1035d/coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc", size = 233598, upload-time = "2024-08-04T19:43:14.078Z" }, 116 | { url = "https://files.pythonhosted.org/packages/53/23/9e2c114d0178abc42b6d8d5281f651a8e6519abfa0ef460a00a91f80879d/coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", size = 234732, upload-time = "2024-08-04T19:43:16.632Z" }, 117 | { url = "https://files.pythonhosted.org/packages/0f/7e/a0230756fb133343a52716e8b855045f13342b70e48e8ad41d8a0d60ab98/coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", size = 233816, upload-time = "2024-08-04T19:43:19.049Z" }, 118 | { url = "https://files.pythonhosted.org/packages/28/7c/3753c8b40d232b1e5eeaed798c875537cf3cb183fb5041017c1fdb7ec14e/coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", size = 232325, upload-time = "2024-08-04T19:43:21.246Z" }, 119 | { url = "https://files.pythonhosted.org/packages/57/e3/818a2b2af5b7573b4b82cf3e9f137ab158c90ea750a8f053716a32f20f06/coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", size = 233418, upload-time = "2024-08-04T19:43:22.945Z" }, 120 | { url = "https://files.pythonhosted.org/packages/c8/fb/4532b0b0cefb3f06d201648715e03b0feb822907edab3935112b61b885e2/coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", size = 209343, upload-time = "2024-08-04T19:43:25.121Z" }, 121 | { url = "https://files.pythonhosted.org/packages/5a/25/af337cc7421eca1c187cc9c315f0a755d48e755d2853715bfe8c418a45fa/coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", size = 210136, upload-time = "2024-08-04T19:43:26.851Z" }, 122 | { url = "https://files.pythonhosted.org/packages/ad/5f/67af7d60d7e8ce61a4e2ddcd1bd5fb787180c8d0ae0fbd073f903b3dd95d/coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", size = 206796, upload-time = "2024-08-04T19:43:29.115Z" }, 123 | { url = "https://files.pythonhosted.org/packages/e1/0e/e52332389e057daa2e03be1fbfef25bb4d626b37d12ed42ae6281d0a274c/coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", size = 207244, upload-time = "2024-08-04T19:43:31.285Z" }, 124 | { url = "https://files.pythonhosted.org/packages/aa/cd/766b45fb6e090f20f8927d9c7cb34237d41c73a939358bc881883fd3a40d/coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", size = 239279, upload-time = "2024-08-04T19:43:33.581Z" }, 125 | { url = "https://files.pythonhosted.org/packages/70/6c/a9ccd6fe50ddaf13442a1e2dd519ca805cbe0f1fcd377fba6d8339b98ccb/coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", size = 236859, upload-time = "2024-08-04T19:43:35.301Z" }, 126 | { url = "https://files.pythonhosted.org/packages/14/6f/8351b465febb4dbc1ca9929505202db909c5a635c6fdf33e089bbc3d7d85/coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", size = 238549, upload-time = "2024-08-04T19:43:37.578Z" }, 127 | { url = "https://files.pythonhosted.org/packages/68/3c/289b81fa18ad72138e6d78c4c11a82b5378a312c0e467e2f6b495c260907/coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", size = 237477, upload-time = "2024-08-04T19:43:39.92Z" }, 128 | { url = "https://files.pythonhosted.org/packages/ed/1c/aa1efa6459d822bd72c4abc0b9418cf268de3f60eeccd65dc4988553bd8d/coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", size = 236134, upload-time = "2024-08-04T19:43:41.453Z" }, 129 | { url = "https://files.pythonhosted.org/packages/fb/c8/521c698f2d2796565fe9c789c2ee1ccdae610b3aa20b9b2ef980cc253640/coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", size = 236910, upload-time = "2024-08-04T19:43:43.037Z" }, 130 | { url = "https://files.pythonhosted.org/packages/7d/30/033e663399ff17dca90d793ee8a2ea2890e7fdf085da58d82468b4220bf7/coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", size = 209348, upload-time = "2024-08-04T19:43:44.787Z" }, 131 | { url = "https://files.pythonhosted.org/packages/20/05/0d1ccbb52727ccdadaa3ff37e4d2dc1cd4d47f0c3df9eb58d9ec8508ca88/coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", size = 210230, upload-time = "2024-08-04T19:43:46.707Z" }, 132 | { url = "https://files.pythonhosted.org/packages/7e/d4/300fc921dff243cd518c7db3a4c614b7e4b2431b0d1145c1e274fd99bd70/coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", size = 206983, upload-time = "2024-08-04T19:43:49.082Z" }, 133 | { url = "https://files.pythonhosted.org/packages/e1/ab/6bf00de5327ecb8db205f9ae596885417a31535eeda6e7b99463108782e1/coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", size = 207221, upload-time = "2024-08-04T19:43:52.15Z" }, 134 | { url = "https://files.pythonhosted.org/packages/92/8f/2ead05e735022d1a7f3a0a683ac7f737de14850395a826192f0288703472/coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", size = 240342, upload-time = "2024-08-04T19:43:53.746Z" }, 135 | { url = "https://files.pythonhosted.org/packages/0f/ef/94043e478201ffa85b8ae2d2c79b4081e5a1b73438aafafccf3e9bafb6b5/coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", size = 237371, upload-time = "2024-08-04T19:43:55.993Z" }, 136 | { url = "https://files.pythonhosted.org/packages/1f/0f/c890339dd605f3ebc269543247bdd43b703cce6825b5ed42ff5f2d6122c7/coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", size = 239455, upload-time = "2024-08-04T19:43:57.618Z" }, 137 | { url = "https://files.pythonhosted.org/packages/d1/04/7fd7b39ec7372a04efb0f70c70e35857a99b6a9188b5205efb4c77d6a57a/coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", size = 238924, upload-time = "2024-08-04T19:44:00.012Z" }, 138 | { url = "https://files.pythonhosted.org/packages/ed/bf/73ce346a9d32a09cf369f14d2a06651329c984e106f5992c89579d25b27e/coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", size = 237252, upload-time = "2024-08-04T19:44:01.713Z" }, 139 | { url = "https://files.pythonhosted.org/packages/86/74/1dc7a20969725e917b1e07fe71a955eb34bc606b938316bcc799f228374b/coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", size = 238897, upload-time = "2024-08-04T19:44:03.898Z" }, 140 | { url = "https://files.pythonhosted.org/packages/b6/e9/d9cc3deceb361c491b81005c668578b0dfa51eed02cd081620e9a62f24ec/coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", size = 209606, upload-time = "2024-08-04T19:44:05.532Z" }, 141 | { url = "https://files.pythonhosted.org/packages/47/c8/5a2e41922ea6740f77d555c4d47544acd7dc3f251fe14199c09c0f5958d3/coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", size = 210373, upload-time = "2024-08-04T19:44:07.079Z" }, 142 | { url = "https://files.pythonhosted.org/packages/8c/f9/9aa4dfb751cb01c949c990d136a0f92027fbcc5781c6e921df1cb1563f20/coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", size = 207007, upload-time = "2024-08-04T19:44:09.453Z" }, 143 | { url = "https://files.pythonhosted.org/packages/b9/67/e1413d5a8591622a46dd04ff80873b04c849268831ed5c304c16433e7e30/coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", size = 207269, upload-time = "2024-08-04T19:44:11.045Z" }, 144 | { url = "https://files.pythonhosted.org/packages/14/5b/9dec847b305e44a5634d0fb8498d135ab1d88330482b74065fcec0622224/coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", size = 239886, upload-time = "2024-08-04T19:44:12.83Z" }, 145 | { url = "https://files.pythonhosted.org/packages/7b/b7/35760a67c168e29f454928f51f970342d23cf75a2bb0323e0f07334c85f3/coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", size = 237037, upload-time = "2024-08-04T19:44:15.393Z" }, 146 | { url = "https://files.pythonhosted.org/packages/f7/95/d2fd31f1d638df806cae59d7daea5abf2b15b5234016a5ebb502c2f3f7ee/coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", size = 239038, upload-time = "2024-08-04T19:44:17.466Z" }, 147 | { url = "https://files.pythonhosted.org/packages/6e/bd/110689ff5752b67924efd5e2aedf5190cbbe245fc81b8dec1abaffba619d/coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", size = 238690, upload-time = "2024-08-04T19:44:19.336Z" }, 148 | { url = "https://files.pythonhosted.org/packages/d3/a8/08d7b38e6ff8df52331c83130d0ab92d9c9a8b5462f9e99c9f051a4ae206/coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", size = 236765, upload-time = "2024-08-04T19:44:20.994Z" }, 149 | { url = "https://files.pythonhosted.org/packages/d6/6a/9cf96839d3147d55ae713eb2d877f4d777e7dc5ba2bce227167d0118dfe8/coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", size = 238611, upload-time = "2024-08-04T19:44:22.616Z" }, 150 | { url = "https://files.pythonhosted.org/packages/74/e4/7ff20d6a0b59eeaab40b3140a71e38cf52547ba21dbcf1d79c5a32bba61b/coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", size = 209671, upload-time = "2024-08-04T19:44:24.418Z" }, 151 | { url = "https://files.pythonhosted.org/packages/35/59/1812f08a85b57c9fdb6d0b383d779e47b6f643bc278ed682859512517e83/coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", size = 210368, upload-time = "2024-08-04T19:44:26.276Z" }, 152 | { url = "https://files.pythonhosted.org/packages/9c/15/08913be1c59d7562a3e39fce20661a98c0a3f59d5754312899acc6cb8a2d/coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", size = 207758, upload-time = "2024-08-04T19:44:29.028Z" }, 153 | { url = "https://files.pythonhosted.org/packages/c4/ae/b5d58dff26cade02ada6ca612a76447acd69dccdbb3a478e9e088eb3d4b9/coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", size = 208035, upload-time = "2024-08-04T19:44:30.673Z" }, 154 | { url = "https://files.pythonhosted.org/packages/b8/d7/62095e355ec0613b08dfb19206ce3033a0eedb6f4a67af5ed267a8800642/coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", size = 250839, upload-time = "2024-08-04T19:44:32.412Z" }, 155 | { url = "https://files.pythonhosted.org/packages/7c/1e/c2967cb7991b112ba3766df0d9c21de46b476d103e32bb401b1b2adf3380/coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", size = 246569, upload-time = "2024-08-04T19:44:34.547Z" }, 156 | { url = "https://files.pythonhosted.org/packages/8b/61/a7a6a55dd266007ed3b1df7a3386a0d760d014542d72f7c2c6938483b7bd/coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", size = 248927, upload-time = "2024-08-04T19:44:36.313Z" }, 157 | { url = "https://files.pythonhosted.org/packages/c8/fa/13a6f56d72b429f56ef612eb3bc5ce1b75b7ee12864b3bd12526ab794847/coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", size = 248401, upload-time = "2024-08-04T19:44:38.155Z" }, 158 | { url = "https://files.pythonhosted.org/packages/75/06/0429c652aa0fb761fc60e8c6b291338c9173c6aa0f4e40e1902345b42830/coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", size = 246301, upload-time = "2024-08-04T19:44:39.883Z" }, 159 | { url = "https://files.pythonhosted.org/packages/52/76/1766bb8b803a88f93c3a2d07e30ffa359467810e5cbc68e375ebe6906efb/coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", size = 247598, upload-time = "2024-08-04T19:44:41.59Z" }, 160 | { url = "https://files.pythonhosted.org/packages/66/8b/f54f8db2ae17188be9566e8166ac6df105c1c611e25da755738025708d54/coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", size = 210307, upload-time = "2024-08-04T19:44:43.301Z" }, 161 | { url = "https://files.pythonhosted.org/packages/9f/b0/e0dca6da9170aefc07515cce067b97178cefafb512d00a87a1c717d2efd5/coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", size = 211453, upload-time = "2024-08-04T19:44:45.677Z" }, 162 | { url = "https://files.pythonhosted.org/packages/19/d3/d54c5aa83268779d54c86deb39c1c4566e5d45c155369ca152765f8db413/coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", size = 206688, upload-time = "2024-08-04T19:45:08.358Z" }, 163 | { url = "https://files.pythonhosted.org/packages/a5/fe/137d5dca72e4a258b1bc17bb04f2e0196898fe495843402ce826a7419fe3/coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", size = 207120, upload-time = "2024-08-04T19:45:11.526Z" }, 164 | { url = "https://files.pythonhosted.org/packages/78/5b/a0a796983f3201ff5485323b225d7c8b74ce30c11f456017e23d8e8d1945/coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", size = 235249, upload-time = "2024-08-04T19:45:13.202Z" }, 165 | { url = "https://files.pythonhosted.org/packages/4e/e1/76089d6a5ef9d68f018f65411fcdaaeb0141b504587b901d74e8587606ad/coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", size = 233237, upload-time = "2024-08-04T19:45:14.961Z" }, 166 | { url = "https://files.pythonhosted.org/packages/9a/6f/eef79b779a540326fee9520e5542a8b428cc3bfa8b7c8f1022c1ee4fc66c/coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", size = 234311, upload-time = "2024-08-04T19:45:16.924Z" }, 167 | { url = "https://files.pythonhosted.org/packages/75/e1/656d65fb126c29a494ef964005702b012f3498db1a30dd562958e85a4049/coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", size = 233453, upload-time = "2024-08-04T19:45:18.672Z" }, 168 | { url = "https://files.pythonhosted.org/packages/68/6a/45f108f137941a4a1238c85f28fd9d048cc46b5466d6b8dda3aba1bb9d4f/coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", size = 231958, upload-time = "2024-08-04T19:45:20.63Z" }, 169 | { url = "https://files.pythonhosted.org/packages/9b/e7/47b809099168b8b8c72ae311efc3e88c8d8a1162b3ba4b8da3cfcdb85743/coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", size = 232938, upload-time = "2024-08-04T19:45:23.062Z" }, 170 | { url = "https://files.pythonhosted.org/packages/52/80/052222ba7058071f905435bad0ba392cc12006380731c37afaf3fe749b88/coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", size = 209352, upload-time = "2024-08-04T19:45:25.042Z" }, 171 | { url = "https://files.pythonhosted.org/packages/b8/d8/1b92e0b3adcf384e98770a00ca095da1b5f7b483e6563ae4eb5e935d24a1/coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", size = 210153, upload-time = "2024-08-04T19:45:27.079Z" }, 172 | { url = "https://files.pythonhosted.org/packages/a5/2b/0354ed096bca64dc8e32a7cbcae28b34cb5ad0b1fe2125d6d99583313ac0/coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", size = 198926, upload-time = "2024-08-04T19:45:28.875Z" }, 173 | ] 174 | 175 | [package.optional-dependencies] 176 | toml = [ 177 | { name = "tomli", marker = "python_full_version <= '3.11'" }, 178 | ] 179 | 180 | [[package]] 181 | name = "decorator" 182 | version = "5.2.1" 183 | source = { registry = "https://pypi.org/simple" } 184 | sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } 185 | wheels = [ 186 | { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, 187 | ] 188 | 189 | [[package]] 190 | name = "distlib" 191 | version = "0.3.9" 192 | source = { registry = "https://pypi.org/simple" } 193 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } 194 | wheels = [ 195 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, 196 | ] 197 | 198 | [[package]] 199 | name = "exceptiongroup" 200 | version = "1.2.2" 201 | source = { registry = "https://pypi.org/simple" } 202 | sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } 203 | wheels = [ 204 | { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, 205 | ] 206 | 207 | [[package]] 208 | name = "filelock" 209 | version = "3.16.1" 210 | source = { registry = "https://pypi.org/simple" } 211 | sdist = { url = "https://files.pythonhosted.org/packages/9d/db/3ef5bb276dae18d6ec2124224403d1d67bccdbefc17af4cc8f553e341ab1/filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435", size = 18037, upload-time = "2024-09-17T19:02:01.779Z" } 212 | wheels = [ 213 | { url = "https://files.pythonhosted.org/packages/b9/f8/feced7779d755758a52d1f6635d990b8d98dc0a29fa568bbe0625f18fdf3/filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", size = 16163, upload-time = "2024-09-17T19:02:00.268Z" }, 214 | ] 215 | 216 | [[package]] 217 | name = "flake8" 218 | version = "5.0.4" 219 | source = { registry = "https://pypi.org/simple" } 220 | dependencies = [ 221 | { name = "mccabe" }, 222 | { name = "pycodestyle" }, 223 | { name = "pyflakes" }, 224 | ] 225 | sdist = { url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", size = 145862, upload-time = "2022-08-03T23:21:27.108Z" } 226 | wheels = [ 227 | { url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248", size = 61897, upload-time = "2022-08-03T23:21:25.027Z" }, 228 | ] 229 | 230 | [[package]] 231 | name = "identify" 232 | version = "2.6.1" 233 | source = { registry = "https://pypi.org/simple" } 234 | sdist = { url = "https://files.pythonhosted.org/packages/29/bb/25024dbcc93516c492b75919e76f389bac754a3e4248682fba32b250c880/identify-2.6.1.tar.gz", hash = "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98", size = 99097, upload-time = "2024-09-14T23:50:32.513Z" } 235 | wheels = [ 236 | { url = "https://files.pythonhosted.org/packages/7d/0c/4ef72754c050979fdcc06c744715ae70ea37e734816bb6514f79df77a42f/identify-2.6.1-py2.py3-none-any.whl", hash = "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", size = 98972, upload-time = "2024-09-14T23:50:30.747Z" }, 237 | ] 238 | 239 | [[package]] 240 | name = "idna" 241 | version = "3.10" 242 | source = { registry = "https://pypi.org/simple" } 243 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } 244 | wheels = [ 245 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, 246 | ] 247 | 248 | [[package]] 249 | name = "iniconfig" 250 | version = "2.1.0" 251 | source = { registry = "https://pypi.org/simple" } 252 | sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } 253 | wheels = [ 254 | { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, 255 | ] 256 | 257 | [[package]] 258 | name = "isort" 259 | version = "5.13.2" 260 | source = { registry = "https://pypi.org/simple" } 261 | sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" } 262 | wheels = [ 263 | { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, 264 | ] 265 | 266 | [[package]] 267 | name = "marshmallow" 268 | version = "3.26.1" 269 | source = { registry = "https://pypi.org/simple" } 270 | dependencies = [ 271 | { name = "packaging" }, 272 | ] 273 | sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } 274 | wheels = [ 275 | { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, 276 | ] 277 | 278 | [[package]] 279 | name = "mccabe" 280 | version = "0.7.0" 281 | source = { registry = "https://pypi.org/simple" } 282 | sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } 283 | wheels = [ 284 | { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, 285 | ] 286 | 287 | [[package]] 288 | name = "nodeenv" 289 | version = "1.9.1" 290 | source = { registry = "https://pypi.org/simple" } 291 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } 292 | wheels = [ 293 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, 294 | ] 295 | 296 | [[package]] 297 | name = "packaging" 298 | version = "25.0" 299 | source = { registry = "https://pypi.org/simple" } 300 | sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } 301 | wheels = [ 302 | { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, 303 | ] 304 | 305 | [[package]] 306 | name = "platformdirs" 307 | version = "4.3.6" 308 | source = { registry = "https://pypi.org/simple" } 309 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" } 310 | wheels = [ 311 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439, upload-time = "2024-09-17T19:06:49.212Z" }, 312 | ] 313 | 314 | [[package]] 315 | name = "pluggy" 316 | version = "1.5.0" 317 | source = { registry = "https://pypi.org/simple" } 318 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } 319 | wheels = [ 320 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, 321 | ] 322 | 323 | [[package]] 324 | name = "pre-commit" 325 | version = "3.5.0" 326 | source = { registry = "https://pypi.org/simple" } 327 | dependencies = [ 328 | { name = "cfgv" }, 329 | { name = "identify" }, 330 | { name = "nodeenv" }, 331 | { name = "pyyaml" }, 332 | { name = "virtualenv" }, 333 | ] 334 | sdist = { url = "https://files.pythonhosted.org/packages/04/b3/4ae08d21eb097162f5aad37f4585f8069a86402ed7f5362cc9ae097f9572/pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", size = 177079, upload-time = "2023-10-13T15:57:48.334Z" } 335 | wheels = [ 336 | { url = "https://files.pythonhosted.org/packages/6c/75/526915fedf462e05eeb1c75ceaf7e3f9cde7b5ce6f62740fe5f7f19a0050/pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660", size = 203698, upload-time = "2023-10-13T15:57:46.378Z" }, 337 | ] 338 | 339 | [[package]] 340 | name = "pycodestyle" 341 | version = "2.9.1" 342 | source = { registry = "https://pypi.org/simple" } 343 | sdist = { url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", size = 102127, upload-time = "2022-08-03T23:13:29.715Z" } 344 | wheels = [ 345 | { url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b", size = 41493, upload-time = "2022-08-03T23:13:27.416Z" }, 346 | ] 347 | 348 | [[package]] 349 | name = "pyflakes" 350 | version = "2.5.0" 351 | source = { registry = "https://pypi.org/simple" } 352 | sdist = { url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3", size = 66388, upload-time = "2022-07-30T17:29:05.816Z" } 353 | wheels = [ 354 | { url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", size = 66116, upload-time = "2022-07-30T17:29:04.179Z" }, 355 | ] 356 | 357 | [[package]] 358 | name = "pytest" 359 | version = "8.3.5" 360 | source = { registry = "https://pypi.org/simple" } 361 | dependencies = [ 362 | { name = "colorama", marker = "sys_platform == 'win32'" }, 363 | { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, 364 | { name = "iniconfig" }, 365 | { name = "packaging" }, 366 | { name = "pluggy" }, 367 | { name = "tomli", marker = "python_full_version < '3.11'" }, 368 | ] 369 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } 370 | wheels = [ 371 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, 372 | ] 373 | 374 | [[package]] 375 | name = "pytest-cov" 376 | version = "5.0.0" 377 | source = { registry = "https://pypi.org/simple" } 378 | dependencies = [ 379 | { name = "coverage", extra = ["toml"] }, 380 | { name = "pytest" }, 381 | ] 382 | sdist = { url = "https://files.pythonhosted.org/packages/74/67/00efc8d11b630c56f15f4ad9c7f9223f1e5ec275aaae3fa9118c6a223ad2/pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857", size = 63042, upload-time = "2024-03-24T20:16:34.856Z" } 383 | wheels = [ 384 | { url = "https://files.pythonhosted.org/packages/78/3a/af5b4fa5961d9a1e6237b530eb87dd04aea6eb83da09d2a4073d81b54ccf/pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652", size = 21990, upload-time = "2024-03-24T20:16:32.444Z" }, 385 | ] 386 | 387 | [[package]] 388 | name = "pytest-jira" 389 | source = { editable = "." } 390 | dependencies = [ 391 | { name = "marshmallow" }, 392 | { name = "packaging" }, 393 | { name = "pytest" }, 394 | { name = "requests" }, 395 | { name = "retry2" }, 396 | { name = "six" }, 397 | ] 398 | 399 | [package.dev-dependencies] 400 | tests = [ 401 | { name = "coverage" }, 402 | { name = "flake8" }, 403 | { name = "isort" }, 404 | { name = "pre-commit" }, 405 | { name = "pytest" }, 406 | { name = "pytest-cov" }, 407 | ] 408 | 409 | [package.metadata] 410 | requires-dist = [ 411 | { name = "marshmallow", specifier = "==3.26.1" }, 412 | { name = "packaging" }, 413 | { name = "pytest", specifier = ">=2.2.4" }, 414 | { name = "requests", specifier = ">=2.13.0" }, 415 | { name = "retry2", specifier = ">=0.9.5" }, 416 | { name = "six" }, 417 | ] 418 | 419 | [package.metadata.requires-dev] 420 | tests = [ 421 | { name = "coverage" }, 422 | { name = "flake8" }, 423 | { name = "isort" }, 424 | { name = "pre-commit" }, 425 | { name = "pytest" }, 426 | { name = "pytest-cov" }, 427 | ] 428 | 429 | [[package]] 430 | name = "pyyaml" 431 | version = "6.0.2" 432 | source = { registry = "https://pypi.org/simple" } 433 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } 434 | wheels = [ 435 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, 436 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, 437 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, 438 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, 439 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, 440 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, 441 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, 442 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, 443 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, 444 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, 445 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, 446 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, 447 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, 448 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, 449 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, 450 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, 451 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, 452 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, 453 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, 454 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, 455 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, 456 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, 457 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, 458 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, 459 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, 460 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, 461 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, 462 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, 463 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, 464 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, 465 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, 466 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, 467 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, 468 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, 469 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, 470 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, 471 | { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, 472 | { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, 473 | { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, 474 | { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, 475 | { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, 476 | { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, 477 | { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, 478 | { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, 479 | { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, 480 | ] 481 | 482 | [[package]] 483 | name = "requests" 484 | version = "2.32.3" 485 | source = { registry = "https://pypi.org/simple" } 486 | dependencies = [ 487 | { name = "certifi" }, 488 | { name = "charset-normalizer" }, 489 | { name = "idna" }, 490 | { name = "urllib3" }, 491 | ] 492 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } 493 | wheels = [ 494 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, 495 | ] 496 | 497 | [[package]] 498 | name = "retry2" 499 | version = "0.9.5" 500 | source = { registry = "https://pypi.org/simple" } 501 | dependencies = [ 502 | { name = "decorator" }, 503 | ] 504 | wheels = [ 505 | { url = "https://files.pythonhosted.org/packages/97/49/1cae6d9b932378cc75f902fa70648945b7ea7190cb0d09ff83b47de3e60a/retry2-0.9.5-py2.py3-none-any.whl", hash = "sha256:f7fee13b1e15d0611c462910a6aa72a8919823988dd0412152bc3719c89a4e55", size = 6013, upload-time = "2023-01-11T21:49:08.397Z" }, 506 | ] 507 | 508 | [[package]] 509 | name = "six" 510 | version = "1.17.0" 511 | source = { registry = "https://pypi.org/simple" } 512 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } 513 | wheels = [ 514 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, 515 | ] 516 | 517 | [[package]] 518 | name = "tomli" 519 | version = "2.2.1" 520 | source = { registry = "https://pypi.org/simple" } 521 | sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } 522 | wheels = [ 523 | { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, 524 | { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, 525 | { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, 526 | { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, 527 | { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, 528 | { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, 529 | { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, 530 | { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, 531 | { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, 532 | { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, 533 | { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, 534 | { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, 535 | { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, 536 | { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, 537 | { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, 538 | { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, 539 | { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, 540 | { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, 541 | { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, 542 | { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, 543 | { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, 544 | { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, 545 | { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, 546 | { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, 547 | { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, 548 | { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, 549 | { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, 550 | { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, 551 | { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, 552 | { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, 553 | { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, 554 | ] 555 | 556 | [[package]] 557 | name = "urllib3" 558 | version = "2.4.0" 559 | source = { registry = "https://pypi.org/simple" } 560 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } 561 | wheels = [ 562 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, 563 | ] 564 | 565 | [[package]] 566 | name = "virtualenv" 567 | version = "20.30.0" 568 | source = { registry = "https://pypi.org/simple" } 569 | dependencies = [ 570 | { name = "distlib" }, 571 | { name = "filelock" }, 572 | { name = "platformdirs" }, 573 | ] 574 | sdist = { url = "https://files.pythonhosted.org/packages/38/e0/633e369b91bbc664df47dcb5454b6c7cf441e8f5b9d0c250ce9f0546401e/virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8", size = 4346945, upload-time = "2025-03-31T16:33:29.185Z" } 575 | wheels = [ 576 | { url = "https://files.pythonhosted.org/packages/4c/ed/3cfeb48175f0671ec430ede81f628f9fb2b1084c9064ca67ebe8c0ed6a05/virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6", size = 4329461, upload-time = "2025-03-31T16:33:26.758Z" }, 577 | ] 578 | --------------------------------------------------------------------------------