init
This commit is contained in:
commit
76266cedc6
23 changed files with 4041 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
/build/
|
||||||
|
/.venv
|
||||||
392
LICENSE
Normal file
392
LICENSE
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
Attribution-NoDerivatives 4.0 International
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||||
|
does not provide legal services or legal advice. Distribution of
|
||||||
|
Creative Commons public licenses does not create a lawyer-client or
|
||||||
|
other relationship. Creative Commons makes its licenses and related
|
||||||
|
information available on an "as-is" basis. Creative Commons gives no
|
||||||
|
warranties regarding its licenses, any material licensed under their
|
||||||
|
terms and conditions, or any related information. Creative Commons
|
||||||
|
disclaims all liability for damages resulting from their use to the
|
||||||
|
fullest extent possible.
|
||||||
|
|
||||||
|
Using Creative Commons Public Licenses
|
||||||
|
|
||||||
|
Creative Commons public licenses provide a standard set of terms and
|
||||||
|
conditions that creators and other rights holders may use to share
|
||||||
|
original works of authorship and other material subject to copyright
|
||||||
|
and certain other rights specified in the public license below. The
|
||||||
|
following considerations are for informational purposes only, are not
|
||||||
|
exhaustive, and do not form part of our licenses.
|
||||||
|
|
||||||
|
Considerations for licensors: Our public licenses are
|
||||||
|
intended for use by those authorized to give the public
|
||||||
|
permission to use material in ways otherwise restricted by
|
||||||
|
copyright and certain other rights. Our licenses are
|
||||||
|
irrevocable. Licensors should read and understand the terms
|
||||||
|
and conditions of the license they choose before applying it.
|
||||||
|
Licensors should also secure all rights necessary before
|
||||||
|
applying our licenses so that the public can reuse the
|
||||||
|
material as expected. Licensors should clearly mark any
|
||||||
|
material not subject to the license. This includes other CC-
|
||||||
|
licensed material, or material used under an exception or
|
||||||
|
limitation to copyright. More considerations for licensors:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensors
|
||||||
|
|
||||||
|
Considerations for the public: By using one of our public
|
||||||
|
licenses, a licensor grants the public permission to use the
|
||||||
|
licensed material under specified terms and conditions. If
|
||||||
|
the licensor's permission is not necessary for any reason--for
|
||||||
|
example, because of any applicable exception or limitation to
|
||||||
|
copyright--then that use is not regulated by the license. Our
|
||||||
|
licenses grant only permissions under copyright and certain
|
||||||
|
other rights that a licensor has authority to grant. Use of
|
||||||
|
the licensed material may still be restricted for other
|
||||||
|
reasons, including because others have copyright or other
|
||||||
|
rights in the material. A licensor may make special requests,
|
||||||
|
such as asking that all changes be marked or described.
|
||||||
|
Although not required by our licenses, you are encouraged to
|
||||||
|
respect those requests where reasonable. More considerations
|
||||||
|
for the public:
|
||||||
|
wiki.creativecommons.org/Considerations_for_licensees
|
||||||
|
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons Attribution-NoDerivatives 4.0 International Public
|
||||||
|
License
|
||||||
|
|
||||||
|
By exercising the Licensed Rights (defined below), You accept and agree
|
||||||
|
to be bound by the terms and conditions of this Creative Commons
|
||||||
|
Attribution-NoDerivatives 4.0 International Public License ("Public
|
||||||
|
License"). To the extent this Public License may be interpreted as a
|
||||||
|
contract, You are granted the Licensed Rights in consideration of Your
|
||||||
|
acceptance of these terms and conditions, and the Licensor grants You
|
||||||
|
such rights in consideration of benefits the Licensor receives from
|
||||||
|
making the Licensed Material available under these terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
|
||||||
|
Section 1 -- Definitions.
|
||||||
|
|
||||||
|
a. Adapted Material means material subject to Copyright and Similar
|
||||||
|
Rights that is derived from or based upon the Licensed Material
|
||||||
|
and in which the Licensed Material is translated, altered,
|
||||||
|
arranged, transformed, or otherwise modified in a manner requiring
|
||||||
|
permission under the Copyright and Similar Rights held by the
|
||||||
|
Licensor. For purposes of this Public License, where the Licensed
|
||||||
|
Material is a musical work, performance, or sound recording,
|
||||||
|
Adapted Material is always produced where the Licensed Material is
|
||||||
|
synched in timed relation with a moving image.
|
||||||
|
|
||||||
|
b. Copyright and Similar Rights means copyright and/or similar rights
|
||||||
|
closely related to copyright including, without limitation,
|
||||||
|
performance, broadcast, sound recording, and Sui Generis Database
|
||||||
|
Rights, without regard to how the rights are labeled or
|
||||||
|
categorized. For purposes of this Public License, the rights
|
||||||
|
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||||
|
Rights.
|
||||||
|
|
||||||
|
c. Effective Technological Measures means those measures that, in the
|
||||||
|
absence of proper authority, may not be circumvented under laws
|
||||||
|
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||||
|
Treaty adopted on December 20, 1996, and/or similar international
|
||||||
|
agreements.
|
||||||
|
|
||||||
|
d. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||||
|
any other exception or limitation to Copyright and Similar Rights
|
||||||
|
that applies to Your use of the Licensed Material.
|
||||||
|
|
||||||
|
e. Licensed Material means the artistic or literary work, database,
|
||||||
|
or other material to which the Licensor applied this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
f. Licensed Rights means the rights granted to You subject to the
|
||||||
|
terms and conditions of this Public License, which are limited to
|
||||||
|
all Copyright and Similar Rights that apply to Your use of the
|
||||||
|
Licensed Material and that the Licensor has authority to license.
|
||||||
|
|
||||||
|
g. Licensor means the individual(s) or entity(ies) granting rights
|
||||||
|
under this Public License.
|
||||||
|
|
||||||
|
h. Share means to provide material to the public by any means or
|
||||||
|
process that requires permission under the Licensed Rights, such
|
||||||
|
as reproduction, public display, public performance, distribution,
|
||||||
|
dissemination, communication, or importation, and to make material
|
||||||
|
available to the public including in ways that members of the
|
||||||
|
public may access the material from a place and at a time
|
||||||
|
individually chosen by them.
|
||||||
|
|
||||||
|
i. Sui Generis Database Rights means rights other than copyright
|
||||||
|
resulting from Directive 96/9/EC of the European Parliament and of
|
||||||
|
the Council of 11 March 1996 on the legal protection of databases,
|
||||||
|
as amended and/or succeeded, as well as other essentially
|
||||||
|
equivalent rights anywhere in the world.
|
||||||
|
|
||||||
|
j. You means the individual or entity exercising the Licensed Rights
|
||||||
|
under this Public License. Your has a corresponding meaning.
|
||||||
|
|
||||||
|
|
||||||
|
Section 2 -- Scope.
|
||||||
|
|
||||||
|
a. License grant.
|
||||||
|
|
||||||
|
1. Subject to the terms and conditions of this Public License,
|
||||||
|
the Licensor hereby grants You a worldwide, royalty-free,
|
||||||
|
non-sublicensable, non-exclusive, irrevocable license to
|
||||||
|
exercise the Licensed Rights in the Licensed Material to:
|
||||||
|
|
||||||
|
a. reproduce and Share the Licensed Material, in whole or
|
||||||
|
in part; and
|
||||||
|
|
||||||
|
b. produce and reproduce, but not Share, Adapted Material.
|
||||||
|
|
||||||
|
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||||
|
Exceptions and Limitations apply to Your use, this Public
|
||||||
|
License does not apply, and You do not need to comply with
|
||||||
|
its terms and conditions.
|
||||||
|
|
||||||
|
3. Term. The term of this Public License is specified in Section
|
||||||
|
6(a).
|
||||||
|
|
||||||
|
4. Media and formats; technical modifications allowed. The
|
||||||
|
Licensor authorizes You to exercise the Licensed Rights in
|
||||||
|
all media and formats whether now known or hereafter created,
|
||||||
|
and to make technical modifications necessary to do so. The
|
||||||
|
Licensor waives and/or agrees not to assert any right or
|
||||||
|
authority to forbid You from making technical modifications
|
||||||
|
necessary to exercise the Licensed Rights, including
|
||||||
|
technical modifications necessary to circumvent Effective
|
||||||
|
Technological Measures. For purposes of this Public License,
|
||||||
|
simply making modifications authorized by this Section 2(a)
|
||||||
|
(4) never produces Adapted Material.
|
||||||
|
|
||||||
|
5. Downstream recipients.
|
||||||
|
|
||||||
|
a. Offer from the Licensor -- Licensed Material. Every
|
||||||
|
recipient of the Licensed Material automatically
|
||||||
|
receives an offer from the Licensor to exercise the
|
||||||
|
Licensed Rights under the terms and conditions of this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
b. No downstream restrictions. You may not offer or impose
|
||||||
|
any additional or different terms or conditions on, or
|
||||||
|
apply any Effective Technological Measures to, the
|
||||||
|
Licensed Material if doing so restricts exercise of the
|
||||||
|
Licensed Rights by any recipient of the Licensed
|
||||||
|
Material.
|
||||||
|
|
||||||
|
6. No endorsement. Nothing in this Public License constitutes or
|
||||||
|
may be construed as permission to assert or imply that You
|
||||||
|
are, or that Your use of the Licensed Material is, connected
|
||||||
|
with, or sponsored, endorsed, or granted official status by,
|
||||||
|
the Licensor or others designated to receive attribution as
|
||||||
|
provided in Section 3(a)(1)(A)(i).
|
||||||
|
|
||||||
|
b. Other rights.
|
||||||
|
|
||||||
|
1. Moral rights, such as the right of integrity, are not
|
||||||
|
licensed under this Public License, nor are publicity,
|
||||||
|
privacy, and/or other similar personality rights; however, to
|
||||||
|
the extent possible, the Licensor waives and/or agrees not to
|
||||||
|
assert any such rights held by the Licensor to the limited
|
||||||
|
extent necessary to allow You to exercise the Licensed
|
||||||
|
Rights, but not otherwise.
|
||||||
|
|
||||||
|
2. Patent and trademark rights are not licensed under this
|
||||||
|
Public License.
|
||||||
|
|
||||||
|
3. To the extent possible, the Licensor waives any right to
|
||||||
|
collect royalties from You for the exercise of the Licensed
|
||||||
|
Rights, whether directly or through a collecting society
|
||||||
|
under any voluntary or waivable statutory or compulsory
|
||||||
|
licensing scheme. In all other cases the Licensor expressly
|
||||||
|
reserves any right to collect such royalties.
|
||||||
|
|
||||||
|
|
||||||
|
Section 3 -- License Conditions.
|
||||||
|
|
||||||
|
Your exercise of the Licensed Rights is expressly made subject to the
|
||||||
|
following conditions.
|
||||||
|
|
||||||
|
a. Attribution.
|
||||||
|
|
||||||
|
1. If You Share the Licensed Material, You must:
|
||||||
|
|
||||||
|
a. retain the following if it is supplied by the Licensor
|
||||||
|
with the Licensed Material:
|
||||||
|
|
||||||
|
i. identification of the creator(s) of the Licensed
|
||||||
|
Material and any others designated to receive
|
||||||
|
attribution, in any reasonable manner requested by
|
||||||
|
the Licensor (including by pseudonym if
|
||||||
|
designated);
|
||||||
|
|
||||||
|
ii. a copyright notice;
|
||||||
|
|
||||||
|
iii. a notice that refers to this Public License;
|
||||||
|
|
||||||
|
iv. a notice that refers to the disclaimer of
|
||||||
|
warranties;
|
||||||
|
|
||||||
|
v. a URI or hyperlink to the Licensed Material to the
|
||||||
|
extent reasonably practicable;
|
||||||
|
|
||||||
|
b. indicate if You modified the Licensed Material and
|
||||||
|
retain an indication of any previous modifications; and
|
||||||
|
|
||||||
|
c. indicate the Licensed Material is licensed under this
|
||||||
|
Public License, and include the text of, or the URI or
|
||||||
|
hyperlink to, this Public License.
|
||||||
|
|
||||||
|
For the avoidance of doubt, You do not have permission under
|
||||||
|
this Public License to Share Adapted Material.
|
||||||
|
|
||||||
|
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||||
|
reasonable manner based on the medium, means, and context in
|
||||||
|
which You Share the Licensed Material. For example, it may be
|
||||||
|
reasonable to satisfy the conditions by providing a URI or
|
||||||
|
hyperlink to a resource that includes the required
|
||||||
|
information.
|
||||||
|
|
||||||
|
3. If requested by the Licensor, You must remove any of the
|
||||||
|
information required by Section 3(a)(1)(A) to the extent
|
||||||
|
reasonably practicable.
|
||||||
|
|
||||||
|
|
||||||
|
Section 4 -- Sui Generis Database Rights.
|
||||||
|
|
||||||
|
Where the Licensed Rights include Sui Generis Database Rights that
|
||||||
|
apply to Your use of the Licensed Material:
|
||||||
|
|
||||||
|
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||||
|
to extract, reuse, reproduce, and Share all or a substantial
|
||||||
|
portion of the contents of the database, provided You do not Share
|
||||||
|
Adapted Material;
|
||||||
|
|
||||||
|
b. if You include all or a substantial portion of the database
|
||||||
|
contents in a database in which You have Sui Generis Database
|
||||||
|
Rights, then the database in which You have Sui Generis Database
|
||||||
|
Rights (but not its individual contents) is Adapted Material; and
|
||||||
|
|
||||||
|
c. You must comply with the conditions in Section 3(a) if You Share
|
||||||
|
all or a substantial portion of the contents of the database.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 4 supplements and does not
|
||||||
|
replace Your obligations under this Public License where the Licensed
|
||||||
|
Rights include other Copyright and Similar Rights.
|
||||||
|
|
||||||
|
|
||||||
|
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||||
|
|
||||||
|
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||||
|
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||||
|
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||||
|
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||||
|
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||||
|
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||||
|
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||||
|
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||||
|
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||||
|
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||||
|
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||||
|
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||||
|
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||||
|
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||||
|
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||||
|
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||||
|
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||||
|
|
||||||
|
c. The disclaimer of warranties and limitation of liability provided
|
||||||
|
above shall be interpreted in a manner that, to the extent
|
||||||
|
possible, most closely approximates an absolute disclaimer and
|
||||||
|
waiver of all liability.
|
||||||
|
|
||||||
|
|
||||||
|
Section 6 -- Term and Termination.
|
||||||
|
|
||||||
|
a. This Public License applies for the term of the Copyright and
|
||||||
|
Similar Rights licensed here. However, if You fail to comply with
|
||||||
|
this Public License, then Your rights under this Public License
|
||||||
|
terminate automatically.
|
||||||
|
|
||||||
|
b. Where Your right to use the Licensed Material has terminated under
|
||||||
|
Section 6(a), it reinstates:
|
||||||
|
|
||||||
|
1. automatically as of the date the violation is cured, provided
|
||||||
|
it is cured within 30 days of Your discovery of the
|
||||||
|
violation; or
|
||||||
|
|
||||||
|
2. upon express reinstatement by the Licensor.
|
||||||
|
|
||||||
|
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||||
|
right the Licensor may have to seek remedies for Your violations
|
||||||
|
of this Public License.
|
||||||
|
|
||||||
|
c. For the avoidance of doubt, the Licensor may also offer the
|
||||||
|
Licensed Material under separate terms or conditions or stop
|
||||||
|
distributing the Licensed Material at any time; however, doing so
|
||||||
|
will not terminate this Public License.
|
||||||
|
|
||||||
|
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||||
|
License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 7 -- Other Terms and Conditions.
|
||||||
|
|
||||||
|
a. The Licensor shall not be bound by any additional or different
|
||||||
|
terms or conditions communicated by You unless expressly agreed.
|
||||||
|
|
||||||
|
b. Any arrangements, understandings, or agreements regarding the
|
||||||
|
Licensed Material not stated herein are separate from and
|
||||||
|
independent of the terms and conditions of this Public License.
|
||||||
|
|
||||||
|
|
||||||
|
Section 8 -- Interpretation.
|
||||||
|
|
||||||
|
a. For the avoidance of doubt, this Public License does not, and
|
||||||
|
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||||
|
conditions on any use of the Licensed Material that could lawfully
|
||||||
|
be made without permission under this Public License.
|
||||||
|
|
||||||
|
b. To the extent possible, if any provision of this Public License is
|
||||||
|
deemed unenforceable, it shall be automatically reformed to the
|
||||||
|
minimum extent necessary to make it enforceable. If the provision
|
||||||
|
cannot be reformed, it shall be severed from this Public License
|
||||||
|
without affecting the enforceability of the remaining terms and
|
||||||
|
conditions.
|
||||||
|
|
||||||
|
c. No term or condition of this Public License will be waived and no
|
||||||
|
failure to comply consented to unless expressly agreed to by the
|
||||||
|
Licensor.
|
||||||
|
|
||||||
|
d. Nothing in this Public License constitutes or may be interpreted
|
||||||
|
as a limitation upon, or waiver of, any privileges and immunities
|
||||||
|
that apply to the Licensor or You, including from the legal
|
||||||
|
processes of any jurisdiction or authority.
|
||||||
|
|
||||||
|
=======================================================================
|
||||||
|
|
||||||
|
Creative Commons is not a party to its public
|
||||||
|
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||||
|
its public licenses to material it publishes and in those instances
|
||||||
|
will be considered the “Licensor.” The text of the Creative Commons
|
||||||
|
public licenses is dedicated to the public domain under the CC0 Public
|
||||||
|
Domain Dedication. Except for the limited purpose of indicating that
|
||||||
|
material is shared under a Creative Commons public license or as
|
||||||
|
otherwise permitted by the Creative Commons policies published at
|
||||||
|
creativecommons.org/policies, Creative Commons does not authorize the
|
||||||
|
use of the trademark "Creative Commons" or any other trademark or logo
|
||||||
|
of Creative Commons without its prior written consent including,
|
||||||
|
without limitation, in connection with any unauthorized modifications
|
||||||
|
to any of its public licenses or any other arrangements,
|
||||||
|
understandings, or agreements concerning use of licensed material. For
|
||||||
|
the avoidance of doubt, this paragraph does not form part of the
|
||||||
|
public licenses.
|
||||||
|
|
||||||
|
Creative Commons may be contacted at creativecommons.org.
|
||||||
12
Makefile
Normal file
12
Makefile
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.PHONY: clean
|
||||||
|
|
||||||
|
all: build/html build/markdown
|
||||||
|
|
||||||
|
build/html:
|
||||||
|
sphinx-build -b html src build/html
|
||||||
|
|
||||||
|
dist/markdown:
|
||||||
|
sphinx-build -M markdown src dist
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rv build
|
||||||
18
Pipfile
Normal file
18
Pipfile
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[[source]]
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
name = "pypi"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
sphinx = "==9.1.0"
|
||||||
|
sphinx-last-updated-by-git = "*"
|
||||||
|
sphinx-markdown-builder = "*"
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
|
||||||
|
[requires]
|
||||||
|
python_version = "3.13"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
build-html = "sphinx-build -b html src/ build/html"
|
||||||
|
build-md = "sphinx-build -M markdown src/ build/"
|
||||||
422
Pipfile.lock
generated
Normal file
422
Pipfile.lock
generated
Normal file
|
|
@ -0,0 +1,422 @@
|
||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "a889c35529339553277566894ad1f7630f478a0a996bee97ad7e704e938b9146"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {
|
||||||
|
"python_version": "3.13"
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"alabaster": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e",
|
||||||
|
"sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==1.0.0"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d",
|
||||||
|
"sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==2.18.0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c",
|
||||||
|
"sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==2026.1.4"
|
||||||
|
},
|
||||||
|
"charset-normalizer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad",
|
||||||
|
"sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93",
|
||||||
|
"sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394",
|
||||||
|
"sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89",
|
||||||
|
"sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc",
|
||||||
|
"sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86",
|
||||||
|
"sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63",
|
||||||
|
"sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d",
|
||||||
|
"sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f",
|
||||||
|
"sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8",
|
||||||
|
"sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0",
|
||||||
|
"sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505",
|
||||||
|
"sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161",
|
||||||
|
"sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af",
|
||||||
|
"sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152",
|
||||||
|
"sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318",
|
||||||
|
"sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72",
|
||||||
|
"sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4",
|
||||||
|
"sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e",
|
||||||
|
"sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3",
|
||||||
|
"sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576",
|
||||||
|
"sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c",
|
||||||
|
"sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1",
|
||||||
|
"sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8",
|
||||||
|
"sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1",
|
||||||
|
"sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2",
|
||||||
|
"sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44",
|
||||||
|
"sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26",
|
||||||
|
"sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88",
|
||||||
|
"sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016",
|
||||||
|
"sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede",
|
||||||
|
"sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf",
|
||||||
|
"sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a",
|
||||||
|
"sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc",
|
||||||
|
"sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0",
|
||||||
|
"sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84",
|
||||||
|
"sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db",
|
||||||
|
"sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1",
|
||||||
|
"sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7",
|
||||||
|
"sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed",
|
||||||
|
"sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8",
|
||||||
|
"sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133",
|
||||||
|
"sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e",
|
||||||
|
"sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef",
|
||||||
|
"sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14",
|
||||||
|
"sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2",
|
||||||
|
"sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0",
|
||||||
|
"sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d",
|
||||||
|
"sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828",
|
||||||
|
"sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f",
|
||||||
|
"sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf",
|
||||||
|
"sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6",
|
||||||
|
"sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328",
|
||||||
|
"sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090",
|
||||||
|
"sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa",
|
||||||
|
"sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381",
|
||||||
|
"sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c",
|
||||||
|
"sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb",
|
||||||
|
"sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc",
|
||||||
|
"sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a",
|
||||||
|
"sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec",
|
||||||
|
"sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc",
|
||||||
|
"sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac",
|
||||||
|
"sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e",
|
||||||
|
"sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313",
|
||||||
|
"sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569",
|
||||||
|
"sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3",
|
||||||
|
"sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d",
|
||||||
|
"sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525",
|
||||||
|
"sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894",
|
||||||
|
"sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3",
|
||||||
|
"sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9",
|
||||||
|
"sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a",
|
||||||
|
"sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9",
|
||||||
|
"sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14",
|
||||||
|
"sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25",
|
||||||
|
"sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50",
|
||||||
|
"sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf",
|
||||||
|
"sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1",
|
||||||
|
"sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3",
|
||||||
|
"sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac",
|
||||||
|
"sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e",
|
||||||
|
"sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815",
|
||||||
|
"sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c",
|
||||||
|
"sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6",
|
||||||
|
"sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6",
|
||||||
|
"sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e",
|
||||||
|
"sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4",
|
||||||
|
"sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84",
|
||||||
|
"sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69",
|
||||||
|
"sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15",
|
||||||
|
"sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191",
|
||||||
|
"sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0",
|
||||||
|
"sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897",
|
||||||
|
"sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd",
|
||||||
|
"sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2",
|
||||||
|
"sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794",
|
||||||
|
"sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d",
|
||||||
|
"sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074",
|
||||||
|
"sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3",
|
||||||
|
"sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224",
|
||||||
|
"sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838",
|
||||||
|
"sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a",
|
||||||
|
"sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d",
|
||||||
|
"sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d",
|
||||||
|
"sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f",
|
||||||
|
"sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8",
|
||||||
|
"sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490",
|
||||||
|
"sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966",
|
||||||
|
"sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9",
|
||||||
|
"sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3",
|
||||||
|
"sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e",
|
||||||
|
"sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.4.4"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968",
|
||||||
|
"sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==0.22.4"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea",
|
||||||
|
"sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==3.11"
|
||||||
|
},
|
||||||
|
"imagesize": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b",
|
||||||
|
"sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
|
"version": "==1.4.1"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d",
|
||||||
|
"sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==3.1.6"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f",
|
||||||
|
"sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a",
|
||||||
|
"sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf",
|
||||||
|
"sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19",
|
||||||
|
"sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf",
|
||||||
|
"sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c",
|
||||||
|
"sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175",
|
||||||
|
"sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219",
|
||||||
|
"sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb",
|
||||||
|
"sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6",
|
||||||
|
"sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab",
|
||||||
|
"sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26",
|
||||||
|
"sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1",
|
||||||
|
"sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce",
|
||||||
|
"sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218",
|
||||||
|
"sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634",
|
||||||
|
"sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695",
|
||||||
|
"sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad",
|
||||||
|
"sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73",
|
||||||
|
"sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c",
|
||||||
|
"sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe",
|
||||||
|
"sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa",
|
||||||
|
"sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559",
|
||||||
|
"sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa",
|
||||||
|
"sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37",
|
||||||
|
"sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758",
|
||||||
|
"sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f",
|
||||||
|
"sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8",
|
||||||
|
"sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d",
|
||||||
|
"sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c",
|
||||||
|
"sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97",
|
||||||
|
"sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a",
|
||||||
|
"sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19",
|
||||||
|
"sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9",
|
||||||
|
"sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9",
|
||||||
|
"sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc",
|
||||||
|
"sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2",
|
||||||
|
"sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4",
|
||||||
|
"sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354",
|
||||||
|
"sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50",
|
||||||
|
"sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698",
|
||||||
|
"sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9",
|
||||||
|
"sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b",
|
||||||
|
"sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc",
|
||||||
|
"sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115",
|
||||||
|
"sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e",
|
||||||
|
"sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485",
|
||||||
|
"sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f",
|
||||||
|
"sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12",
|
||||||
|
"sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025",
|
||||||
|
"sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009",
|
||||||
|
"sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d",
|
||||||
|
"sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b",
|
||||||
|
"sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a",
|
||||||
|
"sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5",
|
||||||
|
"sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f",
|
||||||
|
"sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d",
|
||||||
|
"sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1",
|
||||||
|
"sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287",
|
||||||
|
"sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6",
|
||||||
|
"sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f",
|
||||||
|
"sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581",
|
||||||
|
"sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed",
|
||||||
|
"sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b",
|
||||||
|
"sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c",
|
||||||
|
"sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026",
|
||||||
|
"sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8",
|
||||||
|
"sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676",
|
||||||
|
"sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6",
|
||||||
|
"sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e",
|
||||||
|
"sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d",
|
||||||
|
"sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d",
|
||||||
|
"sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01",
|
||||||
|
"sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7",
|
||||||
|
"sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419",
|
||||||
|
"sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795",
|
||||||
|
"sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1",
|
||||||
|
"sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5",
|
||||||
|
"sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d",
|
||||||
|
"sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42",
|
||||||
|
"sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe",
|
||||||
|
"sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda",
|
||||||
|
"sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e",
|
||||||
|
"sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737",
|
||||||
|
"sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523",
|
||||||
|
"sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591",
|
||||||
|
"sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc",
|
||||||
|
"sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a",
|
||||||
|
"sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==3.0.3"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4",
|
||||||
|
"sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==26.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887",
|
||||||
|
"sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==2.19.2"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6",
|
||||||
|
"sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.32.5"
|
||||||
|
},
|
||||||
|
"roman-numerals": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2",
|
||||||
|
"sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.10'",
|
||||||
|
"version": "==4.1.0"
|
||||||
|
},
|
||||||
|
"snowballstemmer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064",
|
||||||
|
"sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"
|
||||||
|
],
|
||||||
|
"markers": "python_version not in '3.0, 3.1, 3.2'",
|
||||||
|
"version": "==3.0.1"
|
||||||
|
},
|
||||||
|
"sphinx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb",
|
||||||
|
"sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.12'",
|
||||||
|
"version": "==9.1.0"
|
||||||
|
},
|
||||||
|
"sphinx-last-updated-by-git": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6382c8285ac1f222483a58569b78c0371af5e55f7fbf9c01e5e8a72d6fdfa499",
|
||||||
|
"sha256:c145011f4609d841805b69a9300099fc02fed8f5bb9e5bcef77d97aea97b7761"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==0.3.8"
|
||||||
|
},
|
||||||
|
"sphinx-markdown-builder": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:35b555760c48d4a38fe4b27813cb5ca636bbd22d8ef0742ac6959043f8000840",
|
||||||
|
"sha256:e89dc1b9eb837da430c2c230011fad95a3dfab0345ad503a32e35a31d284a722"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==0.6.9"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-applehelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1",
|
||||||
|
"sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-devhelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad",
|
||||||
|
"sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-htmlhelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8",
|
||||||
|
"sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.1.0"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-jsmath": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
|
||||||
|
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-qthelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab",
|
||||||
|
"sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-serializinghtml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331",
|
||||||
|
"sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"tabulate": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c",
|
||||||
|
"sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.7'",
|
||||||
|
"version": "==0.9.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed",
|
||||||
|
"sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.9'",
|
||||||
|
"version": "==2.6.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {}
|
||||||
|
}
|
||||||
1
scripts/ts-mime-todo/.npmrc
Normal file
1
scripts/ts-mime-todo/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry=http://registry.npmjs.org/
|
||||||
53
scripts/ts-mime-todo/bin/main.ts
Normal file
53
scripts/ts-mime-todo/bin/main.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// mime-todo/bin/main.ts
|
||||||
|
import { parseTodoFile } from "../lib/file"
|
||||||
|
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const [, , cmd, ...args] = process.argv
|
||||||
|
const todo = await parseTodoFile()
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case "issues":
|
||||||
|
for (const issue of todo.issues) {
|
||||||
|
console.log(`#${issue.id} [${issue.type}] (${issue.status}) ${issue.title}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "sprints":
|
||||||
|
for (const sprint of todo.sprints) {
|
||||||
|
console.log(`${sprint.name}: ${sprint.start}..${sprint.end}`)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case "issues-in-sprint": {
|
||||||
|
const name = args.join(" ")
|
||||||
|
const sprint = todo.sprints.find(s => s.name === name)
|
||||||
|
if (!sprint) {
|
||||||
|
console.error(`Sprint not found: ${name}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const { start, end } = sprint
|
||||||
|
for (const issue of todo.issues) {
|
||||||
|
const ds = issue.dueStart ?? issue.dueEnd
|
||||||
|
const de = issue.dueEnd ?? issue.dueStart
|
||||||
|
if (!ds || !de) continue
|
||||||
|
if (ds <= end && de >= start) {
|
||||||
|
console.log(`#${issue.id} [${issue.type}] (${issue.status}) ${issue.title}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.log(`Usage:
|
||||||
|
todo list
|
||||||
|
todo sprints
|
||||||
|
todo issues-in-sprint <name>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error(err)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
99
scripts/ts-mime-todo/lib/file.schema.json
Normal file
99
scripts/ts-mime-todo/lib/file.schema.json
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"title": "TODO File Schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sprints": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/Sprint" }
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/Issue" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["sprints", "issues"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
|
||||||
|
"definitions": {
|
||||||
|
"Sprint": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"start": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "start", "end"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"Issue": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["feature", "bugfix", "hotfix"]
|
||||||
|
},
|
||||||
|
"title": { "type": "string" },
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["open", "in-progress", "done", "hold", "cancelled"]
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["low", "medium", "high"]
|
||||||
|
},
|
||||||
|
"created": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"$ref": "#/definitions/IssueRelationships"
|
||||||
|
},
|
||||||
|
"dueStart": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||||
|
},
|
||||||
|
"dueEnd": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||||
|
},
|
||||||
|
"description": { "type": "string" },
|
||||||
|
"body": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"type",
|
||||||
|
"title",
|
||||||
|
"status",
|
||||||
|
"priority",
|
||||||
|
"created",
|
||||||
|
"relationships",
|
||||||
|
"description",
|
||||||
|
"body"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"IssueRelationships": {
|
||||||
|
"type": "object",
|
||||||
|
"patternProperties": {
|
||||||
|
"^(dependsOn|relatesTo|blocks)$": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "integer", "minimum": 1 }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
scripts/ts-mime-todo/lib/file.ts
Normal file
156
scripts/ts-mime-todo/lib/file.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
// mime-todo/lib/file.ts
|
||||||
|
import * as fs from "fs"
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
import { simpleParser } from "mailparser"
|
||||||
|
import yargs from 'yargs';
|
||||||
|
import { hideBin } from 'yargs/helpers';
|
||||||
|
|
||||||
|
import { parseIssue, Issue } from "./issue"
|
||||||
|
import { parseSprints, Sprint } from "./sprint"
|
||||||
|
|
||||||
|
import * as schema from "./file.schema.json"
|
||||||
|
|
||||||
|
import Ajv from "ajv"
|
||||||
|
|
||||||
|
const ajv = new Ajv({ allErrors: true })
|
||||||
|
|
||||||
|
const validateFile = ajv.compile(schema)
|
||||||
|
|
||||||
|
export interface TodoFile {
|
||||||
|
sprints: Sprint[]
|
||||||
|
issues: Issue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseMime(mimeText: string) {
|
||||||
|
return await simpleParser(mimeText)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function preprocessTODO(raw: string): string {
|
||||||
|
const boundary = "ISSUE"
|
||||||
|
|
||||||
|
const rawParts = raw
|
||||||
|
.split(`--${boundary}`)
|
||||||
|
.map(p => p.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
interface Part {
|
||||||
|
type: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts: Part[] = []
|
||||||
|
|
||||||
|
for (const rawPart of rawParts) {
|
||||||
|
const lines = rawPart.split(/\r?\n/)
|
||||||
|
const typeLine = lines[0]
|
||||||
|
|
||||||
|
if (!typeLine?.toLowerCase().startsWith("content-type:")) {
|
||||||
|
throw new Error(`Part missing Content-Type header:\n${rawPart}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = typeLine.slice("Content-Type:".length).trim()
|
||||||
|
const body = lines.slice(1).join("\n").trim()
|
||||||
|
|
||||||
|
parts.push({ type, body })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate sprint parts
|
||||||
|
const sprintParts = parts.filter(p => p.type === "application/sprints")
|
||||||
|
if (sprintParts.length > 1) {
|
||||||
|
throw new Error("Multiple application/sprints parts found")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sprintsPart = sprintParts[0] ?? null
|
||||||
|
|
||||||
|
// Preserve unknown types, but reorder sprints first
|
||||||
|
const orderedParts: Part[] = []
|
||||||
|
if (sprintsPart) orderedParts.push(sprintsPart)
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part !== sprintsPart) orderedParts.push(part)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME envelope
|
||||||
|
const out: string[] = []
|
||||||
|
out.push(`MIME-Version: 1.0`)
|
||||||
|
out.push(`Content-Type: multipart/mixed; boundary="${boundary}"`)
|
||||||
|
out.push("")
|
||||||
|
|
||||||
|
for (const part of orderedParts) {
|
||||||
|
out.push(`--${boundary}`)
|
||||||
|
out.push(`Content-Type: ${part.type}`)
|
||||||
|
out.push("")
|
||||||
|
out.push(part.body)
|
||||||
|
out.push("")
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push(`--${boundary}--`)
|
||||||
|
out.push("")
|
||||||
|
|
||||||
|
return out.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function parseTodoFile(path = "TODO"): Promise<TodoFile> {
|
||||||
|
const raw = fs.readFileSync(path, "utf-8")
|
||||||
|
const mimeWrapped = preprocessTODO(raw)
|
||||||
|
const parsed = await parseMime(mimeWrapped)
|
||||||
|
|
||||||
|
const sprints: Sprint[] = []
|
||||||
|
const issues: Issue[] = []
|
||||||
|
|
||||||
|
const parts =
|
||||||
|
(parsed as any).attachments ??
|
||||||
|
[]
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
|
||||||
|
const contentType = String(part.contentType || "").toLowerCase()
|
||||||
|
const body = part.content?.toString("utf-8") ?? ""
|
||||||
|
|
||||||
|
if (contentType.startsWith("application/sprints")) {
|
||||||
|
// Debug: Log what we're parsing
|
||||||
|
// console.log("Parsing sprints:", body);
|
||||||
|
sprints.push(...parseSprints(body))
|
||||||
|
} else if (contentType.startsWith("application/issue")) {
|
||||||
|
issues.push(parseIssue(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = {sprints: sprints, issues: issues };
|
||||||
|
|
||||||
|
if (!validateFile(file)) {
|
||||||
|
throw new Error(
|
||||||
|
"Sprint schema validation failed: " +
|
||||||
|
JSON.stringify(validateFile.errors, null, 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function index(yargs) {
|
||||||
|
console.log(yargs.argv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _index(yargs) {
|
||||||
|
return yargs.command('index', 'welcome ter yargs!', index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||||
|
|
||||||
|
let argv = yargs()
|
||||||
|
.usage('$0 <cmd> [args]')
|
||||||
|
.help()
|
||||||
|
|
||||||
|
let commands = [
|
||||||
|
_index
|
||||||
|
]
|
||||||
|
|
||||||
|
commands.forEach((method) => {
|
||||||
|
method(argv)
|
||||||
|
});
|
||||||
|
|
||||||
|
yargs.parse(hideBin(process.argv))
|
||||||
|
}
|
||||||
97
scripts/ts-mime-todo/lib/issue.ts
Normal file
97
scripts/ts-mime-todo/lib/issue.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// mime-todo/lib/issue.ts
|
||||||
|
|
||||||
|
|
||||||
|
export type RelationshipKind = "dependsOn" | "relatesTo" | "blocks"
|
||||||
|
|
||||||
|
export interface IssueRelationships {
|
||||||
|
[kind: string]: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IssueType = "feature" | "bugfix" | "hotfix"
|
||||||
|
export type IssueStatus = "open" | "in-progress" | "done" | "hold" | "cancelled"
|
||||||
|
export type IssuePriority = "low" | "medium" | "high"
|
||||||
|
|
||||||
|
export interface Issue {
|
||||||
|
id: number
|
||||||
|
type: IssueType
|
||||||
|
title: string
|
||||||
|
status: IssueStatus
|
||||||
|
priority: IssuePriority
|
||||||
|
created: string // YYYY-MM-DD
|
||||||
|
relationships: IssueRelationships
|
||||||
|
dueStart?: string // YYYY-MM-DD
|
||||||
|
dueEnd?: string // YYYY-MM-DD
|
||||||
|
description: string
|
||||||
|
body: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRelationships(text: string): IssueRelationships {
|
||||||
|
const relationships: IssueRelationships = {}
|
||||||
|
|
||||||
|
// Handle empty relationships
|
||||||
|
if (!text || text.trim() === "") {
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple parsing for format like "dependsOn:3"
|
||||||
|
const parts = text.split(/,\s*/).filter(p => p.trim() !== "")
|
||||||
|
for (const part of parts) {
|
||||||
|
const [key, value] = part.split(":").map(s => s.trim())
|
||||||
|
if (key && value) {
|
||||||
|
relationships[key] = value.split(/[\s]+/).map(Number).filter(n => !isNaN(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseIssue(text: string): Issue {
|
||||||
|
const lines = text.split(/\r?\n/)
|
||||||
|
const issue: Partial<Issue> = {}
|
||||||
|
|
||||||
|
let inDescription = false
|
||||||
|
const descLines: string[] = []
|
||||||
|
const bodyLines: string[] = []
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (!inDescription) {
|
||||||
|
if (line.startsWith("ID:")) issue.id = Number(line.slice(3).trim())
|
||||||
|
else if (line.startsWith("Type:")) issue.type = line.slice(5).trim()
|
||||||
|
else if (line.startsWith("Title:")) issue.title = line.slice(6).trim()
|
||||||
|
else if (line.startsWith("Status:")) issue.status = line.slice(7).trim()
|
||||||
|
else if (line.startsWith("Priority:")) issue.priority = line.slice(9).trim()
|
||||||
|
else if (line.startsWith("Created:")) issue.created = line.slice(8).trim()
|
||||||
|
else if (line.startsWith("DueStart:")) issue.dueStart = line.slice(9).trim()
|
||||||
|
else if (line.startsWith("DueEnd:")) issue.dueEnd = line.slice(7).trim()
|
||||||
|
else if (line.startsWith("Relationships:")) {
|
||||||
|
const rel = line.slice("Relationships:".length).trim()
|
||||||
|
issue.relationships = parseRelationships(rel)
|
||||||
|
}
|
||||||
|
else if (line.startsWith("Description:")) {
|
||||||
|
inDescription = true
|
||||||
|
descLines.push(line.slice("Description:".length).trim())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Description continuation
|
||||||
|
if (line.trim() === "") {
|
||||||
|
// blank line ends description
|
||||||
|
inDescription = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/^\s+/.test(line)) {
|
||||||
|
descLines.push(line.trim())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// first non-indented line ends description
|
||||||
|
inDescription = false
|
||||||
|
bodyLines.push(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issue.description = descLines.join("\n")
|
||||||
|
issue.body = bodyLines.join("\n")
|
||||||
|
|
||||||
|
return issue as Issue
|
||||||
|
}
|
||||||
2
scripts/ts-mime-todo/lib/out.txt
Normal file
2
scripts/ts-mime-todo/lib/out.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
//**/*
|
||||||
|
|
||||||
42
scripts/ts-mime-todo/lib/sprint.ts
Normal file
42
scripts/ts-mime-todo/lib/sprint.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// mime-todo/lib/sprint.ts
|
||||||
|
export interface Sprint {
|
||||||
|
name: string
|
||||||
|
start: string // YYYY-MM-DD
|
||||||
|
end: string // YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseSprints(text: string): Sprint[] {
|
||||||
|
const lines = text.split(/\r?\n/)
|
||||||
|
const sprints: Sprint[] = []
|
||||||
|
|
||||||
|
let current: Partial<Sprint> | null = null
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
// Start of sprint entry (must not trim indentation)
|
||||||
|
if (/^\s*-\s*(Name:.*)?$/.test(line)) {
|
||||||
|
if (current) sprints.push(current as Sprint)
|
||||||
|
current = {}
|
||||||
|
|
||||||
|
const match = line.match(/^\s*-\s*Name:\s*(.*)$/)
|
||||||
|
if (match) current.name = match[1]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Key-value pairs (must be indented)
|
||||||
|
const kv = line.match(/^\s+([A-Za-z][A-Za-z0-9]*):\s*(.*)$/)
|
||||||
|
if (kv && current) {
|
||||||
|
const key = kv[1]
|
||||||
|
const value = kv[2]
|
||||||
|
|
||||||
|
if (key === "Name") current.name = value
|
||||||
|
if (key === "Range") {
|
||||||
|
const [start, end] = value.split("..")
|
||||||
|
current.start = start
|
||||||
|
current.end = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current) sprints.push(current as Sprint)
|
||||||
|
return sprints
|
||||||
|
}
|
||||||
2172
scripts/ts-mime-todo/package-lock.json
generated
Normal file
2172
scripts/ts-mime-todo/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
26
scripts/ts-mime-todo/package.json
Normal file
26
scripts/ts-mime-todo/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "mime-todo",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/yargs": "^17.0.35",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"mailparser": "^3.9.3",
|
||||||
|
"tsx": "^3.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"vitest": "^4.0.18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"issues": "tsx scripts/todo/main.ts issues",
|
||||||
|
"sprints": "tsx scripts/todo/main.ts sprints",
|
||||||
|
"issues-in-sprint": "tsx scripts/todo/main.ts issues-in-sprint"
|
||||||
|
},
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib",
|
||||||
|
"test": "tests"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
scripts/ts-mime-todo/tests/_mocks/todo-basic.txt
Normal file
31
scripts/ts-mime-todo/tests/_mocks/todo-basic.txt
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/sprints
|
||||||
|
Sprints:
|
||||||
|
- Name: Sprint Alpha
|
||||||
|
Range: 2026-02-01..2026-02-14
|
||||||
|
-
|
||||||
|
Name: Sprint Beta
|
||||||
|
Range: 2026-02-15..2026-02-28
|
||||||
|
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/issue
|
||||||
|
ID: 1
|
||||||
|
Type: feature
|
||||||
|
Title: Add streaming parser
|
||||||
|
Status: open
|
||||||
|
Priority: high
|
||||||
|
Created: 2026-02-05
|
||||||
|
Relationships: dependsOn:3
|
||||||
|
Description: Implement streaming JSON parser.
|
||||||
|
Must support SAX-like events.
|
||||||
|
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/issue
|
||||||
|
ID: 2
|
||||||
|
Type: bugfix
|
||||||
|
Title: Fix wraparound
|
||||||
|
Status: in-progress
|
||||||
|
Priority: medium
|
||||||
|
Created: 2026-02-06
|
||||||
|
Relationships:
|
||||||
|
Description: Fix off-by-one in circular buffer.
|
||||||
10
scripts/ts-mime-todo/tests/_mocks/todo-issues-only.txt
Normal file
10
scripts/ts-mime-todo/tests/_mocks/todo-issues-only.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/issue
|
||||||
|
ID: 10
|
||||||
|
Type: hotfix
|
||||||
|
Title: Patch crash
|
||||||
|
Status: done
|
||||||
|
Priority: high
|
||||||
|
Created: 2026-02-03
|
||||||
|
Relationships:
|
||||||
|
Description: Fix crash in allocator.
|
||||||
26
scripts/ts-mime-todo/tests/_mocks/todo-multiple-sprints.txt
Normal file
26
scripts/ts-mime-todo/tests/_mocks/todo-multiple-sprints.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/sprints
|
||||||
|
Sprints:
|
||||||
|
- Name: Sprint Alpha
|
||||||
|
Range: 2026-02-01..2026-02-14
|
||||||
|
-
|
||||||
|
Name: Sprint Beta
|
||||||
|
Range: 2026-02-15..2026-02-28
|
||||||
|
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/sprints
|
||||||
|
Sprints:
|
||||||
|
- Name: Sprint Gamma
|
||||||
|
Range: 2026-02-28..2026-03-15
|
||||||
|
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/issue
|
||||||
|
ID: 1
|
||||||
|
Type: feature
|
||||||
|
Title: Add streaming parser
|
||||||
|
Status: open
|
||||||
|
Priority: high
|
||||||
|
Created: 2026-02-05
|
||||||
|
Relationships: dependsOn:3
|
||||||
|
Description: Implement streaming JSON parser.
|
||||||
|
Must support SAX-like events.
|
||||||
1
scripts/ts-mime-todo/tests/_mocks/todo-no-sprints.txt
Normal file
1
scripts/ts-mime-todo/tests/_mocks/todo-no-sprints.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Sprints:
|
||||||
55
scripts/ts-mime-todo/tests/lib/file.test.ts
Normal file
55
scripts/ts-mime-todo/tests/lib/file.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
import * as fs from "fs"
|
||||||
|
import { describe, it, expect } from "vitest"
|
||||||
|
import { parseSprints, preprocessTODO, parseTodoFile } from "../../lib/file"
|
||||||
|
|
||||||
|
|
||||||
|
describe("parseTodoFile", () => {
|
||||||
|
it("parses full TODO file end-to-end", async () => {
|
||||||
|
const todo = await parseTodoFile("tests/_mocks/todo-basic.txt")
|
||||||
|
|
||||||
|
expect(todo.sprints.length).toBe(2)
|
||||||
|
|
||||||
|
expect(todo.issues.length).toBe(2)
|
||||||
|
|
||||||
|
// expect(todo.issues[0].title).toBe("Add streaming parser")
|
||||||
|
expect(todo.sprints[0].name).toBe("Sprint Alpha")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("works with TODO containing only issues", async () => {
|
||||||
|
const todo = await parseTodoFile("tests/_mocks/todo-issues-only.txt")
|
||||||
|
expect(todo.sprints.length).toBe(0)
|
||||||
|
// expect(todo.issues.length).toBe(1) - skipping for now
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
describe("preprocessTODO", () => {
|
||||||
|
it("wraps TODO into MIME and puts sprints first", () => {
|
||||||
|
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
||||||
|
const mime = preprocessTODO(raw)
|
||||||
|
|
||||||
|
expect(mime).toContain("MIME-Version: 1.0")
|
||||||
|
expect(mime).toContain('Content-Type: multipart/mixed; boundary="ISSUE"')
|
||||||
|
|
||||||
|
const firstPartIndex = mime.indexOf("Content-Type: application/sprints")
|
||||||
|
const secondPartIndex = mime.indexOf("Content-Type: application/issue")
|
||||||
|
|
||||||
|
expect(firstPartIndex).toBeLessThan(secondPartIndex)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws on multiple sprints parts", () => {
|
||||||
|
const raw = fs.readFileSync("tests/_mocks/todo-multiple-sprints.txt", "utf-8")
|
||||||
|
expect(() => preprocessTODO(raw)).toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves unknown MIME types", () => {
|
||||||
|
const raw = `
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: application/unknown
|
||||||
|
|
||||||
|
Hello world
|
||||||
|
`
|
||||||
|
const mime = preprocessTODO(raw)
|
||||||
|
expect(mime).toContain("application/unknown")
|
||||||
|
})
|
||||||
|
})
|
||||||
32
scripts/ts-mime-todo/tests/lib/issue.test.ts
Normal file
32
scripts/ts-mime-todo/tests/lib/issue.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { describe, it, expect } from "vitest"
|
||||||
|
import * as fs from "fs"
|
||||||
|
import { parseIssue } from "../../lib/issue"
|
||||||
|
|
||||||
|
describe("parseIssue", () => {
|
||||||
|
it("parses all required fields", () => {
|
||||||
|
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
||||||
|
const issueText = raw.split("Content-Type: application/issue")[1]
|
||||||
|
const issue = parseIssue(issueText)
|
||||||
|
|
||||||
|
expect(issue.id).toBe(1)
|
||||||
|
expect(issue.type).toBe("feature")
|
||||||
|
expect(issue.status).toBe("open")
|
||||||
|
expect(issue.priority).toBe("high")
|
||||||
|
expect(issue.description).toContain("Implement streaming JSON parser.")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("parses empty relationships", () => {
|
||||||
|
const raw = `
|
||||||
|
ID: 2
|
||||||
|
Type: bugfix
|
||||||
|
Title: T
|
||||||
|
Status: open
|
||||||
|
Priority: low
|
||||||
|
Created: 2026-02-01
|
||||||
|
Relationships:
|
||||||
|
Description: X
|
||||||
|
`
|
||||||
|
const issue = parseIssue(raw)
|
||||||
|
expect(issue.relationships).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
22
scripts/ts-mime-todo/tests/lib/sprint.test.ts
Normal file
22
scripts/ts-mime-todo/tests/lib/sprint.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as fs from "fs"
|
||||||
|
import { describe, it, expect } from "vitest"
|
||||||
|
import { parseSprints } from "../../lib/sprint"
|
||||||
|
|
||||||
|
describe("parseSprints", () => {
|
||||||
|
it("parses compact and expanded sprint entries", () => {
|
||||||
|
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
||||||
|
const sprintsText = raw.split("Content-Type: application/sprints")[1]
|
||||||
|
const sprints = parseSprints(sprintsText)
|
||||||
|
|
||||||
|
expect(sprints.length).toBe(2)
|
||||||
|
expect(sprints[0].name).toBe("Sprint Alpha")
|
||||||
|
expect(sprints[0].start).toBe("2026-02-01")
|
||||||
|
expect(sprints[0].end).toBe("2026-02-14")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles TODO with no sprints", () => {
|
||||||
|
const raw = fs.readFileSync("tests/_mocks/todo-no-sprints.txt", "utf-8")
|
||||||
|
const sprints = parseSprints(raw)
|
||||||
|
expect(sprints.length).toBe(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
308
src/README.rst
Normal file
308
src/README.rst
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
############
|
||||||
|
Introduction
|
||||||
|
############
|
||||||
|
|
||||||
|
This document defines the canonical format, semantics, and processing rules
|
||||||
|
for the repository-root ``TODO`` file. The file is a human-friendly,
|
||||||
|
append-only (allowing modifications) issue tracker that is parsed by TypeScript tooling using a MIME
|
||||||
|
envelope added at parse time.
|
||||||
|
|
||||||
|
The raw ``TODO`` file is not a MIME document. A preprocessor wraps it into a
|
||||||
|
valid ``multipart/mixed`` MIME message before parsing.
|
||||||
|
|
||||||
|
|
||||||
|
Raw File Structure
|
||||||
|
==================
|
||||||
|
|
||||||
|
The raw ``TODO`` file consists of a sequence of *parts*, each beginning with::
|
||||||
|
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: <mime-type>
|
||||||
|
|
||||||
|
Valid part types are:
|
||||||
|
|
||||||
|
* ``application/sprints`` — defines sprint metadata
|
||||||
|
* ``application/issue`` — defines a single issue
|
||||||
|
|
||||||
|
Parts may appear in any order in the raw file. The preprocessor will reorder
|
||||||
|
them so that the ``application/sprints`` part appears first.
|
||||||
|
|
||||||
|
|
||||||
|
MIME Envelope (Added by Preprocessor)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Before parsing, the preprocessor wraps the raw file into a MIME multipart
|
||||||
|
message.
|
||||||
|
|
||||||
|
Prolog added by the preprocessor::
|
||||||
|
|
||||||
|
MIME-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed; boundary="ISSUE"
|
||||||
|
|
||||||
|
Each raw ``--ISSUE`` boundary becomes a MIME boundary. The preprocessor
|
||||||
|
extracts each part's ``Content-Type`` and body, reorders parts, and emits a
|
||||||
|
final closing boundary::
|
||||||
|
|
||||||
|
--ISSUE--
|
||||||
|
|
||||||
|
The resulting MIME message is parsed using a standard MIME parser.
|
||||||
|
|
||||||
|
|
||||||
|
Part: ``application/sprints``
|
||||||
|
=============================
|
||||||
|
|
||||||
|
This part defines sprint metadata.
|
||||||
|
|
||||||
|
The body MUST begin with::
|
||||||
|
|
||||||
|
Sprints:
|
||||||
|
|
||||||
|
followed by one or more sprint entries.
|
||||||
|
|
||||||
|
Sprint Entry Forms
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Both compact and expanded forms are valid.
|
||||||
|
|
||||||
|
Compact form::
|
||||||
|
|
||||||
|
- Name: Sprint 1
|
||||||
|
Range: 2026-02-01..2026-02-14
|
||||||
|
|
||||||
|
Expanded form::
|
||||||
|
|
||||||
|
-
|
||||||
|
Name: Sprint 1
|
||||||
|
Range: 2026-02-01..2026-02-14
|
||||||
|
|
||||||
|
Sprint Entry Grammar
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
A sprint entry begins at a line matching::
|
||||||
|
|
||||||
|
^\s*-\s*(Name:.*)?$
|
||||||
|
|
||||||
|
Key–value lines inside an entry match::
|
||||||
|
|
||||||
|
^\s+[A-Za-z][A-Za-z0-9]*:\s*(.*)$
|
||||||
|
|
||||||
|
Required keys:
|
||||||
|
|
||||||
|
* ``Name: <string>``
|
||||||
|
* ``Range: <YYYY-MM-DD>..<YYYY-MM-DD>``
|
||||||
|
|
||||||
|
The ``Range`` defines a closed interval ``[start, end]``.
|
||||||
|
|
||||||
|
|
||||||
|
Part: ``application/issue``
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Each issue is represented as a structured block with strict field ordering.
|
||||||
|
|
||||||
|
Required Field Order
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The following fields MUST appear in exactly this order::
|
||||||
|
|
||||||
|
ID: <integer>
|
||||||
|
Type: <feature|bugfix|hotfix>
|
||||||
|
Title: <short title>
|
||||||
|
Status: <open|in-progress|done|hold|cancelled>
|
||||||
|
Priority: <low|medium|high>
|
||||||
|
Created: <YYYY-MM-DD>
|
||||||
|
Relationships: <comma-separated list or empty>
|
||||||
|
DueStart: <YYYY-MM-DD> # OPTIONAL
|
||||||
|
DueEnd: <YYYY-MM-DD> # OPTIONAL
|
||||||
|
Description: <first line>
|
||||||
|
<continuation lines>
|
||||||
|
|
||||||
|
Field Semantics
|
||||||
|
---------------
|
||||||
|
|
||||||
|
* **ID**: unique integer, strictly increasing.
|
||||||
|
* **Type**: one of ``feature``, ``bugfix``, ``hotfix``.
|
||||||
|
* **Status**: one of ``open``, ``in-progress``, ``done``, ``hold``, ``cancelled``.
|
||||||
|
* **Priority**: one of ``low``, ``medium``, ``high``.
|
||||||
|
* **Created**: ISO date.
|
||||||
|
* **Relationships**:
|
||||||
|
* Empty::
|
||||||
|
|
||||||
|
Relationships:
|
||||||
|
|
||||||
|
* Or list::
|
||||||
|
|
||||||
|
Relationships: dependsOn:43, relatesTo:10
|
||||||
|
|
||||||
|
* Valid kinds: ``dependsOn``, ``relatesTo``, ``blocks``.
|
||||||
|
* **DueStart / DueEnd**:
|
||||||
|
* Optional.
|
||||||
|
* If only one is present, the other is implicitly equal to it.
|
||||||
|
|
||||||
|
Description Block
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
* The first line appears on the same line as ``Description:``.
|
||||||
|
* Continuation lines:
|
||||||
|
* Must begin with whitespace.
|
||||||
|
* Must align indentation consistently.
|
||||||
|
* No blank lines allowed inside the description block.
|
||||||
|
|
||||||
|
Body
|
||||||
|
----
|
||||||
|
|
||||||
|
Any lines after the description block are considered free-form body text.
|
||||||
|
The body MUST NOT contain another ``ID:`` field or a MIME boundary.
|
||||||
|
|
||||||
|
|
||||||
|
Git Workflow Rules
|
||||||
|
==================
|
||||||
|
|
||||||
|
Branch Naming
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Branches for issues MUST follow::
|
||||||
|
|
||||||
|
<Type>/<ID>
|
||||||
|
|
||||||
|
Examples::
|
||||||
|
|
||||||
|
feature/12
|
||||||
|
bugfix/7
|
||||||
|
|
||||||
|
Modification Rules
|
||||||
|
------------------
|
||||||
|
|
||||||
|
* All edits to ``TODO`` MUST occur on ``develop``.
|
||||||
|
* Feature/bugfix/hotfix branches MUST rebase changes from ``develop``.
|
||||||
|
|
||||||
|
Branch Lifecycle
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* A branch MUST NOT exist before its issue is created.
|
||||||
|
* A branch MUST be named exactly ``<Type>/<ID>``.
|
||||||
|
* A branch MAY merge only when the issue status is ``done``.
|
||||||
|
|
||||||
|
|
||||||
|
Sprint Membership Logic
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Given:
|
||||||
|
|
||||||
|
* Sprint interval ``[S_start, S_end]``
|
||||||
|
* Issue interval ``[I_start, I_end]``
|
||||||
|
|
||||||
|
Normalization
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* Only ``DueEnd`` → ``I_start = I_end = DueEnd``
|
||||||
|
* Only ``DueStart`` → ``I_start = I_end = DueStart``
|
||||||
|
* Neither → issue is not sprint-bound
|
||||||
|
|
||||||
|
Membership Condition
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
An issue belongs to a sprint if::
|
||||||
|
|
||||||
|
I_start ≤ S_end AND I_end ≥ S_start
|
||||||
|
|
||||||
|
Current Sprint for Date D
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
1. All sprints where ``S_start ≤ D ≤ S_end``.
|
||||||
|
2. If multiple match, choose the one with the latest ``S_start``.
|
||||||
|
3. If none match, no active sprint.
|
||||||
|
|
||||||
|
|
||||||
|
Preprocessor Requirements
|
||||||
|
=========================
|
||||||
|
|
||||||
|
The preprocessor MUST:
|
||||||
|
|
||||||
|
* Read the raw ``TODO`` file.
|
||||||
|
* Split into parts using ``--ISSUE``.
|
||||||
|
* Extract each part's ``Content-Type`` and body.
|
||||||
|
* Reorder parts so that:
|
||||||
|
* ``application/sprints`` appears first (if present)
|
||||||
|
* All ``application/issue`` parts follow in original order
|
||||||
|
* Emit a MIME message with:
|
||||||
|
* ``MIME-Version: 1.0``
|
||||||
|
* ``Content-Type: multipart/mixed; boundary="ISSUE"``
|
||||||
|
* Each part wrapped as::
|
||||||
|
|
||||||
|
--ISSUE
|
||||||
|
Content-Type: <type>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
* Final boundary::
|
||||||
|
|
||||||
|
--ISSUE--
|
||||||
|
|
||||||
|
|
||||||
|
Parser Requirements
|
||||||
|
===================
|
||||||
|
|
||||||
|
The parser MUST:
|
||||||
|
|
||||||
|
* Use a MIME parser to extract parts.
|
||||||
|
* Dispatch based on ``Content-Type``:
|
||||||
|
* ``application/sprints`` → sprint parser
|
||||||
|
* ``application/issue`` → issue parser
|
||||||
|
* Enforce:
|
||||||
|
* Field order
|
||||||
|
* Required fields
|
||||||
|
* Description indentation rules
|
||||||
|
* Sprint entry grammar
|
||||||
|
* Produce a ``TodoFile`` object::
|
||||||
|
|
||||||
|
{
|
||||||
|
sprints: Sprint[],
|
||||||
|
issues: Issue[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Error Handling
|
||||||
|
==============
|
||||||
|
|
||||||
|
The parser MUST reject:
|
||||||
|
|
||||||
|
* Missing or malformed ``Content-Type`` headers
|
||||||
|
* Unknown MIME types
|
||||||
|
* Missing required issue fields
|
||||||
|
* Incorrect field order
|
||||||
|
* Invalid sprint ranges
|
||||||
|
* Invalid date formats
|
||||||
|
* Multiple ``application/sprints`` parts
|
||||||
|
* Duplicate issue IDs
|
||||||
|
|
||||||
|
Errors SHOULD include line numbers when possible.
|
||||||
|
|
||||||
|
|
||||||
|
Extensibility
|
||||||
|
=============
|
||||||
|
|
||||||
|
Future part types MAY be added using::
|
||||||
|
|
||||||
|
application/<subtype>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
* ``application/metadata``
|
||||||
|
* ``application/changelog``
|
||||||
|
* ``application/epilog``
|
||||||
|
|
||||||
|
The preprocessor MUST preserve unknown part types but MUST NOT reorder them.
|
||||||
|
|
||||||
|
Always determine first, whether the issue must immediately resolve on the
|
||||||
|
production branch, or whether it can be resolved by introducing it through the
|
||||||
|
development branch.
|
||||||
|
|
||||||
|
Ensure to switch to the branch impacted by the issue.
|
||||||
|
|
||||||
|
Add a new issue to the TODO file.
|
||||||
|
|
||||||
|
Issues first applied to the development branch
|
||||||
|
MUST either be of type ``feature``, or ``bugfix``. Issues applied to the
|
||||||
|
production branch MUST be of type ``hotfix``.
|
||||||
|
|
||||||
|
Define a
|
||||||
62
src/conf.py
Normal file
62
src/conf.py
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import datetime
|
||||||
|
from sphinx.util import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.todo',
|
||||||
|
'sphinx_last_updated_by_git',
|
||||||
|
'sphinx_markdown_builder',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
templates_path = ["_templates"]
|
||||||
|
|
||||||
|
project = "MIME TODO"
|
||||||
|
copyright = "2026, Tiara Rodney"
|
||||||
|
|
||||||
|
html_title = project
|
||||||
|
html_theme = 'bizstyle'
|
||||||
|
html_sidebars = {}
|
||||||
|
html_show_sphinx = False
|
||||||
|
html_show_sourcelink = False
|
||||||
|
|
||||||
|
html_context = {
|
||||||
|
"bitbucket_url": "https://bitbucket.org",
|
||||||
|
"bitbucket_user": "byteb4rb1e",
|
||||||
|
"bitbucket_repo": "ai-mime-todo",
|
||||||
|
"bitbucket_version": "master",
|
||||||
|
"doc_path": "src/"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
language = 'en'
|
||||||
|
|
||||||
|
todo_include_todos = True
|
||||||
|
|
||||||
|
rst_prolog = f"""
|
||||||
|
.. |build-time| replace:: {datetime.datetime.now().strftime("%d %B %Y, %H:%M")}
|
||||||
|
"""
|
||||||
|
|
||||||
|
html_last_updated_fmt = "%d %B %Y, %H:%M"
|
||||||
|
|
||||||
|
root_doc = "README"
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
def on_build_finished(app, exception):
|
||||||
|
if exception is not None:
|
||||||
|
return # skip on build failure
|
||||||
|
|
||||||
|
ilicense = Path(app.confdir) / '..' / 'LICENSE'
|
||||||
|
olicense = Path(app.outdir) / 'LICENSE'
|
||||||
|
|
||||||
|
if ilicense.exists():
|
||||||
|
shutil.copyfile(ilicense, olicense)
|
||||||
|
logger.info(f"copied {ilicense} → {olicense}")
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.connect("build-finished", on_build_finished)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue