diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0a96006 --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +# From https://github.com/4teamwork/ftw-buildouts/blob/master/pycodestyle.cfg +ignore = E121,E122,E123,E125,E126,E127,E128,E203,E301,W503,W606 +max-line-length = 125 +exclude = .git,__pycache__,venv,.tox,manage.py,include,lib,javascript,lib,node_modules,scss,bin,.eggs,wsgi.py,wsgi.py,src,migrations,docs,gever,bootstrap.py diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4ace419 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,15 @@ +## Description + + + + +Belongs to PBI [TI-xxx]. + +## Checklist + +The following checklist should help us to stick to our "definition of done": + +- [ ] Proposed changes include tests. +- [ ] Good error handling with useful messages +- [ ] Changelog added +- [ ] Readme has been updated if changes interfere with the setup process. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8e7f444 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,33 @@ +name: Linting + +on: + - push + +concurrency: + group: ${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: self-hosted + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: 3.13 + - name: Install poetry + uses: abatilo/actions-poetry@v2 + with: + poetry-version: 2.1 + - name: Install dependencies + run: poetry install + - name: isort + run: poetry run isort --check-only --quiet --settings pyproject.toml . + - name: flake8 + run: poetry run flake8 + - name: black + run: poetry run black --check --config pyproject.toml . + - name: tests + run: poetry run pytest diff --git a/HISTORY.txt b/CHANGELOG.md similarity index 67% rename from HISTORY.txt rename to CHANGELOG.md index 87aea06..bdc2168 100644 --- a/HISTORY.txt +++ b/CHANGELOG.md @@ -2,12 +2,6 @@ Changelog ========= -1.4.1 (unreleased) ------------------- - -- Nothing changed yet. - - 1.4.0 (2022-12-14) ------------------ @@ -74,10 +68,8 @@ Changelog 1.1.2 (2020-06-11) ------------------ -- Handle embedded images that also have an external reference. - [buchi] -- Fix renumbering of non-visual image and drawing properties. - [buchi] +- Handle embedded images that also have an external reference. [buchi] +- Fix renumbering of non-visual image and drawing properties. [buchi] 1.1.1 (2020-05-04) @@ -108,19 +100,15 @@ Changelog 1.0.0 (2019-06-13) ------------------ -- Change license from GPL to MIT. - [buchi] +- Change license from GPL to MIT. [buchi] -- Add support for adding, setting and deleting of doc properties. - [buchi] +- Add support for adding, setting and deleting of doc properties. [buchi] 1.0.0a17 (2019-04-25) --------------------- -- Add functionality to get and set content of plain text content controls - (structured document tags). - [buchi] +- Add functionality to get and set content of plain text content controls (structured document tags). [buchi] 1.0.0a16 (2019-01-15) @@ -139,150 +127,103 @@ Changelog 1.0.0a14 (2018-12-04) --------------------- -- Implement generic handling of referenced parts. Among other, this adds - support for embedded Excel charts. - [buchi] - -- Handle embedded SVGs. - [buchi] - -- Add styles from other parts, e.g. footnotes. - [buchi] +- Implement generic handling of referenced parts. Among other, this adds support for embedded Excel charts. [buchi] +- Handle embedded SVGs. [buchi] +- Add styles from other parts, e.g. footnotes. [buchi] 1.0.0a13 (2018-11-05) --------------------- -- Fix list-styles being set incorrectly when restarting numberings. - [deiferni] +- Fix list-styles being set incorrectly when restarting numberings. [deiferni] 1.0.0a12 (2018-10-30) --------------------- -- Fix setting section type for appended documents with only one section. - [deiferni] +- Fix setting section type for appended documents with only one section. [deiferni] 1.0.0a11 (2018-07-30) --------------------- -- Fix handling of section type. - [buchi] - -- Fix an issue where the listing style of the first element was different. - [deiferni] - -- Fix issue when restarting intermittent numbering. - [deiferni] +- Fix handling of section type. [buchi] +- Fix an issue where the listing style of the first element was different. [deiferni] +- Fix issue when restarting intermittent numbering. [deiferni] 1.0.0a10 (2018-07-18) --------------------- -- Add console script command to compose two or more word files. - [deiferni] +- Add console script command to compose two or more word files. [deiferni] 1.0.0a9 (2018-05-01) -------------------- -- Fix error in mapping of num_ids introduced in 1.0.0.a7. - [buchi] - -- Do not fail when numbering zero is referenced. - [deiferni] +- Fix error in mapping of num_ids introduced in 1.0.0.a7. [buchi] +- Do not fail when numbering zero is referenced. [deiferni] 1.0.0a8 (2018-04-26) -------------------- -- Only attempt to set the nsid when it is available. - [deiferni] +- Only attempt to set the nsid when it is available. [deiferni] 1.0.0a7 (2018-04-20) -------------------- -- Fix handling of images in WordprocessingGroups (). - [buchi] - -- Fix handling of shapes in shape groups (). - [buchi] - -- Fix handling of numberings, avoid inserting multiple numbering properties. - [buchi] - -- Fix renumbering of bookmarks. - [buchi] - -- Renumber ids of drawing object properties (). - [buchi] +- Fix handling of images in WordprocessingGroups (). [buchi] +- Fix handling of shapes in shape groups (). [buchi] +- Fix handling of numberings, avoid inserting multiple numbering properties. [buchi] +- Fix renumbering of bookmarks. [buchi] +- Renumber ids of drawing object properties (). [buchi] 1.0.0a6 (2018-02-20) -------------------- -- Do not restart numbering of bullets. - [buchi] +- Do not restart numbering of bullets. [buchi] 1.0.0a5 (2018-01-11) -------------------- -- Renumber bookmarks to avoid duplicate ids. - [buchi] - -- Add support for shapes. - [buchi] +- Renumber bookmarks to avoid duplicate ids. [buchi] +- Add support for shapes. [buchi] 1.0.0a4 (2017-12-27) -------------------- -- Fix handling of styles when composing documents with different languages. - [buchi] - -- Also add numberings referenced in styles. - [buchi] - -- Avoid having multiple elements for the same style. - [buchi] - -- Restart first numbering of inserted documents - [buchi] - -- Add support for anchored images. - [buchi] - -- Handle referenced style ids that are not defined in styles.xml - [buchi] - -- Remove header and footer references in paragraph properties. - [buchi] +- Fix handling of styles when composing documents with different languages. [buchi] +- Also add numberings referenced in styles. [buchi] +- Avoid having multiple elements for the same style. [buchi] +- Restart first numbering of inserted documents [buchi] +- Add support for anchored images. [buchi] +- Handle referenced style ids that are not defined in styles.xml [buchi] +- Remove header and footer references in paragraph properties. [buchi] 1.0.0a3 (2017-11-22) -------------------- -- Make removal of property fields optional. - [buchi] +- Make removal of property fields optional. [buchi] 1.0.0a2 (2017-11-06) --------------------- - -- Fix handling of footnotes containing hyperlinks. - [buchi] +--------------------- +- Fix handling of footnotes containing hyperlinks. [buchi] - Add functionality to deal with custom document properties. Properties can be updated and fields containing properties can be removed. When appending or - inserting documents their custom document properties get removed automatically. - [buchi] + inserting documents their custom document properties get removed automatically. [buchi] 1.0.0a1 (2017-09-13) --------------------- +--------------------- + +New features: -- Initial release - [buchi] +- Initial release [buchi] diff --git a/MANIFEST.in b/MANIFEST.in index 7d23e42..02e4a93 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,2 @@ -graft docxcompose -prune tests -include *.rst -include *.txt -exclude .gitignore -global-exclude *.pyc +# We don't release this package. This dummy MANIFEST.in stops zest.releaser +# from complaining. diff --git a/README.rst b/README.rst index 6ba0d20..244c85d 100644 --- a/README.rst +++ b/README.rst @@ -34,11 +34,11 @@ line, e.g.: Installation for development ---------------------------- -To install docxcompose for development, clone the repository and using a python with setuptools (for example a fresh virtualenv), install it using pip: +To install docxcompose for development, clone the repository and using a python with poetry: .. code:: sh - $ pip install -e .[tests] + $ poetry install Tests can then be run with ``pytest``. diff --git a/bin/.gitignore b/bin/.gitignore index 1a64e0a..e44f032 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -1,3 +1,3 @@ * !.gitignore -!cibuild +!release diff --git a/bin/cibuild b/bin/cibuild deleted file mode 100755 index 1648cc8..0000000 --- a/bin/cibuild +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env sh -set -euo pipefail - -tox - -exit $? diff --git a/bin/release b/bin/release new file mode 100755 index 0000000..07ca5dc --- /dev/null +++ b/bin/release @@ -0,0 +1,7 @@ +#!/usr/bin/env sh + +set -euo pipefail + +git pull +poetry run fullrelease +poetry publish --build diff --git a/changes/.gitignore b/changes/.gitignore new file mode 100644 index 0000000..f935021 --- /dev/null +++ b/changes/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/changes/fix_pkg_resources.bugfix b/changes/fix_pkg_resources.bugfix new file mode 100644 index 0000000..a342628 --- /dev/null +++ b/changes/fix_pkg_resources.bugfix @@ -0,0 +1 @@ +Fix deprecated pkg_resources after it was removed from setuptools. [jch] diff --git a/changes/project_setup.other b/changes/project_setup.other new file mode 100644 index 0000000..dbae3c8 --- /dev/null +++ b/changes/project_setup.other @@ -0,0 +1 @@ +Use poetry for dependecy management and towncrier and zest.releaser for release managment. [jch] diff --git a/docxcompose/command.py b/docxcompose/command.py index 78f4a9a..3b57a84 100644 --- a/docxcompose/command.py +++ b/docxcompose/command.py @@ -1,29 +1,38 @@ +import os.path +import sys from argparse import ArgumentParser + from docx import Document + from docxcompose.composer import Composer -import os.path -import sys def setup_parser(): - parser = ArgumentParser( - description='compose multiple docx files into one file.') - parser.add_argument('master', - help='path to master template that defines styles, ' - 'headings and so on') - parser.add_argument('files', nargs='+', - help='path to one or more word-files to be appended ' - 'to the master template', - metavar='file') - parser.add_argument('-o', '--output-document', dest='ouput_document', - default='composed.docx', - help='path to the output file', metavar='file') + parser = ArgumentParser(description="compose multiple docx files into one file.") + parser.add_argument( + "master", + help="path to master template that defines styles, " "headings and so on", + ) + parser.add_argument( + "files", + nargs="+", + help="path to one or more word-files to be appended " "to the master template", + metavar="file", + ) + parser.add_argument( + "-o", + "--output-document", + dest="ouput_document", + default="composed.docx", + help="path to the output file", + metavar="file", + ) return parser def require_valid_file(parser, path): if not os.path.isfile(path): - parser.error(message='file not found {}'.format(path)) + parser.error(message="file not found {}".format(path)) def parse_args(parser, args): @@ -42,8 +51,9 @@ def compose_files(parser, parsed_args): composer.append(Document(slave_path)) composer.save(parsed_args.ouput_document) - parser.exit(message='successfully composed file at {}\n'.format( - parsed_args.ouput_document)) + parser.exit( + message="successfully composed file at {}\n".format(parsed_args.ouput_document) + ) def main(args=None): diff --git a/docxcompose/composer.py b/docxcompose/composer.py index ae530a5..64c0174 100644 --- a/docxcompose/composer.py +++ b/docxcompose/composer.py @@ -1,5 +1,9 @@ +import os.path +import random +import re from collections import OrderedDict from copy import deepcopy + from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.oxml import serialize_part_xml @@ -8,22 +12,23 @@ from docx.oxml import parse_xml from docx.oxml.section import CT_SectPr from docx.parts.numbering import NumberingPart + from docxcompose.image import ImageWrapper from docxcompose.properties import CustomProperties from docxcompose.utils import NS from docxcompose.utils import xpath -import os.path -import random -import re -FILENAME_IDX_RE = re.compile('([a-zA-Z/_-]+)([1-9][0-9]*)?') -RID_IDX_RE = re.compile('rId([0-9]*)') -REFERENCED_PARTS_IGNORED_RELTYPES = set([ - RT.IMAGE, - RT.HEADER, - RT.FOOTER, -]) +FILENAME_IDX_RE = re.compile("([a-zA-Z/_-]+)([1-9][0-9]*)?") +RID_IDX_RE = re.compile("rId([0-9]*)") + +REFERENCED_PARTS_IGNORED_RELTYPES = set( + [ + RT.IMAGE, + RT.HEADER, + RT.FOOTER, + ] +) PART_RELTYPES_WITH_STYLES = [ RT.FOOTNOTES, @@ -111,26 +116,27 @@ def save(self, filename): self.doc.save(filename) def append_index(self): - section_props = self.doc.element.body.xpath('w:sectPr') + section_props = self.doc.element.body.xpath("w:sectPr") if section_props: return self.doc.element.body.index(section_props[0]) return len(self.doc.element.body) def add_referenced_parts(self, src_part, dst_part, element): - rid_elements = xpath(element, './/*[@r:id]') + rid_elements = xpath(element, ".//*[@r:id]") for rid_element in rid_elements: - rid = rid_element.get('{%s}id' % NS['r']) + rid = rid_element.get("{%s}id" % NS["r"]) rel = src_part.rels[rid] if rel.reltype in REFERENCED_PARTS_IGNORED_RELTYPES: continue new_rel = self.add_relationship(src_part, dst_part, rel) - rid_element.set('{%s}id' % NS['r'], new_rel.rId) + rid_element.set("{%s}id" % NS["r"], new_rel.rId) def add_relationship(self, src_part, dst_part, relationship): """Add relationship and it's target part""" if relationship.is_external: new_rid = dst_part.rels.get_or_add_ext_rel( - relationship.reltype, relationship.target_ref) + relationship.reltype, relationship.target_ref + ) return dst_part.rels[new_rid] part = relationship.target_part @@ -142,19 +148,15 @@ def add_relationship(self, src_part, dst_part, relationship): for p in dst_part.package.iter_parts() if p.partname.startswith(name) ] - used_part_numbers = [ - int(idx) for idx in used_part_numbers if idx is not None] + used_part_numbers = [int(idx) for idx in used_part_numbers if idx is not None] - for n in range(1, len(used_part_numbers)+2): + for n in range(1, len(used_part_numbers) + 2): if n not in used_part_numbers: next_part_number = n break - next_partname = PackURI('%s%d.%s' % ( - name, next_part_number, part.partname.ext)) + next_partname = PackURI("%s%d.%s" % (name, next_part_number, part.partname.ext)) - new_part = Part( - next_partname, part.content_type, part.blob, - dst_part.package) + new_part = Part(next_partname, part.content_type, part.blob, dst_part.package) new_rel = dst_part.rels.get_or_add(relationship.reltype, new_part) # Sort relationships by rId to get the same rId when adding them to the @@ -169,25 +171,24 @@ def sort_key(r): return new_rel def add_diagrams(self, doc, element): - dgm_rels = xpath(element, './/dgm:relIds[@r:dm]') + dgm_rels = xpath(element, ".//dgm:relIds[@r:dm]") for dgm_rel in dgm_rels: for item, rt_type in ( - ('dm', RT.DIAGRAM_DATA), - ('lo', RT.DIAGRAM_LAYOUT), - ('qs', RT.DIAGRAM_QUICK_STYLE), - ('cs', RT.DIAGRAM_COLORS) + ("dm", RT.DIAGRAM_DATA), + ("lo", RT.DIAGRAM_LAYOUT), + ("qs", RT.DIAGRAM_QUICK_STYLE), + ("cs", RT.DIAGRAM_COLORS), ): - dm_rid = dgm_rel.get('{%s}%s' % (NS['r'], item)) + dm_rid = dgm_rel.get("{%s}%s" % (NS["r"], item)) dm_part = doc.part.rels[dm_rid].target_part new_rid = self.doc.part.relate_to(dm_part, rt_type) - dgm_rel.set('{%s}%s' % (NS['r'], item), new_rid) + dgm_rel.set("{%s}%s" % (NS["r"], item), new_rid) def add_images(self, doc, element): """Add images from the given document used in the given element.""" - blips = xpath( - element, '(.//a:blip|.//asvg:svgBlip)[@r:embed]') + blips = xpath(element, "(.//a:blip|.//asvg:svgBlip)[@r:embed]") for blip in blips: - rid = blip.get('{%s}embed' % NS['r']) + rid = blip.get("{%s}embed" % NS["r"]) img_part = doc.part.rels[rid].target_part new_img_part = self.pkg.image_parts._get_by_sha1(img_part.sha1) @@ -196,20 +197,20 @@ def add_images(self, doc, element): new_img_part = self.pkg.image_parts._add_image_part(image) new_rid = self.doc.part.relate_to(new_img_part, RT.IMAGE) - blip.set('{%s}embed' % NS['r'], new_rid) + blip.set("{%s}embed" % NS["r"], new_rid) # handle external reference as images can be embedded and have an # external reference - rid = blip.get('{%s}link' % NS['r']) + rid = blip.get("{%s}link" % NS["r"]) if rid: rel = doc.part.rels[rid] new_rel = self.add_relationship(None, self.doc.part, rel) - blip.set('{%s}link' % NS['r'], new_rel.rId) + blip.set("{%s}link" % NS["r"], new_rel.rId) def add_shapes(self, doc, element): - shapes = xpath(element, './/v:shape/v:imagedata') + shapes = xpath(element, ".//v:shape/v:imagedata") for shape in shapes: - rid = shape.get('{%s}id' % NS['r']) + rid = shape.get("{%s}id" % NS["r"]) img_part = doc.part.rels[rid].target_part new_img_part = self.pkg.image_parts._get_by_sha1(img_part.sha1) @@ -218,11 +219,11 @@ def add_shapes(self, doc, element): new_img_part = self.pkg.image_parts._add_image_part(image) new_rid = self.doc.part.relate_to(new_img_part, RT.IMAGE) - shape.set('{%s}id' % NS['r'], new_rid) + shape.set("{%s}id" % NS["r"], new_rid) def add_footnotes(self, doc, element): """Add footnotes from the given document used in the given element.""" - footnotes_refs = element.findall('.//w:footnoteReference', NS) + footnotes_refs = element.findall(".//w:footnoteReference", NS) if not footnotes_refs: return @@ -235,12 +236,12 @@ def add_footnotes(self, doc, element): next_id = len(footnotes) + 1 for ref in footnotes_refs: - id_ = ref.get('{%s}id' % NS['w']) + id_ = ref.get("{%s}id" % NS["w"]) element = parse_xml(footnote_part.blob) footnote = deepcopy(element.find('.//w:footnote[@w:id="%s"]' % id_, NS)) footnotes.append(footnote) - footnote.set('{%s}id' % NS['w'], str(next_id)) - ref.set('{%s}id' % NS['w'], str(next_id)) + footnote.set("{%s}id" % NS["w"], str(next_id)) + ref.set("{%s}id" % NS["w"], str(next_id)) next_id += 1 self.add_referenced_parts(footnote_part, my_footnote_part, element) @@ -253,22 +254,23 @@ def footnote_part(self): footnote_part = self.doc.part.rels.part_with_reltype(RT.FOOTNOTES) except KeyError: # Create a new empty footnotes part - partname = PackURI('/word/footnotes.xml') + partname = PackURI("/word/footnotes.xml") content_type = CT.WML_FOOTNOTES xml_path = os.path.join( - os.path.dirname(__file__), 'templates', 'footnotes.xml') - with open(xml_path, 'rb') as f: + os.path.dirname(__file__), "templates", "footnotes.xml" + ) + with open(xml_path, "rb") as f: xml_bytes = f.read() footnote_part = Part( - partname, content_type, xml_bytes, self.doc.part.package) + partname, content_type, xml_bytes, self.doc.part.package + ) self.doc.part.relate_to(footnote_part, RT.FOOTNOTES) return footnote_part def mapped_style_id(self, style_id): if style_id not in self._style_id2name: return style_id - return self._style_name2id.get( - self._style_id2name[style_id], style_id) + return self._style_name2id.get(self._style_id2name[style_id], style_id) def _create_style_id_mapping(self, doc): # Style ids are language-specific, but names not (always), WTF? @@ -290,8 +292,11 @@ def add_styles(self, doc, element): """Add styles from the given document used in the given element.""" our_style_ids = [s.style_id for s in self.doc.styles] # de-duplicate ids and keep order to make sure tests are not flaky - used_style_ids = list(OrderedDict.fromkeys([e.val for e in xpath( - element, './/w:tblStyle|.//w:pStyle|.//w:rStyle')])) + used_style_ids = list( + OrderedDict.fromkeys( + [e.val for e in xpath(element, ".//w:tblStyle|.//w:pStyle|.//w:rStyle")] + ) + ) for style_id in used_style_ids: our_style_id = self.mapped_style_id(style_id) @@ -301,37 +306,47 @@ def add_styles(self, doc, element): self.doc.styles.element.append(style_element) self.add_numberings(doc, style_element) # Also add linked styles - linked_style_ids = xpath(style_element, './/w:link/@w:val') + linked_style_ids = xpath(style_element, ".//w:link/@w:val") if linked_style_ids: linked_style_id = linked_style_ids[0] our_linked_style_id = self.mapped_style_id(linked_style_id) if our_linked_style_id not in our_style_ids: our_linked_style = doc.styles.element.get_by_id( - linked_style_id) + linked_style_id + ) if our_linked_style is not None: - self.doc.styles.element.append(deepcopy( - our_linked_style)) + self.doc.styles.element.append( + deepcopy(our_linked_style) + ) else: # Create a mapping for abstractNumIds used in existing styles # This is used when adding numberings to avoid having multiple # elements for the same style. style_element = doc.styles.element.get_by_id(style_id) if style_element is not None: - num_ids = xpath(style_element, './/w:numId/@w:val') + num_ids = xpath(style_element, ".//w:numId/@w:val") if num_ids: anum_ids = xpath( doc.part.numbering_part.element, - './/w:num[@w:numId="%s"]/w:abstractNumId/@w:val' % num_ids[0]) + './/w:num[@w:numId="%s"]/w:abstractNumId/@w:val' + % num_ids[0], + ) if anum_ids: - our_style_element = self.doc.styles.element.get_by_id(our_style_id) - our_num_ids = xpath(our_style_element, './/w:numId/@w:val') + our_style_element = self.doc.styles.element.get_by_id( + our_style_id + ) + our_num_ids = xpath(our_style_element, ".//w:numId/@w:val") if our_num_ids: numbering_part = self.numbering_part() our_anum_ids = xpath( numbering_part.element, - './/w:num[@w:numId="%s"]/w:abstractNumId/@w:val' % our_num_ids[0]) + './/w:num[@w:numId="%s"]/w:abstractNumId/@w:val' + % our_num_ids[0], + ) if our_anum_ids: - self.anum_id_mapping[int(anum_ids[0])] = int(our_anum_ids[0]) + self.anum_id_mapping[int(anum_ids[0])] = int( + our_anum_ids[0] + ) # Replace language-specific style id with our style id if our_style_id != style_id and our_style_id is not None: @@ -339,7 +354,8 @@ def add_styles(self, doc, element): element, './/w:tblStyle[@w:val="%(styleid)s"]|' './/w:pStyle[@w:val="%(styleid)s"]|' - './/w:rStyle[@w:val="%(styleid)s"]' % dict(styleid=style_id)) + './/w:rStyle[@w:val="%(styleid)s"]' % dict(styleid=style_id), + ) for el in style_elements: el.val = our_style_id # Update our style ids @@ -348,7 +364,7 @@ def add_styles(self, doc, element): def add_numberings(self, doc, element): """Add numberings from the given document used in the given element.""" # Search for numbering references - num_ids = set([n.val for n in xpath(element, './/w:numId')]) + num_ids = set([n.val for n in xpath(element, ".//w:numId")]) if not num_ids: return @@ -361,8 +377,7 @@ def add_numberings(self, doc, element): continue # Find the referenced element - res = src_numbering_part.element.xpath( - './/w:num[@w:numId="%s"]' % num_id) + res = src_numbering_part.element.xpath('.//w:num[@w:numId="%s"]' % num_id) if not res: continue num_element = deepcopy(res[0]) @@ -370,25 +385,26 @@ def add_numberings(self, doc, element): self.num_id_mapping[num_id] = next_num_id - anum_id = num_element.xpath('//w:abstractNumId')[0] + anum_id = num_element.xpath("//w:abstractNumId")[0] if anum_id.val not in self.anum_id_mapping: # Find the referenced element res = src_numbering_part.element.xpath( - './/w:abstractNum[@w:abstractNumId="%s"]' % anum_id.val) + './/w:abstractNum[@w:abstractNumId="%s"]' % anum_id.val + ) if not res: continue anum_element = deepcopy(res[0]) self.anum_id_mapping[anum_id.val] = next_anum_id anum_id.val = next_anum_id # anum_element.abstractNumId = next_anum_id - anum_element.set('{%s}abstractNumId' % NS['w'], str(next_anum_id)) + anum_element.set("{%s}abstractNumId" % NS["w"], str(next_anum_id)) # Make sure we have a unique nsid so numberings restart properly - nsid = anum_element.find('.//w:nsid', NS) + nsid = anum_element.find(".//w:nsid", NS) if nsid is not None: nsid.set( - '{%s}val' % NS['w'], - "{0:08X}".format(int(10**8 * random.random())) + "{%s}val" % NS["w"], + "{0:08X}".format(int(10**8 * random.random())), ) self._insert_abstract_num(anum_element) @@ -398,16 +414,14 @@ def add_numberings(self, doc, element): self._insert_num(num_element) # Fix references - for num_id_ref in xpath(element, './/w:numId'): - num_id_ref.val = self.num_id_mapping.get( - num_id_ref.val, num_id_ref.val) + for num_id_ref in xpath(element, ".//w:numId"): + num_id_ref.val = self.num_id_mapping.get(num_id_ref.val, num_id_ref.val) def _next_numbering_ids(self): numbering_part = self.numbering_part() # Determine next unused numId (numbering starts with 1) - current_num_ids = [ - n.numId for n in xpath(numbering_part.element, './/w:num')] + current_num_ids = [n.numId for n in xpath(numbering_part.element, ".//w:num")] if current_num_ids: next_num_id = max(current_num_ids) + 1 else: @@ -415,8 +429,9 @@ def _next_numbering_ids(self): # Determine next unused abstractNumId (numbering starts with 0) current_anum_ids = [ - int(n) for n in - xpath(numbering_part.element, './/w:abstractNum/@w:abstractNumId')] + int(n) + for n in xpath(numbering_part.element, ".//w:abstractNum/@w:abstractNumId") + ] if current_anum_ids: next_anum_id = max(current_anum_ids) + 1 else: @@ -427,7 +442,7 @@ def _next_numbering_ids(self): def _insert_num(self, element): # Find position of last element and insert after that numbering_part = self.numbering_part() - nums = numbering_part.element.xpath('.//w:num') + nums = numbering_part.element.xpath(".//w:num") if nums: num_index = numbering_part.element.index(nums[-1]) numbering_part.element.insert(num_index, element) @@ -438,7 +453,7 @@ def _insert_abstract_num(self, element): # Find position of first element # We'll insert before that numbering_part = self.numbering_part() - nums = numbering_part.element.xpath('.//w:num') + nums = numbering_part.element.xpath(".//w:num") if nums: anum_index = numbering_part.element.index(nums[0]) else: @@ -459,22 +474,24 @@ def numbering_part(self): numbering_part = self.doc.part.rels.part_with_reltype(RT.NUMBERING) except KeyError: # Create a new empty numbering part - partname = PackURI('/word/numbering.xml') + partname = PackURI("/word/numbering.xml") content_type = CT.WML_NUMBERING xml_path = os.path.join( - os.path.dirname(__file__), 'templates', 'numbering.xml') - with open(xml_path, 'rb') as f: + os.path.dirname(__file__), "templates", "numbering.xml" + ) + with open(xml_path, "rb") as f: xml_bytes = f.read() element = parse_xml(xml_bytes) numbering_part = NumberingPart( - partname, content_type, element, self.doc.part.package) + partname, content_type, element, self.doc.part.package + ) self.doc.part.relate_to(numbering_part, RT.NUMBERING) return numbering_part def restart_first_numbering(self, doc, element): if not self.restart_numbering: return - style_id = xpath(element, './/w:pStyle/@w:val') + style_id = xpath(element, ".//w:pStyle/@w:val") if not style_id: return style_id = style_id[0] @@ -483,7 +500,7 @@ def restart_first_numbering(self, doc, element): style_element = self.doc.styles.element.get_by_id(style_id) if style_element is None: return - outline_lvl = xpath(style_element, './/w:outlineLvl') + outline_lvl = xpath(style_element, ".//w:outlineLvl") if outline_lvl: # Styles with an outline level are probably headings. # Do not restart numbering of headings @@ -491,45 +508,45 @@ def restart_first_numbering(self, doc, element): # if there is a numId referenced from the paragraph, that numId is # relevant, otherwise fall back to the style's numId - local_num_id = xpath(element, './/w:numPr/w:numId/@w:val') + local_num_id = xpath(element, ".//w:numPr/w:numId/@w:val") if local_num_id: num_id = local_num_id[0] else: - style_num_id = xpath(style_element, './/w:numId/@w:val') + style_num_id = xpath(style_element, ".//w:numId/@w:val") if not style_num_id: return num_id = style_num_id[0] numbering_part = self.numbering_part() - num_element = xpath( - numbering_part.element, - './/w:num[@w:numId="%s"]' % num_id) + num_element = xpath(numbering_part.element, './/w:num[@w:numId="%s"]' % num_id) if not num_element: # Styles with no numbering element should not be processed return - anum_id = xpath(num_element[0], './/w:abstractNumId/@w:val')[0] + anum_id = xpath(num_element[0], ".//w:abstractNumId/@w:val")[0] anum_element = xpath( - numbering_part.element, - './/w:abstractNum[@w:abstractNumId="%s"]' % anum_id) - num_fmt = xpath( - anum_element[0], './/w:lvl[@w:ilvl="0"]/w:numFmt/@w:val') + numbering_part.element, './/w:abstractNum[@w:abstractNumId="%s"]' % anum_id + ) + num_fmt = xpath(anum_element[0], './/w:lvl[@w:ilvl="0"]/w:numFmt/@w:val') # Do not restart numbering of bullets - if num_fmt and num_fmt[0] == 'bullet': + if num_fmt and num_fmt[0] == "bullet": return new_num_element = deepcopy(num_element[0]) lvl_override = parse_xml( '') + ' w:ilvl="0">' + ) new_num_element.append(lvl_override) next_num_id, next_anum_id = self._next_numbering_ids() new_num_element.numId = next_num_id self._insert_num(new_num_element) - paragraph_props = xpath(element, './/w:pPr/w:pStyle[@w:val="%s"]/parent::w:pPr' % style_id) - num_pr = xpath(paragraph_props[0], './/w:numPr') + paragraph_props = xpath( + element, './/w:pPr/w:pStyle[@w:val="%s"]/parent::w:pPr' % style_id + ) + num_pr = xpath(paragraph_props[0], ".//w:numPr") if num_pr: num_pr = num_pr[0] previous_num_id = num_pr.numId.val @@ -538,98 +555,108 @@ def restart_first_numbering(self, doc, element): else: num_pr = parse_xml( '' - '' % next_num_id) + '' % next_num_id + ) paragraph_props[0].append(num_pr) self._numbering_restarted.add(style_id) def header_part(self, content=None): """The header part of the document.""" header_rels = [ - rel for rel in self.doc.part.rels.values() if rel.reltype == RT.HEADER] + rel for rel in self.doc.part.rels.values() if rel.reltype == RT.HEADER + ] next_id = len(header_rels) + 1 # Create a new header part - partname = PackURI('/word/header%s.xml' % next_id) + partname = PackURI("/word/header%s.xml" % next_id) content_type = CT.WML_HEADER if not content: xml_path = os.path.join( - os.path.dirname(__file__), 'templates', 'header.xml') - with open(xml_path, 'rb') as f: + os.path.dirname(__file__), "templates", "header.xml" + ) + with open(xml_path, "rb") as f: content = f.read() - header_part = Part( - partname, content_type, content, self.doc.part.package) + header_part = Part(partname, content_type, content, self.doc.part.package) self.doc.part.relate_to(header_part, RT.HEADER) return header_part def footer_part(self, content=None): """The footer part of the document.""" footer_rels = [ - rel for rel in self.doc.part.rels.values() if rel.reltype == RT.FOOTER] + rel for rel in self.doc.part.rels.values() if rel.reltype == RT.FOOTER + ] next_id = len(footer_rels) + 1 # Create a new header part - partname = PackURI('/word/footer%s.xml' % next_id) + partname = PackURI("/word/footer%s.xml" % next_id) content_type = CT.WML_FOOTER if not content: xml_path = os.path.join( - os.path.dirname(__file__), 'templates', 'footer.xml') - with open(xml_path, 'rb') as f: + os.path.dirname(__file__), "templates", "footer.xml" + ) + with open(xml_path, "rb") as f: content = f.read() - footer_part = Part( - partname, content_type, content, self.doc.part.package) + footer_part = Part(partname, content_type, content, self.doc.part.package) self.doc.part.relate_to(footer_part, RT.FOOTER) return footer_part def remove_header_and_footer_references(self, doc, element): - refs = xpath( - element, './/w:headerReference|.//w:footerReference') + refs = xpath(element, ".//w:headerReference|.//w:footerReference") for ref in refs: ref.getparent().remove(ref) def renumber_bookmarks(self): - bookmarks_start = xpath(self.doc.element.body, './/w:bookmarkStart') + bookmarks_start = xpath(self.doc.element.body, ".//w:bookmarkStart") bookmark_id = 0 for bookmark in bookmarks_start: - bookmark.set('{%s}id' % NS['w'], str(bookmark_id)) + bookmark.set("{%s}id" % NS["w"], str(bookmark_id)) bookmark_id += 1 - bookmarks_end = xpath(self.doc.element.body, './/w:bookmarkEnd') + bookmarks_end = xpath(self.doc.element.body, ".//w:bookmarkEnd") bookmark_id = 0 for bookmark in bookmarks_end: - bookmark.set('{%s}id' % NS['w'], str(bookmark_id)) + bookmark.set("{%s}id" % NS["w"], str(bookmark_id)) bookmark_id += 1 def renumber_docpr_ids(self): # Ensure that non-visual drawing properties have a unique id - doc_prs = xpath( - self.doc.element.body, './/wp:docPr') + doc_prs = xpath(self.doc.element.body, ".//wp:docPr") doc_pr_id = 1 for doc_pr in doc_prs: doc_pr.id = doc_pr_id doc_pr_id += 1 parts = [ - rel.target_part for rel in self.doc.part.rels.values() - if rel.reltype in [RT.HEADER, RT.FOOTER, ] + rel.target_part + for rel in self.doc.part.rels.values() + if rel.reltype + in [ + RT.HEADER, + RT.FOOTER, + ] ] for part in parts: - doc_prs = xpath(part.element, './/wp:docPr') + doc_prs = xpath(part.element, ".//wp:docPr") for doc_pr in doc_prs: doc_pr.id = doc_pr_id doc_pr_id += 1 def renumber_nvpicpr_ids(self): # Ensure that non-visual image properties have a unique id - c_nv_prs = xpath( - self.doc.element.body, './/pic:cNvPr') + c_nv_prs = xpath(self.doc.element.body, ".//pic:cNvPr") c_nv_pr_id = 1 for c_nv_pr in c_nv_prs: c_nv_pr.id = c_nv_pr_id c_nv_pr_id += 1 parts = [ - rel.target_part for rel in self.doc.part.rels.values() - if rel.reltype in [RT.HEADER, RT.FOOTER, ] + rel.target_part + for rel in self.doc.part.rels.values() + if rel.reltype + in [ + RT.HEADER, + RT.FOOTER, + ] ] for part in parts: - c_nv_prs = xpath(part.element, './/pic:cNvPr') + c_nv_prs = xpath(part.element, ".//pic:cNvPr") for c_nv_pr in c_nv_prs: c_nv_pr.id = c_nv_pr_id c_nv_pr_id += 1 @@ -647,7 +674,9 @@ def fix_section_types(self, doc): return first_new_section_idx = len(self.doc.sections) - len(doc.sections) - self.doc.sections[first_new_section_idx].start_type = self.doc.sections[-1].start_type + self.doc.sections[first_new_section_idx].start_type = self.doc.sections[ + -1 + ].start_type self.doc.sections[-1].start_type = doc.sections[-1].start_type def fix_header_and_footers(self, doc): @@ -674,7 +703,7 @@ def fix_header_and_footers(self, doc): last_section = self.doc.sections[-1] first_section = self.doc.sections[first_new_section_idx] - for footer_name in ('footer', 'even_page_footer', 'first_page_footer'): + for footer_name in ("footer", "even_page_footer", "first_page_footer"): footer_main = getattr(last_section, footer_name) if not footer_main._has_definition: continue @@ -682,7 +711,7 @@ def fix_header_and_footers(self, doc): rid = footer_main._sectPr.get_footerReference(footer_main._hdrftr_index).rId footer_sec._sectPr.add_footerReference(footer_main._hdrftr_index, rid) - for header_name in ('header', 'even_page_header', 'first_page_header'): + for header_name in ("header", "even_page_header", "first_page_header"): header_main = getattr(last_section, header_name) if not header_main._has_definition: continue diff --git a/docxcompose/properties.py b/docxcompose/properties.py index aa8cd2d..c3039c4 100644 --- a/docxcompose/properties.py +++ b/docxcompose/properties.py @@ -1,6 +1,9 @@ -from babel.dates import format_datetime +import importlib.resources as importlib_resources +import re from copy import deepcopy from datetime import datetime + +from babel.dates import format_datetime from docx.opc.constants import CONTENT_TYPE as CT from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.opc.oxml import serialize_part_xml @@ -8,75 +11,74 @@ from docx.opc.part import Part from docx.oxml import parse_xml from docx.oxml.coreprops import CT_CoreProperties -from docxcompose.utils import NS -from docxcompose.utils import word_to_python_date_format -from docxcompose.utils import xpath from lxml.etree import FunctionNamespace from lxml.etree import QName from six import binary_type from six import text_type -import pkg_resources -import re + +from docxcompose.utils import NS +from docxcompose.utils import word_to_python_date_format +from docxcompose.utils import xpath -CUSTOM_PROPERTY_FMTID = '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}' +CUSTOM_PROPERTY_FMTID = "{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" CUSTOM_PROPERTY_TYPES = { - 'text': ''.format(NS['vt']), - 'int': ''.format(NS['vt']), - 'bool': ''.format(NS['vt']), - 'datetime': ''.format(NS['vt']), - 'float': ''.format(NS['vt']), + "text": ''.format(NS["vt"]), + "int": ''.format(NS["vt"]), + "bool": ''.format(NS["vt"]), + "datetime": ''.format(NS["vt"]), + "float": ''.format(NS["vt"]), } MIN_PID = 2 # Property IDs have to start with 2 def value2vt(value): if isinstance(value, bool): - el = parse_xml(CUSTOM_PROPERTY_TYPES['bool']) - el.text = 'true' if value else 'false' + el = parse_xml(CUSTOM_PROPERTY_TYPES["bool"]) + el.text = "true" if value else "false" elif isinstance(value, int): - el = parse_xml(CUSTOM_PROPERTY_TYPES['int']) + el = parse_xml(CUSTOM_PROPERTY_TYPES["int"]) el.text = text_type(value) elif isinstance(value, float): - el = parse_xml(CUSTOM_PROPERTY_TYPES['float']) + el = parse_xml(CUSTOM_PROPERTY_TYPES["float"]) el.text = text_type(value) elif isinstance(value, datetime): - el = parse_xml(CUSTOM_PROPERTY_TYPES['datetime']) - el.text = value.strftime('%Y-%m-%dT%H:%M:%SZ') + el = parse_xml(CUSTOM_PROPERTY_TYPES["datetime"]) + el.text = value.strftime("%Y-%m-%dT%H:%M:%SZ") elif isinstance(value, text_type): - el = parse_xml(CUSTOM_PROPERTY_TYPES['text']) + el = parse_xml(CUSTOM_PROPERTY_TYPES["text"]) el.text = value elif isinstance(value, binary_type): - value = value.decode('utf-8') - el = parse_xml(CUSTOM_PROPERTY_TYPES['text']) + value = value.decode("utf-8") + el = parse_xml(CUSTOM_PROPERTY_TYPES["text"]) el.text = value else: - raise TypeError('Unsupported type {}'.format(type(value))) + raise TypeError("Unsupported type {}".format(type(value))) return el def vt2value(element): tag = QName(element).localname - if tag == 'bool': - if element.text.lower() == u'true': + if tag == "bool": + if element.text.lower() == "true": return True else: return False - elif tag in ['i1', 'i2', 'i4', 'int', 'ui1', 'ui2', 'ui4', 'uint']: + elif tag in ["i1", "i2", "i4", "int", "ui1", "ui2", "ui4", "uint"]: return int(element.text) - elif tag in ['r4', 'r8']: + elif tag in ["r4", "r8"]: return float(element.text) - elif tag == 'filetime': + elif tag == "filetime": return CT_CoreProperties._parse_W3CDTF_to_datetime(element.text) - elif tag == 'lpwstr': - return element.text if element.text else u'' + elif tag == "lpwstr": + return element.text if element.text else "" else: return element.text def is_text_property(property): tag = QName(property).localname - return tag in ['bstr', 'lpstr', 'lpwstr'] + return tag in ["bstr", "lpstr", "lpwstr"] ns = FunctionNamespace(None) @@ -84,15 +86,16 @@ def is_text_property(property): # lxml doesn't support XPath 2.0 functions # Thus we implement lower-case() as an extension function -@ns('lower-case') +@ns("lower-case") def lower_case(context, a): return [el.lower() for el in a] class CustomProperties(object): """Custom doc properties stored in ``/docProps/custom.xml``. - Allows updating of doc properties in a document. + Allows updating of doc properties in a document. """ + def __init__(self, doc): self.doc = doc self.part = None @@ -108,16 +111,22 @@ def __init__(self, doc): self._element = parse_xml(part.blob) def _part_template(self): - return pkg_resources.resource_string( - 'docxcompose', 'templates/custom.xml') + return ( + importlib_resources.files("docxcompose") + .joinpath("templates/custom.xml") + .read_bytes() + ) def _update_part(self): if self.part is None: # Create a new part for custom properties - partname = PackURI('/docProps/custom.xml') + partname = PackURI("/docProps/custom.xml") self.part = Part( - partname, CT.OFC_CUSTOM_PROPERTIES, - serialize_part_xml(self._element), self.doc.part.package) + partname, + CT.OFC_CUSTOM_PROPERTIES, + serialize_part_xml(self._element), + self.doc.part.package, + ) self.doc.part.package.relate_to(self.part, RT.CUSTOM_PROPERTIES) self._element = parse_xml(self.part.blob) else: @@ -126,8 +135,8 @@ def _update_part(self): def __getitem__(self, key): """Get the value of a property.""" props = xpath( - self._element, - u'.//cp:property[lower-case(@name)="{}"]'.format(key.lower())) + self._element, './/cp:property[lower-case(@name)="{}"]'.format(key.lower()) + ) if not props: raise KeyError(key) @@ -137,8 +146,8 @@ def __getitem__(self, key): def __setitem__(self, key, value): """Set the value of a property.""" props = xpath( - self._element, - u'.//cp:property[lower-case(@name)="{}"]'.format(key.lower())) + self._element, './/cp:property[lower-case(@name)="{}"]'.format(key.lower()) + ) if not props: self.add(key, value) return @@ -152,8 +161,8 @@ def __setitem__(self, key, value): def __delitem__(self, key): """Delete a property.""" props = xpath( - self._element, - u'.//cp:property[lower-case(@name)="{}"]'.format(key.lower())) + self._element, './/cp:property[lower-case(@name)="{}"]'.format(key.lower()) + ) if not props: raise KeyError(key) @@ -162,7 +171,7 @@ def __delitem__(self, key): # Renumber pids pid = MIN_PID for prop in self._element: - prop.set('pid', text_type(pid)) + prop.set("pid", text_type(pid)) pid += 1 self._update_part() @@ -190,21 +199,21 @@ def nullify(self, key): """ props = xpath( - self._element, - u'.//cp:property[lower-case(@name)="{}"]'.format(key.lower())) + self._element, './/cp:property[lower-case(@name)="{}"]'.format(key.lower()) + ) if not props: raise KeyError(key) if is_text_property(props[0][0]): - self[key] = '' + self[key] = "" else: del self[key] def __contains__(self, item): props = xpath( - self._element, - u'.//cp:property[lower-case(@name)="{}"]'.format(item.lower())) + self._element, './/cp:property[lower-case(@name)="{}"]'.format(item.lower()) + ) if props: return True else: @@ -218,15 +227,15 @@ def get(self, key, default=None): def add(self, name, value): """Add a property.""" - pids = [int(pid) for pid in xpath(self._element, u'.//cp:property/@pid')] + pids = [int(pid) for pid in xpath(self._element, ".//cp:property/@pid")] if pids: pid = max(pids) + 1 else: pid = MIN_PID - prop = parse_xml(''.format(NS['cp'])) - prop.set('fmtid', CUSTOM_PROPERTY_FMTID) - prop.set('name', name) - prop.set('pid', text_type(pid)) + prop = parse_xml(''.format(NS["cp"])) + prop.set("fmtid", CUSTOM_PROPERTY_FMTID) + prop.set("name", name) + prop.set("pid", text_type(pid)) value_el = value2vt(value) prop.append(value_el) self._element.append(prop) @@ -237,22 +246,22 @@ def keys(self): if self._element is None: return [] - props = xpath(self._element, u'.//cp:property') - return [prop.get('name') for prop in props] + props = xpath(self._element, ".//cp:property") + return [prop.get("name") for prop in props] def values(self): if self._element is None: return [] - props = xpath(self._element, u'.//cp:property') + props = xpath(self._element, ".//cp:property") return [vt2value(prop[0]) for prop in props] def items(self): if self._element is None: return [] - props = xpath(self._element, u'.//cp:property') - return [(prop.get('name'), vt2value(prop[0])) for prop in props] + props = xpath(self._element, ".//cp:property") + return [(prop.get("name"), vt2value(prop[0])) for prop in props] def set_properties(self, properties): for name, value in properties.items(): @@ -264,13 +273,14 @@ def find_docprops_in_document(self, name=None): """ docprops = [] for section in self.doc.sections: - all_header_footers = [section.first_page_header, - section.header, - section.even_page_header, - section.first_page_footer, - section.footer, - section.even_page_footer, - ] + all_header_footers = [ + section.first_page_header, + section.header, + section.even_page_header, + section.first_page_footer, + section.footer, + section.even_page_footer, + ] # word seems to keep "hidden" header and footer definitions, so # even though some may have been deactivated via the # "different first page" or "different odd & even pages" checkboxes @@ -281,26 +291,24 @@ def find_docprops_in_document(self, name=None): # method if the header/footer has a definition in xml. for container in all_header_footers: if container._has_definition and not container.is_linked_to_previous: - docprops.extend(self._find_docprops_in( - container.part.element, name=name)) + docprops.extend( + self._find_docprops_in(container.part.element, name=name) + ) - docprops.extend(self._find_docprops_in( - self.doc.element.body, name=name)) + docprops.extend(self._find_docprops_in(self.doc.element.body, name=name)) return docprops def _find_docprops_in(self, element, name=None): # First we search for the simple fields: sfield_nodes = xpath( - element, - u'.//w:fldSimple[contains(@w:instr, \'DOCPROPERTY \')]') + element, ".//w:fldSimple[contains(@w:instr, 'DOCPROPERTY ')]" + ) docprops = [SimpleField(sfield_node) for sfield_node in sfield_nodes] # Now for the complex fields - cfield_nodes = xpath( - element, - u'.//w:instrText[contains(.,\'DOCPROPERTY \')]') + cfield_nodes = xpath(element, ".//w:instrText[contains(.,'DOCPROPERTY ')]") docprops.extend([ComplexField(cfield_node) for cfield_node in cfield_nodes]) @@ -334,11 +342,12 @@ def dissolve_fields(self, name): class FieldBase(object): - """Class used to represent a docproperty field in the document.xml. - """ + """Class used to represent a docproperty field in the document.xml.""" + fieldname_and_format_search_expr = re.compile( r'DOCPROPERTY +"{0,1}([^\\]*?)"{0,1} +(?:\\\@ +"{0,1}([^\\]*?)"{0,1} +){0,1}\\\* MERGEFORMAT', - flags=re.UNICODE) + flags=re.UNICODE, + ) def __init__(self, field_node): self.node = field_node @@ -350,7 +359,7 @@ def __init__(self, field_node): def _format_value(self, value, language=None): if isinstance(value, bool): - return u'Y' if value else u'N' + return "Y" if value else "N" elif isinstance(value, datetime): if language is not None: return format_datetime(value, self.date_format, locale=language) @@ -359,12 +368,11 @@ def _format_value(self, value, language=None): return text_type(value) def update(self, value, language=None): - """ Sets the value of the docproperty in the document - """ + """Sets the value of the docproperty in the document""" raise NotImplementedError() def replace_field_with_value(self): - """ Removes the field from the document, replacing it with + """Removes the field from the document, replacing it with its value. """ raise NotImplementedError() @@ -374,14 +382,15 @@ def _get_fieldname_string(self): def _parse_fieldname_and_format(self): match = self.fieldname_and_format_search_expr.search( - self._get_fieldname_string()) + self._get_fieldname_string() + ) if match is None: return None, None return match.groups() class SimpleField(FieldBase): - """ Represents a simple field, i.e. node in the + """Represents a simple field, i.e. node in the document.xml, its body containing the value of the field. self.node here is the node. """ @@ -392,7 +401,7 @@ def _get_fieldname_string(self): return self.node.attrib[self.attr_name] def update(self, value, language=None): - text = xpath(self.node, './/w:t') + text = xpath(self.node, ".//w:t") if text: text[0].text = self._format_value(value, language=language) @@ -410,15 +419,19 @@ class InvalidComplexField(Exception): class ComplexField(FieldBase): - """ Represents a complex field, i.e. a several nodes delimited by runs + """Represents a complex field, i.e. a several nodes delimited by runs containing and . In these fields, the actual value is stored in nodes that come after a node. """ - XPATH_PRECEDING_BEGINS = "./preceding-sibling::w:r/w:fldChar[@w:fldCharType=\"begin\"]/.." - XPATH_FOLLOWING_ENDS = "./following-sibling::w:r/w:fldChar[@w:fldCharType=\"end\"]/.." - XPATH_FOLLOWING_SEPARATES = "./following-sibling::w:r/w:fldChar[@w:fldCharType=\"separate\"]/.." + XPATH_PRECEDING_BEGINS = ( + './preceding-sibling::w:r/w:fldChar[@w:fldCharType="begin"]/..' + ) + XPATH_FOLLOWING_ENDS = './following-sibling::w:r/w:fldChar[@w:fldCharType="end"]/..' + XPATH_FOLLOWING_SEPARATES = ( + './following-sibling::w:r/w:fldChar[@w:fldCharType="separate"]/..' + ) XPATH_TEXTS = "w:instrText" def __init__(self, field_node): @@ -433,8 +446,11 @@ def _get_fieldname_string(self): separate or end runs """ separate_run = self.get_separate_run() - last = (self.w_p.index(separate_run) if separate_run is not None - else self.w_p.index(self.end_run)) + last = ( + self.w_p.index(separate_run) + if separate_run is not None + else self.w_p.index(self.end_run) + ) runs = [run for run in self._runs if self.w_p.index(run) < last] texts = [] for run in runs: @@ -489,9 +505,11 @@ def get_runs_for_update(self): return [] separate_index = self.w_p.index(separate_run) - return [run for run in self._runs - if self.w_p.index(run) > separate_index and - self.w_p.index(run) < end_index] + return [ + run + for run in self._runs + if self.w_p.index(run) > separate_index and self.w_p.index(run) < end_index + ] def get_runs_to_replace_field_with_value(self): """ @@ -505,12 +523,10 @@ def get_runs_to_replace_field_with_value(self): # meaning we can remove the whole field. if separate_run is None: end_index = self.w_p.index(self.end_run) - runs = [run for run in self._runs - if self.w_p.index(run) < end_index] + runs = [run for run in self._runs if self.w_p.index(run) < end_index] else: separate_index = self.w_p.index(separate_run) - runs = [run for run in self._runs - if self.w_p.index(run) <= separate_index] + runs = [run for run in self._runs if self.w_p.index(run) <= separate_index] runs.insert(0, self.begin_run) runs.append(self.end_run) return runs @@ -520,7 +536,7 @@ def update(self, value, language=None): if runs_after_separate: first_w_r = runs_after_separate[0] - text = xpath(first_w_r, u'.//w:t') + text = xpath(first_w_r, ".//w:t") if text: text[0].text = self._format_value(value, language=language) # remove any additional text-nodes inside the first run. we @@ -539,7 +555,7 @@ def update(self, value, language=None): # removed to avoid duplicating parts of the cached docproperty # value. for run in runs_after_separate[1:]: - text = xpath(run, u'.//w:t') + text = xpath(run, ".//w:t") if text: self.w_p.remove(run) else: @@ -550,8 +566,8 @@ def update(self, value, language=None): # node. # we just swap out the fldCharType from begin to separate. separate_run = deepcopy(self.begin_run) - w_fld_char = xpath(separate_run, 'w:fldChar')[0] - w_fld_char.set('{{{}}}fldCharType'.format(NS['w']), 'separate') + w_fld_char = xpath(separate_run, "w:fldChar")[0] + w_fld_char.set("{{{}}}fldCharType".format(NS["w"]), "separate") # create new run containing the actual docproperty value using # the run as a template. @@ -560,8 +576,8 @@ def update(self, value, language=None): # node. # we drop the fldChar node and insert a text node instead. value_run = deepcopy(self.begin_run) - value_run.remove(xpath(value_run, 'w:fldChar')[0]) - text = parse_xml(''.format(NS['w'])) + value_run.remove(xpath(value_run, "w:fldChar")[0]) + text = parse_xml(''.format(NS["w"])) text.text = self._format_value(value, language=language) value_run.append(text) diff --git a/docxcompose/sdt.py b/docxcompose/sdt.py index f243d93..dd7f7a2 100644 --- a/docxcompose/sdt.py +++ b/docxcompose/sdt.py @@ -1,7 +1,8 @@ -from docxcompose.utils import xpath from lxml.etree import Element from lxml.etree import QName +from docxcompose.utils import xpath + class StructuredDocumentTags(object): """Structured Document Tags (aka Content Controls)""" @@ -13,7 +14,8 @@ def tags_by_alias(self, alias): """Get Structured Document Tags by alias.""" return xpath( self.doc.element.body, - './/w:sdt/w:sdtPr/w:alias[@w:val="%s"]/ancestor::w:sdt' % alias) + './/w:sdt/w:sdtPr/w:alias[@w:val="%s"]/ancestor::w:sdt' % alias, + ) def set_text(self, alias, text): """Set the text content of all Structured Document Tags identified by @@ -27,19 +29,19 @@ def set_text(self, alias, text): tags = self.tags_by_alias(alias) for tag in tags: # Ignore if it's not a plain text SDT - plain_text = xpath(tag, './w:sdtPr/w:text') + plain_text = xpath(tag, "./w:sdtPr/w:text") if not plain_text: continue nsmap = tag.nsmap - is_multiline = bool(plain_text[0].xpath('./@w:multiLine', namespaces=nsmap)) + is_multiline = bool(plain_text[0].xpath("./@w:multiLine", namespaces=nsmap)) - properties = xpath(tag, './w:sdtPr') - content = xpath(tag, './w:sdtContent') + properties = xpath(tag, "./w:sdtPr") + content = xpath(tag, "./w:sdtContent") if not content: continue - run_elements = xpath(content[0], './/w:r') + run_elements = xpath(content[0], ".//w:r") if not run_elements: continue @@ -65,59 +67,56 @@ def set_text(self, alias, text): # in the input value string and create text nodes delimited by # line breaks. if not is_multiline: - text = text.replace('\n', ' ') + text = text.replace("\n", " ") lines = text.splitlines() for i, line in enumerate(lines, start=1): - txt_node = Element(QName(nsmap['w'], "t")) + txt_node = Element(QName(nsmap["w"], "t")) txt_node.text = line first_run.append(txt_node) if i != len(lines): - br = Element(QName(nsmap['w'], "br")) + br = Element(QName(nsmap["w"], "br")) first_run.append(br) def _remove_placeholder(self, properties, content, first_run): - """Remove placeholder marker and style. - """ - showing_placeholder = xpath(properties[0], './w:showingPlcHdr') + """Remove placeholder marker and style.""" + showing_placeholder = xpath(properties[0], "./w:showingPlcHdr") if showing_placeholder: properties[0].remove(showing_placeholder[0]) - run_props = xpath(first_run, './w:rPr') + run_props = xpath(first_run, "./w:rPr") if run_props: first_run.remove(run_props[0]) def _remove_all_runs_except_first(self, run_elements): - """Remove all runs except the first one. - """ + """Remove all runs except the first one.""" for run in run_elements[1:]: run.getparent().remove(run) def _clean_first_run(self, first_run): - """Remove all elements from the first run except run formatting. - """ + """Remove all elements from the first run except run formatting.""" for child in first_run.getchildren(): # Preserve formatting - if QName(child).localname == 'rPr': + if QName(child).localname == "rPr": continue first_run.remove(child) def get_text(self, alias): """Get the text content of the first Structured Document Tag identified - by the given alias. + by the given alias. """ tags = self.tags_by_alias(alias) for tag in tags: # Ignore if it's not a plain text SDT - if not xpath(tag, './w:sdtPr/w:text'): + if not xpath(tag, "./w:sdtPr/w:text"): continue tokens = [] - text_and_brs = xpath(tag, './w:sdtContent//w:r/*[self::w:t or self::w:br]') + text_and_brs = xpath(tag, "./w:sdtContent//w:r/*[self::w:t or self::w:br]") for el in text_and_brs: - if QName(el).localname == 't': + if QName(el).localname == "t": tokens.append(el.text) - elif QName(el).localname == 'br': - tokens.append('\n') + elif QName(el).localname == "br": + tokens.append("\n") - return ''.join(tokens) + return "".join(tokens) diff --git a/docxcompose/utils.py b/docxcompose/utils.py index d0568ff..36be83a 100644 --- a/docxcompose/utils.py +++ b/docxcompose/utils.py @@ -1,28 +1,30 @@ -from docx.oxml.xmlchemy import BaseOxmlElement -from docx.oxml.ns import nsmap import re +from docx.oxml.ns import nsmap +from docx.oxml.xmlchemy import BaseOxmlElement + + NS = { - 'a': 'http://schemas.openxmlformats.org/drawingml/2006/main', - 'asvg': 'http://schemas.microsoft.com/office/drawing/2016/SVG/main', - 'cp': 'http://schemas.openxmlformats.org/officeDocument/2006/custom-properties', - 'o': 'urn:schemas-microsoft-com:office:office', - 'pic': 'http://schemas.openxmlformats.org/drawingml/2006/picture', - 'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', - 'v': 'urn:schemas-microsoft-com:vml', - 'vt': 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes', - 'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', - 'wp': 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing', - 'dgm': 'http://schemas.openxmlformats.org/drawingml/2006/diagram', + "a": "http://schemas.openxmlformats.org/drawingml/2006/main", + "asvg": "http://schemas.microsoft.com/office/drawing/2016/SVG/main", + "cp": "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties", + "o": "urn:schemas-microsoft-com:office:office", + "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "v": "urn:schemas-microsoft-com:vml", + "vt": "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes", + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "dgm": "http://schemas.openxmlformats.org/drawingml/2006/diagram", } nsmap.update(NS) def xpath(element, xpath_str): """Performs an XPath query on the given element and returns all matching - elements. - Works with lxml.etree._Element and with - docx.oxml.xmlchemy.BaseOxmlElement elements. + elements. + Works with lxml.etree._Element and with + docx.oxml.xmlchemy.BaseOxmlElement elements. """ if isinstance(element, BaseOxmlElement): return element.xpath(xpath_str) @@ -33,12 +35,12 @@ def xpath(element, xpath_str): # Format specifications for docproperties can be found at # https://support.microsoft.com/en-us/office/format-field-results-baa61f5a-5636-4f11-ab4f-6c36ae43508c?ui=en-US&rs=en-US&ad=US#ID0EAABAAA=Date-Time_format_switch_(\@) date_format_map = ( - ('Y', 'y'), # Upper or lower case Y are equivalent - ('D', 'd'), - ('dddd', 'EEEE'), - ('ddd', 'E'), - ('am/pm', 'AM/PM'), # Upper or lower case are equivalent - ('AM/PM', 'a') + ("Y", "y"), # Upper or lower case Y are equivalent + ("D", "d"), + ("dddd", "EEEE"), + ("ddd", "E"), + ("am/pm", "AM/PM"), # Upper or lower case are equivalent + ("AM/PM", "a"), ) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..afe3f36 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1642 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "babel" +version = "2.18.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] + +[[package]] +name = "black" +version = "25.12.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8"}, + {file = "black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a"}, + {file = "black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea"}, + {file = "black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f"}, + {file = "black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da"}, + {file = "black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a"}, + {file = "black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be"}, + {file = "black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b"}, + {file = "black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5"}, + {file = "black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655"}, + {file = "black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a"}, + {file = "black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783"}, + {file = "black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59"}, + {file = "black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892"}, + {file = "black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43"}, + {file = "black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5"}, + {file = "black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f"}, + {file = "black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf"}, + {file = "black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d"}, + {file = "black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce"}, + {file = "black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5"}, + {file = "black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f"}, + {file = "black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f"}, + {file = "black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83"}, + {file = "black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b"}, + {file = "black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828"}, + {file = "black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +pytokens = ">=0.3.0" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "build" +version = "1.4.0" +description = "A simple, correct Python build frontend" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596"}, + {file = "build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "os_name == \"nt\""} +packaging = ">=24.0" +pyproject_hooks = "*" + +[package.extras] +uv = ["uv (>=0.1.18)"] +virtualenv = ["virtualenv (>=20.11) ; python_version < \"3.10\"", "virtualenv (>=20.17) ; python_version >= \"3.10\" and python_version < \"3.14\"", "virtualenv (>=20.31) ; python_version >= \"3.14\""] + +[[package]] +name = "certifi" +version = "2026.1.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "cmarkgfm" +version = "2025.10.22" +description = "Minimal bindings to GitHub's fork of cmark" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "cmarkgfm-2025.10.22-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37e0b126a3868e1497f8e10c11a49b007f6f4b3104032d3a51bb84404c0f4486"}, + {file = "cmarkgfm-2025.10.22-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1214acf1ef779b129e13a1a51e446425feaed64fd11124f44f78c93021bba853"}, + {file = "cmarkgfm-2025.10.22-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df2e8e1140e811d952c9eabc9471517f9193438e5eb17a9530f9559cbf216492"}, + {file = "cmarkgfm-2025.10.22-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:30e8a48bc36e1e06e6938952e3d69e4c2a73cf2dcee1071942a7c036df1982ff"}, + {file = "cmarkgfm-2025.10.22-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8d7ab8333f9cca9d4894d668ab06b19a97a30734d19c2e53ed83e33fe16d1891"}, + {file = "cmarkgfm-2025.10.22-cp310-cp310-win32.whl", hash = "sha256:451da49653abcde96d4671824c37acc900f6d01f69687ebeb0bd59ebf99738e0"}, + {file = "cmarkgfm-2025.10.22-cp310-cp310-win_amd64.whl", hash = "sha256:a815dab1d0f2e7af95613b26101143eb44dc92a3b44449096fa97ed54add822e"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18aba514abb3eedc04c3df7dd5a7743a92d7f313a04a3f7e17a059befce6fd5f"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3da228e10238411fc6823e2d4db4d514ca41d93629a6f8be751325a5477288b9"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:76e1cd82deb79d0d6c1a0a9822116c277c1f7c43496cd151c340999ac4721dec"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:87fd3616505159090b031a9601b2a24ebb2ee999abc562d924b99711fc6bb498"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f848d9698023ac9e352d73d92ab58119cb1268b12c3100578cf2d1ca1aeab2bf"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-win32.whl", hash = "sha256:93f34a753939b034a478a36687c6fef9010023e2cbe451b0ec83205e34252419"}, + {file = "cmarkgfm-2025.10.22-cp311-cp311-win_amd64.whl", hash = "sha256:43cb1e912675dd91fba97db47f8de7c19c0b3cf6456d188ff584c120bcaa12b9"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74a22cf245b32a918325a2895a9a3f6e737f0b10368b39e9a99d9e76fda4a78a"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68368a18b90e2dd795182c6fc34cee3180b2c8b380cad50c7fbd5563abff01b1"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:115fd0feaf93806b3a7c980615649b3c76a49c9dd89d5ea0c6240e10c6c71cee"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:69ccb4afa039f5b81d35de95fe935405577e115f367dda534309d66a455db5cb"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bed95895226cba280a96543f49d46b469b9e42528b119f280f9852cd4fc7749"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-win32.whl", hash = "sha256:fdf0a4689fb6febcbcaf675f2011a8074b100a4fc323f5754f627183ce492694"}, + {file = "cmarkgfm-2025.10.22-cp312-cp312-win_amd64.whl", hash = "sha256:fc14ae28769b501f61a7364a1188c827dfcf839213f02d0159801bb71c8ae989"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8030955c836827b95f46f8fc520a58ab2a03fb23d4b56e2d976618099273298"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:862b7f15ecf040fe9cd82f3be208286daddc5af94e1a20091af8451a0fe5fe74"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77840ddd24152881c2d374eae5e1baca462d24c2d78a937b6f30a12c2685cc0c"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba7693b9a4c30b2ae2d07d10c4bd3fd01dfaaaaa67c93784923b792dd10bb037"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03435c2f57ed49be0e83b2743af5dced98c1287fb9ae41a12b8055cff984bf58"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-win32.whl", hash = "sha256:aee2bf397cdf133025a2e66c6281e4fb6bd70420e3734b6dcf787ea9c2aadd78"}, + {file = "cmarkgfm-2025.10.22-cp313-cp313-win_amd64.whl", hash = "sha256:f41b76d274c8886d0a440d6577cc0d73d0ea631c3bb07758adce74ba6911d790"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:974eb8a69d6835eeaf11cb8f7ed0ad4cb4ddda9223693ad02aeb56cb0c036afb"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a591b213ab226232dee0b6ac8873560f01bd8cf423310bd8ce3f1c7cf913fd1f"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54070905b888d0d4590e03f60c5153dd456f2297ff5ff9fc43ba6d561f2eff72"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:37eef93238957bb238669810e1e3fe835706f9cbb25362b5ae8bbd51e39af45f"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a6ee1d36735abaed7af1c8459bfabe664c3f5472bf65a390f52d5e12626304b9"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-win32.whl", hash = "sha256:60e7745b429d5e3019380750b3cfaf10da4a5461ead3adf9c149251d8a6e1a3c"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314-win_amd64.whl", hash = "sha256:ee90cbccd9521aa51e8d619284bb7904c5b64387eef86cbad50717b8d943ce6d"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a8d39b0c2c1c58d81a1294ed99200cba1250ec217c079b36aff11ca6b2ca4881"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:481740a8ab020c4b8ee49746ea6ac45ec7b68b71740d12c696a64c30e26f6f49"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:367d1ab78f26af73b866a165358382c6e8e66d49da73621e832febeaeeb6400c"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2176dc0e5e4966ca4746dcbd26324adbe17be86f48756f28438d157ae1f26520"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c96d1238d91cf35e9c022af2e8c0be0a7ac227eb94497ffdf10584a684e38a3e"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-win32.whl", hash = "sha256:905a773bc866ccb4dc97a343057e2bfe07934522e1380831be413d3f93626f62"}, + {file = "cmarkgfm-2025.10.22-cp314-cp314t-win_amd64.whl", hash = "sha256:f2a04d119d09f7f5c8b565b1e8c691596bfbc59d8cabac4d7fa542a069c2c70f"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e42a5fd9869d7d4e7511df4262e8509b46453acc0fa4fcfc8e3acc74d99e85b"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd70cef08a675b83dbce79a50af102cd9d2b40d8093c97d3d39e02b3365ca026"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0651de7f1327cbaf29c6f257c29786c26a3f90a9506efb2a1fda70ab1cd99591"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ec1184ef6c4aed1f526c37a52155f49bed2a65b59178cfee39556d01564c1643"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dc11bdc6b9024df35a7043ebef95b581705e08c764fc900b50da96641ffbab06"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-win32.whl", hash = "sha256:05423a8cd8489302825fef3cdfacb6762101c07599e210ade523667d5a0ec87a"}, + {file = "cmarkgfm-2025.10.22-cp39-cp39-win_amd64.whl", hash = "sha256:35f7008547e629aaec44c063c674f393f53ab6202b2ebfa8d3273a68b093e710"}, + {file = "cmarkgfm-2025.10.22.tar.gz", hash = "sha256:5bec61007b65b919488442c838c58a6c8bf4741f5103c593b2ef180d39818eda"}, +] + +[package.dependencies] +cffi = ">=2.0.0" + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "cryptography" +version = "46.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32"}, + {file = "cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616"}, + {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0"}, + {file = "cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0"}, + {file = "cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5"}, + {file = "cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b"}, + {file = "cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e"}, + {file = "cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f"}, + {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82"}, + {file = "cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c"}, + {file = "cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061"}, + {file = "cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7"}, + {file = "cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b"}, + {file = "cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019"}, + {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4"}, + {file = "cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b"}, + {file = "cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc"}, + {file = "cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947"}, + {file = "cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3"}, + {file = "cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.4)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "docutils" +version = "0.22.4" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, +] + +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + +[[package]] +name = "id" +version = "1.6.1" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "id-1.6.1-py3-none-any.whl", hash = "sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca"}, + {file = "id-1.6.1.tar.gz", hash = "sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069"}, +] + +[package.dependencies] +urllib3 = ">=2,<3" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.14.15)"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "isort" +version = "6.1.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"}, + {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +description = "Utility functions for Python class constructs" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, + {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, +] + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "jaraco-context" +version = "6.1.0" +description = "Useful decorators and context managers" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda"}, + {file = "jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.test (>=5.6.0)", "portend", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jaraco-functools" +version = "4.4.0" +description = "Functools like those found in stdlib" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176"}, + {file = "jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb"}, +] + +[package.dependencies] +more_itertools = "*" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] + +[[package]] +name = "jeepney" +version = "0.9.0" +description = "Low-level, pure Python DBus protocol wrapper." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"}, + {file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"}, +] + +[package.extras] +test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["trio"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "keyring" +version = "25.7.0" +description = "Store and access your passwords safely." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f"}, + {file = "keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b"}, +] + +[package.dependencies] +"jaraco.classes" = "*" +"jaraco.context" = "*" +"jaraco.functools" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +completion = ["shtab (>=1.1.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=3.4)"] +test = ["pyfakefs", "pytest (>=6,!=8.1.*)"] +type = ["pygobject-stubs", "pytest-mypy (>=1.0.1)", "shtab", "types-pywin32"] + +[[package]] +name = "lxml" +version = "6.0.2" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, + {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"}, + {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"}, + {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"}, + {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"}, + {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"}, + {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"}, + {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"}, + {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"}, + {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"}, + {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"}, + {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"}, + {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"}, + {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"}, + {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"}, + {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"}, + {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"}, + {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"}, + {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"}, + {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"}, + {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"}, + {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"}, + {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"}, + {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"}, + {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"}, + {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"}, + {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"}, + {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"}, + {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"}, + {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"}, + {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"}, + {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"}, + {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"}, + {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"}, + {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"}, + {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"}, + {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"}, + {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"}, + {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"}, + {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"}, + {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"}, + {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"}, + {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "more-itertools" +version = "10.8.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\"" +files = [ + {file = "more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b"}, + {file = "more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nh3" +version = "0.3.2" +description = "Python binding to Ammonia HTML sanitizer Rust crate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d"}, + {file = "nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130"}, + {file = "nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99"}, + {file = "nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868"}, + {file = "nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93"}, + {file = "nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13"}, + {file = "nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80"}, + {file = "nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7"}, + {file = "nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0"}, + {file = "nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6"}, + {file = "nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b"}, + {file = "nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe"}, + {file = "nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104"}, + {file = "nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "platformdirs" +version = "4.5.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +description = "Wrappers to call pyproject.toml-based build backend hooks." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, + {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, +] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-docx" +version = "1.2.0" +description = "Create, read, and update Microsoft Word .docx files." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_docx-1.2.0-py3-none-any.whl", hash = "sha256:3fd478f3250fbbbfd3b94fe1e985955737c145627498896a8a6bf81f4baf66c7"}, + {file = "python_docx-1.2.0.tar.gz", hash = "sha256:7bc9d7b7d8a69c9c02ca09216118c86552704edc23bac179283f2e38f86220ce"}, +] + +[package.dependencies] +lxml = ">=3.1.0" +typing_extensions = ">=4.9.0" + +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"win32\"" +files = [ + {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, + {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +description = "readme_renderer is a library for rendering readme descriptions for Warehouse" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, + {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, +] + +[package.dependencies] +cmarkgfm = {version = ">=0.8.0", optional = true, markers = "extra == \"md\""} +docutils = ">=0.21.2" +nh3 = ">=0.2.14" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +description = "A utility belt for advanced users of python-requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, + {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, +] + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" + +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "14.3.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["dev"] +files = [ + {file = "rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69"}, + {file = "rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "secretstorage" +version = "3.5.0" +description = "Python bindings to FreeDesktop.org Secret Service API" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\" and sys_platform == \"linux\"" +files = [ + {file = "secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137"}, + {file = "secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be"}, +] + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + +[[package]] +name = "setuptools" +version = "82.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0"}, + {file = "setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "tomli" +version = "2.4.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, +] + +[[package]] +name = "towncrier" +version = "25.8.0" +description = "Building newsfiles for your project." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513"}, + {file = "towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1"}, +] + +[package.dependencies] +click = "*" +jinja2 = "*" + +[package.extras] +dev = ["furo (>=2024.5.6)", "nox", "packaging", "sphinx (>=5)", "twisted"] + +[[package]] +name = "twine" +version = "6.2.0" +description = "Collection of utilities for publishing packages on PyPI" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8"}, + {file = "twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf"}, +] + +[package.dependencies] +id = "*" +keyring = {version = ">=21.2.0", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} +packaging = ">=24.0" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + +[package.extras] +keyring = ["keyring (>=21.2.0)"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "wheel" +version = "0.46.3" +description = "Command line tool for manipulating wheel files" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d"}, + {file = "wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803"}, +] + +[package.dependencies] +packaging = ">=24.0" + +[package.extras] +test = ["pytest (>=6.0.0)", "setuptools (>=77)"] + +[[package]] +name = "zest-releaser" +version = "9.8.0" +description = "Software releasing made easy and repeatable" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "zest_releaser-9.8.0-py3-none-any.whl", hash = "sha256:a725e25bb52e901a75fed4b37ed91a5717860e296398c4cc78976323124eeb5b"}, + {file = "zest_releaser-9.8.0.tar.gz", hash = "sha256:1eea651d82723b71aa94ab1429d3e6752af935f9f61ab52c0f66b6c5abd50a15"}, +] + +[package.dependencies] +build = ">=1.2.0" +colorama = "*" +packaging = "*" +readme_renderer = {version = ">=40", extras = ["md"]} +requests = "*" +setuptools = ">=61.0.0" +twine = ">=1.6.0" +wheel = "*" + +[package.extras] +recommended = ["check-manifest", "pep440", "pyroma"] +test = ["zope.testing", "zope.testrunner"] + +[[package]] +name = "zestreleaser-towncrier" +version = "1.3.0" +description = "zest.releaser plugin to call towncrier" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "zestreleaser.towncrier-1.3.0.tar.gz", hash = "sha256:7a2fb65a760efc3b6448f8977b6181d801dd9a46625a404154240785f365d357"}, +] + +[package.dependencies] +setuptools = "*" +tomli = {version = "*", markers = "python_version >= \"3.6\""} +towncrier = ">=19.9.0" +"zest.releaser" = ">=6.17.0" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.12, <4" +content-hash = "c10f227d51ce44b413f7670b8817bb9bbe5dcb924a0a5bf2ca7577a698b5eaf3" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ef0056a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = "docxcompose" +version = "2.0.0" +description = "Compose .docx documents" +authors = ["4teamwork AG"] +readme = "README.rst" +packages = [{include = "docxcompose"}] + +[tool.poetry.dependencies] +babel = "2.18.0" +lxml = "6.0.2" +python = ">=3.12, <4" +python-docx = "1.2.0" +six = "1.17.0" + +[tool.poetry.group.dev.dependencies] +black = "^25.1.0" +flake8 = "7.3.0" +isort = "^6.0.1" +pytest = "9.0.2" +towncrier = "^25.8.0" +zest-releaser = "^9.6.2" +zestreleaser-towncrier = "^1.3.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.isort] +force_alphabetical_sort_within_sections = true +force_single_line = true +from_first = false +line_length = 200 +known_first_party = "docxcompose" +lines_after_imports = 2 + +[tool.pytest.ini_options] +python_files = ["test_*.py"] +testpaths = "tests" + +[tool.towncrier] +package = "docxcompose" +directory = "changes" +template = "towncrier.md" +filename = "CHANGELOG.md" +underlines = ["-", ""] + +[[tool.towncrier.type]] +directory = "feature" +name = "New features:" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug fixes:" +showcontent = true + +[[tool.towncrier.type]] +directory = "other" +name = "Other changes:" +showcontent = true diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..8d49e27 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[zest.releaser] +release = no +history-file = CHANGELOG.md +push-changes = yes diff --git a/setup.py b/setup.py deleted file mode 100644 index 1d7d53c..0000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -"""Installer for the docxcompose package.""" - -from setuptools import find_packages -from setuptools import setup - - -tests_require = [ - 'pytest', -] - -setup( - name='docxcompose', - version='1.4.1.dev0', - description="Compose .docx documents", - long_description=(open("README.rst").read() + "\n" + - open("HISTORY.txt").read()), - # Get more from https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - ], - keywords='Python DOCX Word OOXML', - author='Thomas Buchberger', - author_email='t.buchberger@4teamwork.ch', - url='https://github.com/4teamwork/docxcompose', - license='MIT license', - packages=find_packages(exclude=['ez_setup']), - include_package_data=True, - zip_safe=True, - install_requires=[ - 'lxml', - 'python-docx >= 0.8.8', - 'setuptools', - 'six', - 'babel', - ], - extras_require={ - 'test': tests_require, - 'tests': tests_require, - }, - entry_points={ - 'console_scripts': [ - 'docxcompose = docxcompose.command:main' - ] - }, -) diff --git a/tests/conftest.py b/tests/conftest.py index d5f04c7..0aad03e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,8 +1,9 @@ +import random from difflib import unified_diff + +import pytest from lxml import etree from utils import ComparableDocument -import pytest -import random @pytest.fixture @@ -12,37 +13,44 @@ def static_reseed(): def pytest_assertrepr_compare(config, op, left, right): - if (isinstance(left, ComparableDocument) - and isinstance(right, ComparableDocument) and op == "=="): + if ( + isinstance(left, ComparableDocument) + and isinstance(right, ComparableDocument) + and op == "==" + ): left.post_compare_failed(right) right.post_compare_failed(left) if left.has_neq_partnames: extra_right = [ - item for item in right.partnames if item not in left.partnames] + item for item in right.partnames if item not in left.partnames + ] extra_left = [ - item for item in left.partnames if item not in right.partnames] + item for item in left.partnames if item not in right.partnames + ] - explanation = ['documents contain same parts'] + explanation = ["documents contain same parts"] if right.doc is None: - explanation.append('Right document is None') + explanation.append("Right document is None") if left.doc is None: - explanation.append('Left document is None') + explanation.append("Left document is None") if extra_left: - explanation.append('Left contains extra parts {}'.format( - ', '.join(extra_left))) + explanation.append( + "Left contains extra parts {}".format(", ".join(extra_left)) + ) if extra_right: - explanation.append('Right contains extra parts {}'.format( - ', '.join(extra_right))) + explanation.append( + "Right contains extra parts {}".format(", ".join(extra_right)) + ) return explanation diffs = [] for lpart, rpart in left.neq_parts: - if not lpart.partname.endswith('.xml'): - diffs.append('Binary parts differ {}'.format(lpart.partname)) - diffs.append('') + if not lpart.partname.endswith(".xml"): + diffs.append("Binary parts differ {}".format(lpart.partname)) + diffs.append("") continue doc = etree.fromstring(lpart.blob) @@ -50,16 +58,22 @@ def pytest_assertrepr_compare(config, op, left, right): doc = etree.fromstring(rpart.blob) right_xml = etree.tounicode(doc, pretty_print=True) - diffs.extend(unified_diff( - left_xml.splitlines(), - right_xml.splitlines(), - fromfile=lpart.partname, - tofile=lpart.partname)) - diffs.append('') + diffs.extend( + unified_diff( + left_xml.splitlines(), + right_xml.splitlines(), + fromfile=lpart.partname, + tofile=lpart.partname, + ) + ) + diffs.append("") if diffs: filenames = [p[0].partname for p in left.neq_parts] diffs.insert( - 0, 'document parts are equal. Not equal parts: {}'.format( - ', '.join(filenames))) + 0, + "document parts are equal. Not equal parts: {}".format( + ", ".join(filenames) + ), + ) return diffs diff --git a/tests/test_command.py b/tests/test_command.py index f2a3ce5..0f84f17 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,15 +1,19 @@ -from docxcompose import command -from utils import docx_path import pytest +from utils import docx_path + +from docxcompose import command def test_command_creates_composed_docx_file_at_output_path(tmpdir): - output_path = tmpdir.join('outfile.docx') + output_path = tmpdir.join("outfile.docx") assert not output_path.exists() - arguments = [docx_path('master.docx'), - docx_path('table.docx'), - '--output-document', output_path.strpath] + arguments = [ + docx_path("master.docx"), + docx_path("table.docx"), + "--output-document", + output_path.strpath, + ] with pytest.raises(SystemExit) as exc_info: command.main(arguments) diff --git a/tests/test_content_controls.py b/tests/test_content_controls.py index 0acbd20..dc4af10 100644 --- a/tests/test_content_controls.py +++ b/tests/test_content_controls.py @@ -1,17 +1,18 @@ from docx import Document -from docxcompose.sdt import StructuredDocumentTags from utils import ComparableDocument from utils import docx_path from utils import FixtureDocument +from docxcompose.sdt import StructuredDocumentTags + def test_set_sdt_text_content(): - doc = Document(docx_path('content_controls.docx')) + doc = Document(docx_path("content_controls.docx")) sdt = StructuredDocumentTags(doc) - sdt.set_text('cc.plain_text', u'Foo Bar') - sdt.set_text('cc.plain_text_multiline', u'Foo Bar') - sdt.set_text('cc.plain_text_empty', u'Foo Bar') - sdt.set_text('cc.rich_text', u'Foo Bar') + sdt.set_text("cc.plain_text", "Foo Bar") + sdt.set_text("cc.plain_text_multiline", "Foo Bar") + sdt.set_text("cc.plain_text_empty", "Foo Bar") + sdt.set_text("cc.rich_text", "Foo Bar") updated = ComparableDocument(doc) expected = FixtureDocument("content_controls.docx") @@ -20,12 +21,12 @@ def test_set_sdt_text_content(): def test_set_sdt_multiline_text_content(): - doc = Document(docx_path('content_controls_multiline_formatted.docx')) + doc = Document(docx_path("content_controls_multiline_formatted.docx")) sdt = StructuredDocumentTags(doc) - sdt.set_text('cc.plain_text', u'Line 1\nLine 2') - sdt.set_text('cc.plain_text_multiline', u'Line 1\nLine 2') - sdt.set_text('cc.plain_text_empty', u'Line 1\nLine 2') - sdt.set_text('cc.rich_text', u'Line 1\nLine 2') + sdt.set_text("cc.plain_text", "Line 1\nLine 2") + sdt.set_text("cc.plain_text_multiline", "Line 1\nLine 2") + sdt.set_text("cc.plain_text_empty", "Line 1\nLine 2") + sdt.set_text("cc.rich_text", "Line 1\nLine 2") updated = ComparableDocument(doc) expected = FixtureDocument("content_controls_multiline_formatted.docx") @@ -37,6 +38,6 @@ def test_get_sdt_multiline_text_content(): doc = FixtureDocument("content_controls_multiline_formatted.docx") sdt = StructuredDocumentTags(doc.doc) - assert sdt.get_text('cc.plain_text') == 'Line 1 Line 2' - assert sdt.get_text('cc.plain_text_multiline') == 'Line 1\nLine 2' - assert sdt.get_text('cc.plain_text_empty') == 'Line 1 Line 2' + assert sdt.get_text("cc.plain_text") == "Line 1 Line 2" + assert sdt.get_text("cc.plain_text_multiline") == "Line 1\nLine 2" + assert sdt.get_text("cc.plain_text_empty") == "Line 1 Line 2" diff --git a/tests/test_embedded_excel.py b/tests/test_embedded_excel.py index 31d55e0..32e74c7 100644 --- a/tests/test_embedded_excel.py +++ b/tests/test_embedded_excel.py @@ -4,7 +4,6 @@ def test_hyperlinks(): doc = FixtureDocument("embedded_excel_chart.docx") - composed = ComposedDocument( - "master.docx", "embedded_excel_chart.docx") + composed = ComposedDocument("master.docx", "embedded_excel_chart.docx") assert composed == doc diff --git a/tests/test_fields.py b/tests/test_fields.py index 278b092..135d961 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,5 +1,7 @@ -from babel.dates import format_datetime from datetime import datetime + +from babel.dates import format_datetime + from docxcompose.properties import FieldBase from docxcompose.utils import word_to_python_date_format @@ -17,7 +19,7 @@ def test_can_parse_quoted_property_names(self): assert FieldForTesting(node).name == "Propertyname" def test_can_parse_unquoted_property_names(self): - node = ' DOCPROPERTY Propertyname \\* MERGEFORMAT ' + node = " DOCPROPERTY Propertyname \\* MERGEFORMAT " assert FieldForTesting(node).name == "Propertyname" def test_can_parse_quoted_property_names_with_spaces(self): @@ -25,7 +27,7 @@ def test_can_parse_quoted_property_names_with_spaces(self): assert FieldForTesting(node).name == "Text Property" def test_can_parse_unquoted_property_names_with_spaces(self): - node = ' DOCPROPERTY Text Property \\* MERGEFORMAT ' + node = " DOCPROPERTY Text Property \\* MERGEFORMAT " assert FieldForTesting(node).name == "Text Property" def test_can_parse_quoted_property_names_with_extra_spaces(self): @@ -33,7 +35,7 @@ def test_can_parse_quoted_property_names_with_extra_spaces(self): assert FieldForTesting(node).name == "Text Property" def test_can_parse_unquoted_property_names_with_extra_spaces(self): - node = ' DOCPROPERTY Text Property \\* MERGEFORMAT ' + node = " DOCPROPERTY Text Property \\* MERGEFORMAT " assert FieldForTesting(node).name == "Text Property" @@ -75,47 +77,76 @@ class TestFieldDateFormatMapping(object): def test_correctly_maps_simple_date(self): date = datetime(2020, 11, 19) - assert word_to_python_date_format('yy/MM/dd') == 'yy/MM/dd' - assert word_to_python_date_format('YY/MM/DD') == 'yy/MM/dd' - assert format_datetime( - date, word_to_python_date_format('yy/MM/dd')) == '20/11/19' + assert word_to_python_date_format("yy/MM/dd") == "yy/MM/dd" + assert word_to_python_date_format("YY/MM/DD") == "yy/MM/dd" + assert ( + format_datetime(date, word_to_python_date_format("yy/MM/dd"), locale="en") + == "20/11/19" + ) - assert word_to_python_date_format('yyyy/MM/dd') == 'yyyy/MM/dd' - assert word_to_python_date_format('YYYY/MM/DD') == 'yyyy/MM/dd' - assert format_datetime( - date, word_to_python_date_format('YYYY/MM/DD')) == '2020/11/19' + assert word_to_python_date_format("yyyy/MM/dd") == "yyyy/MM/dd" + assert word_to_python_date_format("YYYY/MM/DD") == "yyyy/MM/dd" + assert ( + format_datetime(date, word_to_python_date_format("YYYY/MM/DD"), locale="en") + == "2020/11/19" + ) def test_correctly_maps_date_padding(self): date = datetime(2001, 2, 4) - assert word_to_python_date_format('yy/MM/dd') == 'yy/MM/dd' - assert format_datetime( - date, word_to_python_date_format('yy/MM/dd')) == '01/02/04' + assert word_to_python_date_format("yy/MM/dd") == "yy/MM/dd" + assert ( + format_datetime(date, word_to_python_date_format("yy/MM/dd"), locale="en") + == "01/02/04" + ) - assert word_to_python_date_format('yy/M/d') == 'yy/M/d' - assert format_datetime( - date, word_to_python_date_format('yy/M/d')) == '01/2/4' + assert word_to_python_date_format("yy/M/d") == "yy/M/d" + assert ( + format_datetime(date, word_to_python_date_format("yy/M/d"), locale="en") + == "01/2/4" + ) def test_correctly_maps_date_with_weekday_and_month_name(self): date = datetime(2020, 11, 19, 23, 59, 43) - assert word_to_python_date_format('ddd dd MMM yyyy') == 'E dd MMM yyyy' - assert format_datetime(date, word_to_python_date_format( - 'ddd dd MMM yyyy')) == 'Thu 19 Nov 2020' - - assert word_to_python_date_format('dddd dd MMMM yyyy') == 'EEEE dd MMMM yyyy' - assert format_datetime(date, word_to_python_date_format( - 'dddd dd MMMM yyyy')) == 'Thursday 19 November 2020' + assert word_to_python_date_format("ddd dd MMM yyyy") == "E dd MMM yyyy" + assert ( + format_datetime( + date, word_to_python_date_format("ddd dd MMM yyyy"), locale="en" + ) + == "Thu 19 Nov 2020" + ) + + assert word_to_python_date_format("dddd dd MMMM yyyy") == "EEEE dd MMMM yyyy" + assert ( + format_datetime( + date, word_to_python_date_format("dddd dd MMMM yyyy"), locale="en" + ) + == "Thursday 19 November 2020" + ) def test_correctly_maps_date_with_time(self): date = datetime(2020, 11, 19, 1, 9, 8) - assert word_to_python_date_format( - 'ddd DD MMM YYYY HH:mm:ss') == 'E dd MMM yyyy HH:mm:ss' - assert format_datetime(date, word_to_python_date_format( - 'ddd DD MMM YYYY HH:mm:ss')) == 'Thu 19 Nov 2020 01:09:08' - - assert word_to_python_date_format( - 'ddd DD MMM YYYY H:m:s') == 'E dd MMM yyyy H:m:s' - assert format_datetime(date, word_to_python_date_format( - 'ddd DD MMM YYYY H:m:s')) == 'Thu 19 Nov 2020 1:9:8' + assert ( + word_to_python_date_format("ddd DD MMM YYYY HH:mm:ss") + == "E dd MMM yyyy HH:mm:ss" + ) + assert ( + format_datetime( + date, + word_to_python_date_format("ddd DD MMM YYYY HH:mm:ss"), + locale="en", + ) + == "Thu 19 Nov 2020 01:09:08" + ) + + assert ( + word_to_python_date_format("ddd DD MMM YYYY H:m:s") == "E dd MMM yyyy H:m:s" + ) + assert ( + format_datetime( + date, word_to_python_date_format("ddd DD MMM YYYY H:m:s"), locale="en" + ) + == "Thu 19 Nov 2020 1:9:8" + ) diff --git a/tests/test_footnotes.py b/tests/test_footnotes.py index 8caff32..2e96888 100644 --- a/tests/test_footnotes.py +++ b/tests/test_footnotes.py @@ -4,15 +4,13 @@ def test_footnote(): doc = FixtureDocument("footnote.docx") - composed = ComposedDocument( - "master.docx", "footnote.docx") + composed = ComposedDocument("master.docx", "footnote.docx") assert composed == doc def test_footnotes_with_hyperlinks(): doc = FixtureDocument("footnotes_with_hyperlinks.docx") - composed = ComposedDocument( - "master.docx", "footnotes_with_hyperlinks.docx") + composed = ComposedDocument("master.docx", "footnotes_with_hyperlinks.docx") assert composed == doc diff --git a/tests/test_header.py b/tests/test_header.py index 6f14529..1d01990 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -1,38 +1,41 @@ +import pytest from docx import Document -from docxcompose.composer import Composer -from docxcompose.utils import xpath from utils import ComposedDocument from utils import docx_path from utils import FixtureDocument -import pytest + +from docxcompose.composer import Composer +from docxcompose.utils import xpath def test_header_and_footer_refs_in_paragraph_props_get_removed(header_footer): refs = xpath( header_footer.doc.element.body, - './/w:pPr/w:sectPr/w:headerReference|.//w:pPr/w:sectPr/w:footerReference') + ".//w:pPr/w:sectPr/w:headerReference|.//w:pPr/w:sectPr/w:footerReference", + ) assert len(refs) == 0 def test_master_header_and_footer_are_preserved_when_adding_sections(): doc = FixtureDocument("master_header_footer_with_sections.docx") composed = ComposedDocument( - "master_header_footer.docx", "header_footer_sections.docx") + "master_header_footer.docx", "header_footer_sections.docx" + ) assert composed == doc def test_header_footer(): doc = FixtureDocument("header_footer.docx") - composed = ComposedDocument( - "header_footer.docx", "header_footer.docx") + composed = ComposedDocument("header_footer.docx", "header_footer.docx") assert composed == doc def test_header_footer_sections(): doc = FixtureDocument("header_footer_sections.docx") composed = ComposedDocument( - "header_footer_sections.docx", "header_footer_sections.docx") + "header_footer_sections.docx", "header_footer_sections.docx" + ) assert composed == doc diff --git a/tests/test_hyperlinks.py b/tests/test_hyperlinks.py index 4cffc48..4a1d49b 100644 --- a/tests/test_hyperlinks.py +++ b/tests/test_hyperlinks.py @@ -4,7 +4,6 @@ def test_hyperlinks(): doc = FixtureDocument("hyperlinks.docx") - composed = ComposedDocument( - "master.docx", "hyperlinks.docx") + composed = ComposedDocument("master.docx", "hyperlinks.docx") assert composed == doc diff --git a/tests/test_images.py b/tests/test_images.py index 38f8041..ff9b632 100644 --- a/tests/test_images.py +++ b/tests/test_images.py @@ -4,23 +4,20 @@ def test_images(): doc = FixtureDocument("images.docx") - composed = ComposedDocument( - "master.docx", "images.docx") + composed = ComposedDocument("master.docx", "images.docx") assert composed == doc def test_embedded_and_external_image(): doc = FixtureDocument("embedded_and_external_image.docx") - composed = ComposedDocument( - "master.docx", "embedded_and_external_image.docx") + composed = ComposedDocument("master.docx", "embedded_and_external_image.docx") assert composed == doc def test_renumbering_of_non_visual_properties(): expected = FixtureDocument("renumbering_nv_props.docx") - composed = ComposedDocument( - "header_with_image.docx", "image.docx") + composed = ComposedDocument("header_with_image.docx", "image.docx") assert composed == expected diff --git a/tests/test_numberings.py b/tests/test_numberings.py index 854e23c..fe7a83f 100644 --- a/tests/test_numberings.py +++ b/tests/test_numberings.py @@ -1,43 +1,49 @@ +import pytest from docx import Document -from docxcompose.composer import Composer -from docxcompose.utils import xpath from utils import ComposedDocument from utils import docx_path from utils import FixtureDocument -import pytest + +from docxcompose.composer import Composer +from docxcompose.utils import xpath def test_abstractnums_from_styles_are_not_duplicated(multiple_numberings): anums = xpath( multiple_numberings.doc.part.numbering_part.element, - './/w:abstractNum[.//w:pStyle]') + ".//w:abstractNum[.//w:pStyle]", + ) assert len(anums) == 2 def test_restart_first_numbering(multiple_numberings): - paragraphs = xpath(multiple_numberings.doc.element.body, './/w:p') - assert len(xpath(paragraphs[9], './/w:numId')) == 1 + paragraphs = xpath(multiple_numberings.doc.element.body, ".//w:p") + assert len(xpath(paragraphs[9], ".//w:numId")) == 1 def test_do_not_restart_numbering_of_bullets(mixed_numberings): - paragraphs = xpath(mixed_numberings.doc.element.body, './/w:p') - assert len(xpath(paragraphs[10], './/w:numId')) == 0 + paragraphs = xpath(mixed_numberings.doc.element.body, ".//w:p") + assert len(xpath(paragraphs[10], ".//w:numId")) == 0 def test_do_not_break_on_custom_styled_numbering(custom_styled_numbering): - assert custom_styled_numbering.doc.element.xpath('.//w:numId/@w:val') == ['2']*4 + assert custom_styled_numbering.doc.element.xpath(".//w:numId/@w:val") == ["2"] * 4 def test_preserve_zero_numbering_references(numberings_with_zero_reference): numberings_with_zero_ref = xpath( - numberings_with_zero_reference.doc.element.body, - './/w:p//w:numId[@w:val="0"]') + numberings_with_zero_reference.doc.element.body, './/w:p//w:numId[@w:val="0"]' + ) assert len(numberings_with_zero_ref) == 2 -def test_restarts_numbering_if_sequence_is_split_across_elements(numbering_with_paragraphs): - numbering_ids = [each.val for each in xpath( - numbering_with_paragraphs.doc.element.body, './/w:numId')] +def test_restarts_numbering_if_sequence_is_split_across_elements( + numbering_with_paragraphs, +): + numbering_ids = [ + each.val + for each in xpath(numbering_with_paragraphs.doc.element.body, ".//w:numId") + ] assert numbering_ids == [3, 3, 3, 3, 4, 4, 4] @@ -45,39 +51,38 @@ def test_restarts_numbering_if_sequence_is_split_across_elements(numbering_with_ def test_restart_numbering_manages_shared_style_names(static_reseed): doc = FixtureDocument("common_stylename_different_id.docx") composed = ComposedDocument( - "common_stylename_different_id1.docx", - "common_stylename_different_id2.docx") + "common_stylename_different_id1.docx", "common_stylename_different_id2.docx" + ) assert composed == doc def test_numberings(static_reseed): doc = FixtureDocument("numberings.docx") - composed = ComposedDocument( - "numberings.docx", "numberings.docx") + composed = ComposedDocument("numberings.docx", "numberings.docx") assert composed == doc def test_restart_numberings(): doc = FixtureDocument("numberings_restart.docx") - composed = ComposedDocument( - "numberings_restart.docx", "numberings_restart.docx") + composed = ComposedDocument("numberings_restart.docx", "numberings_restart.docx") assert composed == doc def test_numberings_styles(): doc = FixtureDocument("numberings_styles.docx") - composed = ComposedDocument( - "numberings_styles.docx", "numberings_styles.docx") + composed = ComposedDocument("numberings_styles.docx", "numberings_styles.docx") assert composed == doc def test_numbering_reference_to_numbering_zero(): doc = FixtureDocument("numbering_reference_to_numbering_zero.docx") - composed = ComposedDocument("numbering_reference_to_numbering_zero.docx", - "numbering_reference_to_numbering_zero.docx") + composed = ComposedDocument( + "numbering_reference_to_numbering_zero.docx", + "numbering_reference_to_numbering_zero.docx", + ) assert composed == doc @@ -86,15 +91,15 @@ def test_restarts_numbering_for_all_elements_of_same_sequence(): doc = FixtureDocument("numbering_with_paragraphs_in_between.docx") composed = ComposedDocument( "numbering_with_paragraphs_in_between.docx", - "numbering_with_paragraphs_in_between.docx") + "numbering_with_paragraphs_in_between.docx", + ) assert composed == doc def test_preserves_list_styles_when_restarting_numberings(): doc = FixtureDocument("broken_listing.docx") - composed = ComposedDocument( - "broken_listing_master.docx", "broken_listing.docx") + composed = ComposedDocument("broken_listing_master.docx", "broken_listing.docx") assert composed == doc @@ -102,7 +107,8 @@ def test_preserves_list_styles_when_restarting_numberings(): def test_preserves_list_styles_when_restarting_many_numberings(): doc = FixtureDocument("broken_listing_many.docx") composed = ComposedDocument( - "broken_listing_master.docx", "broken_listing_many.docx") + "broken_listing_master.docx", "broken_listing_many.docx" + ) assert composed == doc @@ -110,17 +116,18 @@ def test_preserves_list_styles_when_restarting_many_numberings(): def test_preserves_list_styles_when_restarting_nested_numberings(): doc = FixtureDocument("broken_listing_nested.docx") composed = ComposedDocument( - "broken_listing_master.docx", "broken_listing_nested.docx") + "broken_listing_master.docx", "broken_listing_nested.docx" + ) assert composed == doc @pytest.fixture def numberings_with_zero_reference(): - composer = Composer(Document( - docx_path("numbering_reference_to_numbering_zero.docx"))) - composer.append(Document( - docx_path("numbering_reference_to_numbering_zero.docx"))) + composer = Composer( + Document(docx_path("numbering_reference_to_numbering_zero.docx")) + ) + composer.append(Document(docx_path("numbering_reference_to_numbering_zero.docx"))) return composer @@ -154,6 +161,6 @@ def numbering_with_paragraphs(): @pytest.fixture def custom_styled_numbering(): - composer = Composer(Document(docx_path('master.docx'))) + composer = Composer(Document(docx_path("master.docx"))) composer.append(Document(docx_path("custom_list_style.docx"))) return composer diff --git a/tests/test_properties.py b/tests/test_properties.py index 99250a9..baadd91 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -1,7 +1,18 @@ from datetime import datetime +from datetime import timezone + +import docx +import pytest from docx import Document from docx.opc.constants import RELATIONSHIP_TYPE as RT from docx.oxml import parse_xml +from lxml.etree import tostring +from utils import assert_complex_field_value +from utils import assert_simple_field_value +from utils import cached_complex_field_values +from utils import docx_path +from utils import simple_field_expression + from docxcompose.properties import ComplexField from docxcompose.properties import CUSTOM_PROPERTY_TYPES from docxcompose.properties import CustomProperties @@ -9,63 +20,61 @@ from docxcompose.properties import value2vt from docxcompose.properties import vt2value from docxcompose.utils import xpath -from lxml.etree import tostring -from utils import assert_complex_field_value -from utils import assert_simple_field_value -from utils import cached_complex_field_values -from utils import docx_path -from utils import simple_field_expression -import docx -import pytest class TestIdentifyDocpropertiesInDocument(object): def test_identifies_simple_fields_correctly(self): - document = Document(docx_path('outdated_docproperty_with_umlauts.docx')) + document = Document(docx_path("outdated_docproperty_with_umlauts.docx")) properties = CustomProperties(document).find_docprops_in_document() - assert 1 == len(properties), \ - 'input should contain 1 simple field docproperty' + assert 1 == len(properties), "input should contain 1 simple field docproperty" prop = properties[0] - assert prop.name == u'F\xfc\xfc' + assert prop.name == "F\xfc\xfc" assert isinstance(prop, SimpleField) def test_identifies_complex_fields_correctly(self): - document = Document(docx_path('three_props_in_same_paragraph.docx')) + document = Document(docx_path("three_props_in_same_paragraph.docx")) properties = CustomProperties(document).find_docprops_in_document() - assert len(document.paragraphs) == 1, 'input file should contains one paragraph' - assert len(properties) == 3, \ - 'input should contain three complex field docproperties' + assert len(document.paragraphs) == 1, "input file should contains one paragraph" + assert ( + len(properties) == 3 + ), "input should contain three complex field docproperties" # check that all fields were identified as complex fields for prop in properties: assert isinstance(prop, ComplexField) # check that all field names were parsed correctly - expected_names = ('Text Property', 'Number Property', 'Text Property') + expected_names = ("Text Property", "Number Property", "Text Property") for name, prop in zip(expected_names, properties): assert name == prop.name # check that begin, separate and end were identified correctly - attrib_key = '{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldCharType' + attrib_key = ( + "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldCharType" + ) expected_indexes = ((1, 3, 11), (13, 15, 17), (25, 27, 32)) for prop, indexes in zip(properties, expected_indexes): assert prop.begin_run.getchildren()[1].attrib[attrib_key] == "begin" - assert prop.get_separate_run().getchildren()[1].attrib[attrib_key] == "separate" + assert ( + prop.get_separate_run().getchildren()[1].attrib[attrib_key] + == "separate" + ) assert prop.end_run.getchildren()[1].attrib[attrib_key] == "end" assert prop.w_p.index(prop.begin_run) == indexes[0] assert prop.w_p.index(prop.get_separate_run()) == indexes[1] assert prop.w_p.index(prop.end_run) == indexes[2] def test_finds_run_nodes_in_complex_fields_correctly(self): - document = Document(docx_path('three_props_in_same_paragraph.docx')) + document = Document(docx_path("three_props_in_same_paragraph.docx")) properties = CustomProperties(document).find_docprops_in_document() - assert len(properties) == 3, \ - 'input should contain three complex field docproperties' + assert ( + len(properties) == 3 + ), "input should contain three complex field docproperties" # In the first field, there are the following runs: begin, docprop, # a separate, 3 runs for the value (because of spellcheck) and end @@ -113,22 +122,24 @@ def test_finds_run_nodes_in_complex_fields_correctly(self): assert runs[-1] == prop.end_run def test_finds_run_nodes_in_complex_field_without_separate_correctly(self): - document = Document(docx_path('complex_field_without_separate.docx')) + document = Document(docx_path("complex_field_without_separate.docx")) properties = CustomProperties(document).find_docprops_in_document() - assert len(properties) == 2, \ - 'input should contain two complex field docproperties' + assert ( + len(properties) == 2 + ), "input should contain two complex field docproperties" # The "User.FullName" docproperty should be the one without a separate run # In this field, there are the following runs: begin, docprop and end - matches = [prop for prop in properties if prop.name == 'User.FullName'] - assert len(matches) == 1, \ - "There should be only one User.FullName docproperty" + matches = [prop for prop in properties if prop.name == "User.FullName"] + assert len(matches) == 1, "There should be only one User.FullName docproperty" prop = matches[0] - assert prop.get_separate_run() is None, \ - "This complex field should not have a separate run." - assert prop.get_runs_for_update() == [], \ - "As there is no separate run, there should be no run to update" + assert ( + prop.get_separate_run() is None + ), "This complex field should not have a separate run." + assert ( + prop.get_runs_for_update() == [] + ), "As there is no separate run, there should be no run to update" # As there are no separate, all runs should be removed when dissolving # the property. @@ -139,47 +150,54 @@ def test_finds_run_nodes_in_complex_field_without_separate_correctly(self): assert runs[2] == prop.end_run def test_finds_field_name_and_format_when_split_in_several_runs(self): - document = Document(docx_path('complex_field_with_split_fieldname.docx')) + document = Document(docx_path("complex_field_with_split_fieldname.docx")) properties = CustomProperties(document).find_docprops_in_document() - assert len(properties) == 1, \ - 'input should contain one complex field docproperties' + assert ( + len(properties) == 1 + ), "input should contain one complex field docproperties" prop = properties[0] # make sure that the field name and format are indeed split over several runs - assert [[each.text for each in xpath(run, "w:instrText")] - for run in prop._runs[:9]] == \ - [[' DOCPROPERTY "ogg.document.document_date" \\@ "ddd'], - ['d'], - [' dd'], - [' '], - ['MM'], - ['M'], - ['M'], - [' yy'], - ['yy hh:mm:s" \\* MERGEFORMAT ']] - - assert prop._get_fieldname_string() == \ - ' DOCPROPERTY "ogg.document.document_date" \\@ "ddd' +\ - 'd dd MMMM yyyy hh:mm:s" \\* MERGEFORMAT ' - - assert prop.name == 'ogg.document.document_date' - assert prop.date_format == 'EEEE dd MMMM yyyy hh:mm:s' + assert [ + [each.text for each in xpath(run, "w:instrText")] for run in prop._runs[:9] + ] == [ + [' DOCPROPERTY "ogg.document.document_date" \\@ "ddd'], + ["d"], + [" dd"], + [" "], + ["MM"], + ["M"], + ["M"], + [" yy"], + ['yy hh:mm:s" \\* MERGEFORMAT '], + ] + + assert ( + prop._get_fieldname_string() + == ' DOCPROPERTY "ogg.document.document_date" \\@ "ddd' + + 'd dd MMMM yyyy hh:mm:s" \\* MERGEFORMAT ' + ) + + assert prop.name == "ogg.document.document_date" + assert prop.date_format == "EEEE dd MMMM yyyy hh:mm:s" def test_finds_header_footer_of_different_sections(self): - document = Document(docx_path('docproperties_header_footer_3_sections.docx')) + document = Document(docx_path("docproperties_header_footer_3_sections.docx")) properties = CustomProperties(document).find_docprops_in_document() - assert len(properties) == 5, \ - 'input should contain 5 properties in header/footer and body' + assert ( + len(properties) == 5 + ), "input should contain 5 properties in header/footer and body" expected_properties = [ - 'Text Property', - 'Number Property', - 'Date Property', - 'Float Property', - 'Boolean Property'] + "Text Property", + "Number Property", + "Date Property", + "Float Property", + "Boolean Property", + ] assert [each.name for each in properties] == expected_properties @@ -187,128 +205,106 @@ def test_finds_header_footer_of_different_sections(self): class TestUpdateAllDocproperties(object): def test_updates_doc_properties_in_header(self): - document = Document(docx_path('docproperties_header.docx')) + document = Document(docx_path("docproperties_header.docx")) assert_simple_field_value( - u'xxx', - document.sections[0].header.part.element, - u'my.text-prop') + "xxx", document.sections[0].header.part.element, "my.text-prop" + ) CustomProperties(document).update_all() assert_simple_field_value( - u"i'm some text", - document.sections[0].header.part.element, - u'my.text-prop') + "i'm some text", document.sections[0].header.part.element, "my.text-prop" + ) def test_updates_doc_properties_in_footer(self): - document = Document(docx_path('docproperties_footer.docx')) + document = Document(docx_path("docproperties_footer.docx")) assert_simple_field_value( - u'xxx', - document.sections[0].footer.part.element, - u'my.text-prop') + "xxx", document.sections[0].footer.part.element, "my.text-prop" + ) CustomProperties(document).update_all() assert_simple_field_value( - u'b\xe4hh', - document.sections[0].footer.part.element, - u'my.text-prop') + "b\xe4hh", document.sections[0].footer.part.element, "my.text-prop" + ) def test_updates_doc_properties_different_first_page(self): - document = Document(docx_path('docproperties_different_first_page_1_section.docx')) + document = Document( + docx_path("docproperties_different_first_page_1_section.docx") + ) assert_simple_field_value( - u'xxx', - document.sections[0].first_page_header.part.element, - u'page1.header') + "xxx", document.sections[0].first_page_header.part.element, "page1.header" + ) assert_simple_field_value( - u'xxx', - document.sections[0].first_page_footer.part.element, - u'page1.footer') + "xxx", document.sections[0].first_page_footer.part.element, "page1.footer" + ) assert_simple_field_value( - u'0', - document.sections[0].header.part.element, - u'page2.header') + "0", document.sections[0].header.part.element, "page2.header" + ) assert_simple_field_value( - u'01.01.1970', - document.sections[0].footer.part.element, - u'page2.footer') + "01.01.1970", document.sections[0].footer.part.element, "page2.footer" + ) CustomProperties(document).update_all() assert_simple_field_value( - u'p1h', - document.sections[0].first_page_header.part.element, - u'page1.header') + "p1h", document.sections[0].first_page_header.part.element, "page1.header" + ) assert_simple_field_value( - u'p1f', - document.sections[0].first_page_footer.part.element, - u'page1.footer') + "p1f", document.sections[0].first_page_footer.part.element, "page1.footer" + ) assert_simple_field_value( - u'42', - document.sections[0].header.part.element, - u'page2.header') + "42", document.sections[0].header.part.element, "page2.header" + ) assert_simple_field_value( - u'18.10.1984', - document.sections[0].footer.part.element, - u'page2.footer') + "18.10.1984", document.sections[0].footer.part.element, "page2.footer" + ) def test_updates_doc_properties_different_odd_even_pages(self): - document = Document(docx_path('docproperties_different_odd_even_pages_1_section.docx')) + document = Document( + docx_path("docproperties_different_odd_even_pages_1_section.docx") + ) assert_simple_field_value( - u'xxx', - document.sections[0].header.part.element, - u'odd.header') + "xxx", document.sections[0].header.part.element, "odd.header" + ) assert_simple_field_value( - u'xxx', - document.sections[0].footer.part.element, - u'odd.footer') + "xxx", document.sections[0].footer.part.element, "odd.footer" + ) assert_simple_field_value( - u'0', - document.sections[0].even_page_header.part.element, - u'even.header') + "0", document.sections[0].even_page_header.part.element, "even.header" + ) assert_simple_field_value( - u'Y', - document.sections[0].even_page_footer.part.element, - u'even.footer') + "Y", document.sections[0].even_page_footer.part.element, "even.footer" + ) CustomProperties(document).update_all() assert_simple_field_value( - u'odd-header', - document.sections[0].header.part.element, - u'odd.header') + "odd-header", document.sections[0].header.part.element, "odd.header" + ) assert_simple_field_value( - u'odd-footer', - document.sections[0].footer.part.element, - u'odd.footer') + "odd-footer", document.sections[0].footer.part.element, "odd.footer" + ) assert_simple_field_value( - u'1337', - document.sections[0].even_page_header.part.element, - u'even.header') + "1337", document.sections[0].even_page_header.part.element, "even.header" + ) assert_simple_field_value( - u'N', - document.sections[0].even_page_footer.part.element, - u'even.footer') + "N", document.sections[0].even_page_footer.part.element, "even.footer" + ) def test_updates_docproperties_shared_footer(self): - document = Document(docx_path('docproperties_shared_footer_2_sections.docx')) + document = Document(docx_path("docproperties_shared_footer_2_sections.docx")) header_1 = document.sections[0].header header_2 = document.sections[1].header assert not header_1.is_linked_to_previous assert not header_2.is_linked_to_previous - assert_complex_field_value( - u'xxx', - header_1.part.element, - u'section1.header') - assert_complex_field_value( - u'yyy', - header_2.part.element, - u'section2.header') + assert_complex_field_value("xxx", header_1.part.element, "section1.header") + assert_complex_field_value("yyy", header_2.part.element, "section2.header") # the same footer should be referenced by both sections # the sections should be considered as linked @@ -316,40 +312,26 @@ def test_updates_docproperties_shared_footer(self): footer_2 = document.sections[1].footer assert not footer_1.is_linked_to_previous assert footer_2.is_linked_to_previous - assert_complex_field_value( - u'99', - footer_1.part.element, - u'shared.footer') - assert_complex_field_value( - u'99', - footer_2.part.element, - u'shared.footer') + assert_complex_field_value("99", footer_1.part.element, "shared.footer") + assert_complex_field_value("99", footer_2.part.element, "shared.footer") CustomProperties(document).update_all() assert_complex_field_value( - u'h\xe4der 1', - header_1.part.element, - u'section1.header') + "h\xe4der 1", header_1.part.element, "section1.header" + ) assert_complex_field_value( - u'h\xf6der 2', - header_2.part.element, - u'section2.header') + "h\xf6der 2", header_2.part.element, "section2.header" + ) # the same footer should be referenced by both sections footer_1 = document.sections[0].footer footer_2 = document.sections[1].footer - assert_complex_field_value( - u'123123123', - footer_1.part.element, - u'shared.footer') - assert_complex_field_value( - u'123123123', - footer_2.part.element, - u'shared.footer') + assert_complex_field_value("123123123", footer_1.part.element, "shared.footer") + assert_complex_field_value("123123123", footer_2.part.element, "shared.footer") def test_updates_docproperties_shared_header(self): - document = Document(docx_path('docproperties_shared_header_2_sections.docx')) + document = Document(docx_path("docproperties_shared_header_2_sections.docx")) # the same header should be referenced by both sections # the sections should be considered as linked @@ -357,53 +339,33 @@ def test_updates_docproperties_shared_header(self): header_2 = document.sections[1].header assert not header_1.is_linked_to_previous assert header_2.is_linked_to_previous - assert_complex_field_value( - u'xxx', - header_1.part.element, - u'shared.header') - assert_complex_field_value( - u'xxx', - header_2.part.element, - u'shared.header') + assert_complex_field_value("xxx", header_1.part.element, "shared.header") + assert_complex_field_value("xxx", header_2.part.element, "shared.header") footer_1 = document.sections[0].footer footer_2 = document.sections[1].footer assert not footer_1.is_linked_to_previous assert not footer_2.is_linked_to_previous - assert_complex_field_value( - u'123', - footer_1.part.element, - u'section1.footer') - assert_complex_field_value( - u'yyy', - footer_2.part.element, - u'section2.footer') + assert_complex_field_value("123", footer_1.part.element, "section1.footer") + assert_complex_field_value("yyy", footer_2.part.element, "section2.footer") CustomProperties(document).update_all() # the same header should be referenced by both sections - assert_complex_field_value( - u'sh\xe4red', - header_1.part.element, - u'shared.header') - assert_complex_field_value( - u'sh\xe4red', - header_2.part.element, - u'shared.header') + assert_complex_field_value("sh\xe4red", header_1.part.element, "shared.header") + assert_complex_field_value("sh\xe4red", header_2.part.element, "shared.header") footer_1 = document.sections[0].footer footer_2 = document.sections[1].footer + assert_complex_field_value("-1.0", footer_1.part.element, "section1.footer") assert_complex_field_value( - u'-1.0', - footer_1.part.element, - u'section1.footer') - assert_complex_field_value( - u'f\xfc\xfcter', - footer_2.part.element, - u'section2.footer') + "f\xfc\xfcter", footer_2.part.element, "section2.footer" + ) def test_updates_docproperties_shared_header_footer(self): - document = Document(docx_path('docproperties_shared_header_footer_2_sections.docx')) + document = Document( + docx_path("docproperties_shared_header_footer_2_sections.docx") + ) # the same header should be referenced by both sections # the sections should be considered as linked @@ -411,14 +373,8 @@ def test_updates_docproperties_shared_header_footer(self): header_2 = document.sections[1].header assert not header_1.is_linked_to_previous assert header_2.is_linked_to_previous - assert_complex_field_value( - u'blub', - header_1.part.element, - u'shared.header') - assert_complex_field_value( - u'blub', - header_2.part.element, - u'shared.header') + assert_complex_field_value("blub", header_1.part.element, "shared.header") + assert_complex_field_value("blub", header_2.part.element, "shared.header") # the same footer should be referenced by both sections # the sections should be considered as linked @@ -426,102 +382,85 @@ def test_updates_docproperties_shared_header_footer(self): footer_2 = document.sections[1].footer assert not footer_1.is_linked_to_previous assert footer_2.is_linked_to_previous - assert_complex_field_value( - u'N', - footer_1.part.element, - u'shared.footer') - assert_complex_field_value( - u'N', - footer_2.part.element, - u'shared.footer') + assert_complex_field_value("N", footer_1.part.element, "shared.footer") + assert_complex_field_value("N", footer_2.part.element, "shared.footer") CustomProperties(document).update_all() # the same header should be referenced by both sections - assert_complex_field_value( - u'ig bi obe', - header_1.part.element, - u'shared.header') - assert_complex_field_value( - u'ig bi obe', - header_2.part.element, - u'shared.header') + assert_complex_field_value("ig bi obe", header_1.part.element, "shared.header") + assert_complex_field_value("ig bi obe", header_2.part.element, "shared.header") # the same footer should be referenced by both sections footer_1 = document.sections[0].footer footer_2 = document.sections[1].footer - assert_complex_field_value( - u'Y', - footer_1.part.element, - u'shared.footer') - assert_complex_field_value( - u'Y', - footer_2.part.element, - u'shared.footer') + assert_complex_field_value("Y", footer_1.part.element, "shared.footer") + assert_complex_field_value("Y", footer_2.part.element, "shared.footer") def test_updates_doc_properties_with_umlauts(self): - document = Document(docx_path('outdated_docproperty_with_umlauts.docx')) + document = Document(docx_path("outdated_docproperty_with_umlauts.docx")) - assert_simple_field_value( - u'xxx', document.element.body, u"F\xfc\xfc") + assert_simple_field_value("xxx", document.element.body, "F\xfc\xfc") CustomProperties(document).update_all() - assert_simple_field_value( - u'j\xe4ja.', document.element.body, u"F\xfc\xfc") + assert_simple_field_value("j\xe4ja.", document.element.body, "F\xfc\xfc") def test_complex_docprop_fields_with_multiple_textnodes_are_updated(self): - document = Document(docx_path('spellchecked_docproperty.docx')) - paragraphs = xpath(document.element.body, '//w:p') - assert len(paragraphs) == 1, 'input file contains one paragraph' - assert len(xpath(document.element.body, '//w:instrText')) == 1, \ - 'input contains one complex field docproperty' + document = Document(docx_path("spellchecked_docproperty.docx")) + paragraphs = xpath(document.element.body, "//w:p") + assert len(paragraphs) == 1, "input file contains one paragraph" + assert ( + len(xpath(document.element.body, "//w:instrText")) == 1 + ), "input contains one complex field docproperty" w_p = paragraphs[0] cached_values = cached_complex_field_values(w_p) - assert len(cached_values) == 4, \ - 'doc property value is scattered over 4 parts' - assert ''.join(cached_values) == 'i will be spllchecked!' + assert len(cached_values) == 4, "doc property value is scattered over 4 parts" + assert "".join(cached_values) == "i will be spllchecked!" CustomProperties(document).update_all() - w_p = xpath(document.element.body, '//w:p')[0] + w_p = xpath(document.element.body, "//w:p")[0] cached_values = cached_complex_field_values(w_p) - assert len(cached_values) == 1, \ - 'doc property value has been reset to one cached value' - assert cached_values[0] == 'i will be spllchecked!' + assert ( + len(cached_values) == 1 + ), "doc property value has been reset to one cached value" + assert cached_values[0] == "i will be spllchecked!" def test_complex_docprop_with_multiple_textnode_in_same_run_are_updated(self): - document = Document(docx_path('two_textnodes_in_run_docproperty.docx')) - paragraphs = xpath(document.element.body, '//w:p') - assert len(paragraphs) == 1, 'input file contains one paragraph' - assert len(xpath(document.element.body, '//w:instrText')) == 1, \ - 'input contains one complex field docproperty' + document = Document(docx_path("two_textnodes_in_run_docproperty.docx")) + paragraphs = xpath(document.element.body, "//w:p") + assert len(paragraphs) == 1, "input file contains one paragraph" + assert ( + len(xpath(document.element.body, "//w:instrText")) == 1 + ), "input contains one complex field docproperty" w_p = paragraphs[0] cached_values = cached_complex_field_values(w_p) - assert len(cached_values) == 2, \ - 'doc property value is scattered over 2 parts' - assert ''.join(cached_values) == 'Hello there' + assert len(cached_values) == 2, "doc property value is scattered over 2 parts" + assert "".join(cached_values) == "Hello there" CustomProperties(document).update_all() - w_p = xpath(document.element.body, '//w:p')[0] + w_p = xpath(document.element.body, "//w:p")[0] cached_values = cached_complex_field_values(w_p) - assert len(cached_values) == 1, \ - 'doc property value has been reset to one cached value' - assert cached_values[0] == 'i will be spllchecked!' + assert ( + len(cached_values) == 1 + ), "doc property value has been reset to one cached value" + assert cached_values[0] == "i will be spllchecked!" def test_three_complex_docprop_in_same_paragraph(self): - document = Document(docx_path('three_props_in_same_paragraph.docx')) + document = Document(docx_path("three_props_in_same_paragraph.docx")) properties = CustomProperties(document) - assert len(document.paragraphs) == 1, 'input file should contains one paragraph' + assert len(document.paragraphs) == 1, "input file should contains one paragraph" paragraph = document.paragraphs[0] - assert len(properties.find_docprops_in_document()) == 3, \ - 'input should contain three complex field docproperties' + assert ( + len(properties.find_docprops_in_document()) == 3 + ), "input should contain three complex field docproperties" - text = u'{text} / {num} mor between the fields {text} and some afte the three fields' + text = "{text} / {num} mor between the fields {text} and some afte the three fields" assert paragraph.text == text.format(text="I was spellcecked", num=0) properties.update_all() @@ -529,185 +468,198 @@ def test_three_complex_docprop_in_same_paragraph(self): assert paragraph.text == text.format(text="Foo", num=2) def test_multiple_identical_docprops_get_updated(self): - document = Document(docx_path('multiple_identical_properties.docx')) - assert len(document.paragraphs) == 3, 'input file should contain 3 paragraphs' + document = Document(docx_path("multiple_identical_properties.docx")) + assert len(document.paragraphs) == 3, "input file should contain 3 paragraphs" for paragraph in document.paragraphs: - assert len(xpath(paragraph._p, './/w:instrText')) == 1, \ - 'paragraph should contain one complex field docproperties' + assert ( + len(xpath(paragraph._p, ".//w:instrText")) == 1 + ), "paragraph should contain one complex field docproperties" - assert paragraph.text == u'Foo' + assert paragraph.text == "Foo" CustomProperties(document).update_all() for i, paragraph in enumerate(document.paragraphs): - assert paragraph.text == u'Bar', 'docprop {} was not updated'.format(i+1) + assert paragraph.text == "Bar", "docprop {} was not updated".format(i + 1) def test_docproperty_without_separate_does_get_updated(self): - document = Document(docx_path('complex_field_without_separate.docx')) + document = Document(docx_path("complex_field_without_separate.docx")) custom_properties = CustomProperties(document) properties = custom_properties.find_docprops_in_document() paragraphs = document.paragraphs # Make sure that a value is set for 'User.FullName' - assert ('User.FullName', 'Test User') in custom_properties.items() - assert ('Dossier.Title', ' Some Title') in custom_properties.items() + assert ("User.FullName", "Test User") in custom_properties.items() + assert ("Dossier.Title", " Some Title") in custom_properties.items() # Make sure that 'User.FullName' field has no separate node - matches = [prop for prop in properties if prop.name == 'User.FullName'] - assert len(matches) == 1, \ - "There should be only one User.FullName docproperty" + matches = [prop for prop in properties if prop.name == "User.FullName"] + assert len(matches) == 1, "There should be only one User.FullName docproperty" fullname = matches[0] - assert fullname.get_separate_run() is None, \ - "This complex field should not have a separate run." + assert ( + fullname.get_separate_run() is None + ), "This complex field should not have a separate run." # Make sure that 'Dossier.Title' field has a separate node - matches = [prop for prop in properties if prop.name == 'Dossier.Title'] - assert len(matches) == 1, \ - "There should be only one Dossier.Title docproperty" + matches = [prop for prop in properties if prop.name == "Dossier.Title"] + assert len(matches) == 1, "There should be only one Dossier.Title docproperty" title = matches[0] - assert title.get_separate_run() is not None, \ - "This complex field should have a separate run." + assert ( + title.get_separate_run() is not None + ), "This complex field should have a separate run." # Check the content of the paragraphs before update assert len(paragraphs) == 2 - assert paragraphs[0].text == u'Sachbearbeiter: ' - assert u'Sachbearbeiter: ' in fullname.w_p.xml - assert paragraphs[1].text == u'Dossier Titel: ' + assert paragraphs[0].text == "Sachbearbeiter: " + assert "Sachbearbeiter: " in fullname.w_p.xml + assert paragraphs[1].text == "Dossier Titel: " custom_properties.update_all() # Field with missing separate was not updated - assert paragraphs[0].text == u'Sachbearbeiter: Test User' + assert paragraphs[0].text == "Sachbearbeiter: Test User" # Next field was updated correctly - assert paragraphs[1].text == u'Dossier Titel: Some Title' + assert paragraphs[1].text == "Dossier Titel: Some Title" def test_date_docprops_with_format_get_updated(self): - document = Document(docx_path('date_docproperties_with_format.docx')) - assert len(document.paragraphs) == 3, 'input file should contain 3 paragraph' + document = Document(docx_path("date_docproperties_with_format.docx")) + assert len(document.paragraphs) == 3, "input file should contain 3 paragraph" - expected_values = [u'11.06.19', u'mardi 11 juin 2019', u'11-6-19 0:0:0'] - for i, (expected, paragraph) in enumerate(zip(expected_values, document.paragraphs)): + expected_values = ["11.06.19", "mardi 11 juin 2019", "11-6-19 0:0:0"] + for i, (expected, paragraph) in enumerate( + zip(expected_values, document.paragraphs) + ): assert paragraph.text == expected CustomProperties(document).update_all() - expected_values = [u'23.01.20', u'jeudi 23 janvier 2020', u'23-1-20 10:0:0'] - for i, (expected, paragraph) in enumerate(zip(expected_values, document.paragraphs)): - assert paragraph.text == expected, 'docprop {} was not updated correctly'.format(i+1) + expected_values = ["23.01.20", "jeudi 23 janvier 2020", "23-1-20 10:0:0"] + for i, (expected, paragraph) in enumerate( + zip(expected_values, document.paragraphs) + ): + assert ( + paragraph.text == expected + ), "docprop {} was not updated correctly".format(i + 1) def test_date_docprops_respect_language(self): - document = Document(docx_path('date_docproperties_with_format.docx')) - assert len(document.paragraphs) == 3, 'input file should contain 3 paragraph' + document = Document(docx_path("date_docproperties_with_format.docx")) + assert len(document.paragraphs) == 3, "input file should contain 3 paragraph" CustomProperties(document).update_all() - document.paragraphs[1].text == u'jeudi 23 janvier 2020' + document.paragraphs[1].text == "jeudi 23 janvier 2020" - document.element.xpath('.//w:lang')[0].set(docx.oxml.shared.qn('w:val'),'de-CH') + document.element.xpath(".//w:lang")[0].set( + docx.oxml.shared.qn("w:val"), "de-CH" + ) CustomProperties(document).update_all() - document.paragraphs[1].text == u'Donnerstag 23 Januar 2020' + document.paragraphs[1].text == "Donnerstag 23 Januar 2020" - document.element.xpath('.//w:lang')[0].set(docx.oxml.shared.qn('w:val'),'en-US') + document.element.xpath(".//w:lang")[0].set( + docx.oxml.shared.qn("w:val"), "en-US" + ) CustomProperties(document).update_all() - document.paragraphs[1].text == u'Thursday 23 January 2020' + document.paragraphs[1].text == "Thursday 23 January 2020" def test_docprops_with_split_fieldname_get_updated(self): - document = Document(docx_path('complex_field_with_split_fieldname.docx')) - assert len(document.paragraphs) == 1, 'input file should contain 1 paragraph' + document = Document(docx_path("complex_field_with_split_fieldname.docx")) + assert len(document.paragraphs) == 1, "input file should contain 1 paragraph" paragraph = document.paragraphs[0] - assert paragraph.text == 'Datum: Tuesday 09 February 2021 00:00:00' + assert paragraph.text == "Datum: Tuesday 09 February 2021 00:00:00" CustomProperties(document).update_all() - assert paragraph.text == 'Datum: Friday 11 March 2022 10:00:0' + assert paragraph.text == "Datum: Friday 11 March 2022 10:00:0" class TestUpdateSpecificDocproperty(object): def test_simple_field_gets_updated(self): - document = Document(docx_path('outdated_docproperty_with_umlauts.docx')) - assert_simple_field_value( - u'xxx', document.element.body, u"F\xfc\xfc") + document = Document(docx_path("outdated_docproperty_with_umlauts.docx")) + assert_simple_field_value("xxx", document.element.body, "F\xfc\xfc") - CustomProperties(document).update(u"F\xfc\xfc", u"new v\xe4lue") + CustomProperties(document).update("F\xfc\xfc", "new v\xe4lue") - assert_simple_field_value( - u"new v\xe4lue", document.element.body, u"F\xfc\xfc") + assert_simple_field_value("new v\xe4lue", document.element.body, "F\xfc\xfc") def test_complex_field_gets_updated(self): - document = Document(docx_path('docproperties.docx')) - assert len(document.paragraphs) == 6, 'input file should contain 6 paragraphs' - - properties = xpath(document.element.body, './/w:instrText') - assert len(properties) == 5,\ - 'input should contain five complex field docproperties' - - expected_paragraphs = [u'Custom Doc Properties', - u'Text: Foo Bar', - u'Number: 123', - u'Boolean: Y', - u'Date: 11.06.2019', - u'Float: 1.1'] + document = Document(docx_path("docproperties.docx")) + assert len(document.paragraphs) == 6, "input file should contain 6 paragraphs" + + properties = xpath(document.element.body, ".//w:instrText") + assert ( + len(properties) == 5 + ), "input should contain five complex field docproperties" + + expected_paragraphs = [ + "Custom Doc Properties", + "Text: Foo Bar", + "Number: 123", + "Boolean: Y", + "Date: 11.06.2019", + "Float: 1.1", + ] actual_paragraphs = [paragraph.text for paragraph in document.paragraphs] assert actual_paragraphs == expected_paragraphs CustomProperties(document).update("Number Property", 423) - expected_paragraphs[2] = u'Number: 423' + expected_paragraphs[2] = "Number: 423" actual_paragraphs = [paragraph.text for paragraph in document.paragraphs] assert actual_paragraphs == expected_paragraphs def test_multiple_identical_docprops_get_updated(self): - document = Document(docx_path('multiple_identical_properties.docx')) - assert len(document.paragraphs) == 3, 'input file should contain 3 paragraphs' + document = Document(docx_path("multiple_identical_properties.docx")) + assert len(document.paragraphs) == 3, "input file should contain 3 paragraphs" for paragraph in document.paragraphs: - assert len(xpath(paragraph._p, './/w:instrText')) == 1, \ - 'paragraph should contain one complex field docproperties' + assert ( + len(xpath(paragraph._p, ".//w:instrText")) == 1 + ), "paragraph should contain one complex field docproperties" - assert paragraph.text == u'Foo' + assert paragraph.text == "Foo" CustomProperties(document).update("Text Property", "New value") for i, paragraph in enumerate(document.paragraphs): - assert paragraph.text == u'New value',\ - 'docprop {} was not updated'.format(i+1) + assert paragraph.text == "New value", "docprop {} was not updated".format( + i + 1 + ) class TestDissolveField(object): def test_removes_simple_field_but_keeps_value(self): - document = Document(docx_path('outdated_docproperty_with_umlauts.docx')) - assert len(document.paragraphs) == 1, 'input file should contain 1 paragraph' - fields = xpath( - document.element.body, - simple_field_expression(u"F\xfc\xfc")) - assert len(fields) == 1, 'should contain one simple field docproperty' - - assert document.paragraphs[0].text == u'Hie chund ds property: ' - assert fields[0].text == u'xxx' - - CustomProperties(document).dissolve_fields(u"F\xfc\xfc") - fields = xpath( - document.element.body, - simple_field_expression(u"F\xfc\xfc")) - assert len(fields) == 0, 'should not contain any docproperties anymore' + document = Document(docx_path("outdated_docproperty_with_umlauts.docx")) + assert len(document.paragraphs) == 1, "input file should contain 1 paragraph" + fields = xpath(document.element.body, simple_field_expression("F\xfc\xfc")) + assert len(fields) == 1, "should contain one simple field docproperty" + + assert document.paragraphs[0].text == "Hie chund ds property: " + assert fields[0].text == "xxx" + + CustomProperties(document).dissolve_fields("F\xfc\xfc") + fields = xpath(document.element.body, simple_field_expression("F\xfc\xfc")) + assert len(fields) == 0, "should not contain any docproperties anymore" # when simple field is removed, the value is moved one up in the hierarchy - assert document.paragraphs[0].text == u'Hie chund ds property: xxx' + assert document.paragraphs[0].text == "Hie chund ds property: xxx" def test_removes_complex_field_but_keeps_value(self): # test fails because field has 2 spaces before docprop name - document = Document(docx_path('docproperties.docx')) - assert len(document.paragraphs) == 6, 'input file should contain 6 paragraphs' - - properties = xpath(document.element.body, './/w:instrText') - assert len(properties) == 5,\ - 'input should contain five complex field docproperties' - - expected_paragraphs = [u'Custom Doc Properties', - u'Text: Foo Bar', - u'Number: 123', - u'Boolean: Y', - u'Date: 11.06.2019', - u'Float: 1.1'] + document = Document(docx_path("docproperties.docx")) + assert len(document.paragraphs) == 6, "input file should contain 6 paragraphs" + + properties = xpath(document.element.body, ".//w:instrText") + assert ( + len(properties) == 5 + ), "input should contain five complex field docproperties" + + expected_paragraphs = [ + "Custom Doc Properties", + "Text: Foo Bar", + "Number: 123", + "Boolean: Y", + "Date: 11.06.2019", + "Float: 1.1", + ] actual_paragraphs = [paragraph.text for paragraph in document.paragraphs] assert actual_paragraphs == expected_paragraphs @@ -716,216 +668,230 @@ def test_removes_complex_field_but_keeps_value(self): actual_paragraphs = [paragraph.text for paragraph in document.paragraphs] assert actual_paragraphs == expected_paragraphs - properties = xpath(document.element.body, './/w:instrText') - assert len(properties) == 4,\ - 'only 4 fields should remain after removal of one' + properties = xpath(document.element.body, ".//w:instrText") + assert len(properties) == 4, "only 4 fields should remain after removal of one" def test_dissolves_all_instances_of_given_field(self): - document = Document(docx_path('multiple_identical_properties.docx')) - assert len(document.paragraphs) == 3, 'input file should contain 3 paragraphs' - assert len(xpath(document.element.body, './/w:instrText')) == 3, \ - 'document should contain three complex field docproperties' + document = Document(docx_path("multiple_identical_properties.docx")) + assert len(document.paragraphs) == 3, "input file should contain 3 paragraphs" + assert ( + len(xpath(document.element.body, ".//w:instrText")) == 3 + ), "document should contain three complex field docproperties" for paragraph in document.paragraphs: - assert paragraph.text == u'Foo' + assert paragraph.text == "Foo" CustomProperties(document).dissolve_fields("Text Property") assert len(document.paragraphs) == 3 - assert len(xpath(document.element.body, './/w:instrText')) == 0, \ - 'document should not contain any complex field anymore' + assert ( + len(xpath(document.element.body, ".//w:instrText")) == 0 + ), "document should not contain any complex field anymore" for paragraph in document.paragraphs: - assert paragraph.text == u'Foo', "value should have been kept in document" + assert paragraph.text == "Foo", "value should have been kept in document" def test_dissolving_field_when_three_complex_docprop_in_same_paragraph(self): - document = Document(docx_path('three_props_in_same_paragraph.docx')) - assert len(document.paragraphs) == 1, 'input file should contains one paragraph' + document = Document(docx_path("three_props_in_same_paragraph.docx")) + assert len(document.paragraphs) == 1, "input file should contains one paragraph" paragraph = document.paragraphs[0] properties = CustomProperties(document) - assert len(properties.find_docprops_in_document()) == 3, \ - 'input should contain three complex field docproperties' + assert ( + len(properties.find_docprops_in_document()) == 3 + ), "input should contain three complex field docproperties" - text = u'{text} / {num} mor between the fields {text} and some afte the three fields' + text = "{text} / {num} mor between the fields {text} and some afte the three fields" assert paragraph.text == text.format(text="I was spellcecked", num=0) properties.dissolve_fields("Text Property") assert len(document.paragraphs) == 1 - assert len(properties.find_docprops_in_document()) == 1, \ - 'document should contain one complex field after removal' + assert ( + len(properties.find_docprops_in_document()) == 1 + ), "document should contain one complex field after removal" assert paragraph.text == text.format(text="I was spellcecked", num=0) def test_get_doc_properties(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - assert props['Text Property'] == 'Foo Bar' - assert props['Number Property'] == 123 - assert props['Boolean Property'] is True - assert props['Date Property'] == datetime(2019, 6, 11, 10, 0) + assert props["Text Property"] == "Foo Bar" + assert props["Number Property"] == 123 + assert props["Boolean Property"] is True + assert props["Date Property"] == datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc) - assert props.get('Text Property') == 'Foo Bar' - assert props.get('Number Property') == 123 - assert props.get('Boolean Property') is True - assert props.get('Date Property') == datetime(2019, 6, 11, 10, 0) + assert props.get("Text Property") == "Foo Bar" + assert props.get("Number Property") == 123 + assert props.get("Boolean Property") is True + assert props.get("Date Property") == datetime( + 2019, 6, 11, 10, 0, tzinfo=timezone.utc + ) def test_get_doc_property_is_case_insensitive(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - assert props['text property'] == 'Foo Bar' - assert props.get('text property') == 'Foo Bar' + assert props["text property"] == "Foo Bar" + assert props.get("text property") == "Foo Bar" def test_add_doc_properties(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - props.add('My Text Property', 'foo bar') - assert props.get('My Text Property') == 'foo bar' + props.add("My Text Property", "foo bar") + assert props.get("My Text Property") == "foo bar" - props.add('My Boolean Property', True) - assert props.get('My Boolean Property') is True + props.add("My Boolean Property", True) + assert props.get("My Boolean Property") is True - props.add('My Number Property', 123) - assert props.get('My Number Property') == 123 + props.add("My Number Property", 123) + assert props.get("My Number Property") == 123 - props.add('My Date Property', datetime(2019, 10, 23, 15, 44, 50)) - assert props.get('My Date Property') == datetime(2019, 10, 23, 15, 44, 50) + props.add( + "My Date Property", datetime(2019, 10, 23, 15, 44, 50, tzinfo=timezone.utc) + ) + assert props.get("My Date Property") == datetime( + 2019, 10, 23, 15, 44, 50, tzinfo=timezone.utc + ) def test_add_utf8_property(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - props.add('My Text Property', u'f\xfc\xfc'.encode('utf-8')) - assert props.get('My Text Property') == u'f\xfc\xfc' + props.add("My Text Property", "f\xfc\xfc".encode("utf-8")) + assert props.get("My Text Property") == "f\xfc\xfc" def test_set_doc_properties(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - props['Text Property'] = 'baz' - assert props['Text Property'] == 'baz' + props["Text Property"] = "baz" + assert props["Text Property"] == "baz" - props['Boolean Property'] = False - assert props['Boolean Property'] is False + props["Boolean Property"] = False + assert props["Boolean Property"] is False - props['Number Property'] = 456 - assert props['Number Property'] == 456 + props["Number Property"] = 456 + assert props["Number Property"] == 456 - props['Date Property'] = datetime(2019, 10, 20, 12, 0) - assert props['Date Property'] == datetime(2019, 10, 20, 12, 0) + props["Date Property"] = datetime(2019, 10, 20, 12, 0, tzinfo=timezone.utc) + assert props["Date Property"] == datetime(2019, 10, 20, 12, 0, tzinfo=timezone.utc) def test_set_doc_property_is_case_insensitive(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - props['text property'] = 'baz' - assert props['Text Property'] == 'baz' + props["text property"] = "baz" + assert props["Text Property"] == "baz" def test_delete_doc_properties(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - del props['Text Property'] - del props['Number Property'] + del props["Text Property"] + del props["Number Property"] - assert 'Text Property' not in props - assert 'Number Property' not in props + assert "Text Property" not in props + assert "Number Property" not in props - assert xpath(props._element, u'.//cp:property/@pid') == ['2', '3', '4'] + assert xpath(props._element, ".//cp:property/@pid") == ["2", "3", "4"] def test_delete_doc_property_is_case_insensitive(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - del props['text property'] + del props["text property"] - assert 'Text Property' not in props + assert "Text Property" not in props def test_contains_is_case_insensitive(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - assert 'text property' in props + assert "text property" in props def test_nullify_doc_properties(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - props.nullify('Text Property') - props.nullify('Number Property') - props.nullify('Boolean Property') - props.nullify('Date Property') - props.nullify('Float Property') + props.nullify("Text Property") + props.nullify("Number Property") + props.nullify("Boolean Property") + props.nullify("Date Property") + props.nullify("Float Property") - assert props['Text Property'] == '' - assert 'Number Property' not in props - assert 'Boolean Property' not in props - assert 'Date Property' not in props - assert 'Float Property' not in props + assert props["Text Property"] == "" + assert "Number Property" not in props + assert "Boolean Property" not in props + assert "Date Property" not in props + assert "Float Property" not in props def test_nullify_doc_property_is_case_insensitive(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) - props.nullify('text property') - assert props['Text Property'] == '' + props.nullify("text property") + assert props["Text Property"] == "" def test_set_doc_property_on_document_without_properties_creates_new_part(): - document = Document(docx_path('master.docx')) + document = Document(docx_path("master.docx")) props = CustomProperties(document) - props['Text Property'] = 'Foo' + props["Text Property"] = "Foo" assert props.part is not None - assert props['Text Property'] == 'Foo' + assert props["Text Property"] == "Foo" part = document.part.package.part_related_by(RT.CUSTOM_PROPERTIES) assert part is not None def test_doc_properties_keys(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) assert props.keys() == [ - 'Text Property', - 'Number Property', - 'Boolean Property', - 'Date Property', - 'Float Property', + "Text Property", + "Number Property", + "Boolean Property", + "Date Property", + "Float Property", ] def test_doc_properties_values(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) assert props.values() == [ - 'Foo Bar', 123, True, datetime(2019, 6, 11, 10, 0), 1.1] + "Foo Bar", + 123, + True, + datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc), + 1.1, + ] def test_doc_properties_items(): - document = Document(docx_path('docproperties.docx')) + document = Document(docx_path("docproperties.docx")) props = CustomProperties(document) assert props.items() == [ - ('Text Property', 'Foo Bar'), - ('Number Property', 123), - ('Boolean Property', True), - ('Date Property', datetime(2019, 6, 11, 10, 0)), - ('Float Property', 1.1), + ("Text Property", "Foo Bar"), + ("Number Property", 123), + ("Boolean Property", True), + ("Date Property", datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc)), + ("Float Property", 1.1), ] @@ -933,37 +899,37 @@ def test_vt2value_value2vt_roundtrip(): assert vt2value(value2vt(42)) == 42 assert vt2value(value2vt(True)) is True assert vt2value(value2vt(1.1)) == pytest.approx(1.1) - dt = datetime(2019, 6, 11, 10, 0) + dt = datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc) assert vt2value(value2vt(dt)) == dt - assert vt2value(value2vt(u'foo')) == u'foo' - assert vt2value(value2vt(u'')) == u'' + assert vt2value(value2vt("foo")) == "foo" + assert vt2value(value2vt("")) == "" - node = parse_xml(CUSTOM_PROPERTY_TYPES['int']) - node.text = '42' + node = parse_xml(CUSTOM_PROPERTY_TYPES["int"]) + node.text = "42" assert tostring(value2vt(vt2value(node))) == tostring(node) - node = parse_xml(CUSTOM_PROPERTY_TYPES['bool']) - node.text = 'true' + node = parse_xml(CUSTOM_PROPERTY_TYPES["bool"]) + node.text = "true" assert tostring(value2vt(vt2value(node))) == tostring(node) - node = parse_xml(CUSTOM_PROPERTY_TYPES['float']) - node.text = '1.1' + node = parse_xml(CUSTOM_PROPERTY_TYPES["float"]) + node.text = "1.1" assert tostring(value2vt(vt2value(node))) == tostring(node) - node = parse_xml(CUSTOM_PROPERTY_TYPES['datetime']) - node.text = '2003-12-31T10:14:55Z' + node = parse_xml(CUSTOM_PROPERTY_TYPES["datetime"]) + node.text = "2003-12-31T10:14:55Z" assert tostring(value2vt(vt2value(node))) == tostring(node) - node = parse_xml(CUSTOM_PROPERTY_TYPES['text']) - node.text = 'foo' + node = parse_xml(CUSTOM_PROPERTY_TYPES["text"]) + node.text = "foo" assert tostring(value2vt(vt2value(node))) == tostring(node) - node = parse_xml(CUSTOM_PROPERTY_TYPES['text']) - node.text = '' + node = parse_xml(CUSTOM_PROPERTY_TYPES["text"]) + node.text = "" assert tostring(value2vt(vt2value(node))) == tostring(node) def test_vt2value_returns_empty_string_for_missing_text_node(): - node = parse_xml(CUSTOM_PROPERTY_TYPES['text']) + node = parse_xml(CUSTOM_PROPERTY_TYPES["text"]) node.text = None - assert vt2value(node) == u'' + assert vt2value(node) == "" diff --git a/tests/test_section.py b/tests/test_section.py index 89c99f6..888d01a 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -4,14 +4,16 @@ def test_section_types_are_correct(): composed = ComposedDocument( - "continous_section_break.docx", "continous_section_break.docx") + "continous_section_break.docx", "continous_section_break.docx" + ) assert [s.start_type for s in composed.doc.sections] == [2, 0, 0] def test_continuous_continuous_section_break(): doc = FixtureDocument("continous_section_break.docx") composed = ComposedDocument( - "continous_section_break.docx", "continous_section_break.docx") + "continous_section_break.docx", "continous_section_break.docx" + ) assert composed == doc @@ -19,22 +21,21 @@ def test_continuous_continuous_section_break(): def test_continuous_odd_section_break(): doc = FixtureDocument("continous_odd_section_break.docx") composed = ComposedDocument( - "continous_section_break.docx", "odd_section_break.docx") + "continous_section_break.docx", "odd_section_break.docx" + ) assert composed == doc def test_next_page_section_break(): doc = FixtureDocument("next_page_section_break.docx") - composed = ComposedDocument( - "next_page_section_break.docx", "legacy_empty.docx") + composed = ComposedDocument("next_page_section_break.docx", "legacy_empty.docx") assert composed == doc def test_next_page_section_break_in_otherwise_empty_file_preseved(): doc = FixtureDocument("next_page_section_break_empty.docx") - composed = ComposedDocument( - "master.docx", "next_page_section_break_empty.docx") + composed = ComposedDocument("master.docx", "next_page_section_break_empty.docx") assert composed == doc diff --git a/tests/test_shapes.py b/tests/test_shapes.py index 35aa6de..a9a420c 100644 --- a/tests/test_shapes.py +++ b/tests/test_shapes.py @@ -4,7 +4,6 @@ def test_shapes(): doc = FixtureDocument("embedded_visio.docx") - composed = ComposedDocument( - "master.docx", "embedded_visio.docx") + composed = ComposedDocument("master.docx", "embedded_visio.docx") assert composed == doc diff --git a/tests/test_smartart.py b/tests/test_smartart.py index a3e62e0..e2730f8 100644 --- a/tests/test_smartart.py +++ b/tests/test_smartart.py @@ -4,7 +4,6 @@ def test_smartart(): doc = FixtureDocument("smart_art.docx") - composed = ComposedDocument( - "master.docx", "smart_art.docx") + composed = ComposedDocument("master.docx", "smart_art.docx") assert composed == doc diff --git a/tests/test_styles.py b/tests/test_styles.py index b62fce3..3b0060e 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -1,52 +1,51 @@ +import pytest from docx import Document -from docxcompose.composer import Composer from utils import ComposedDocument from utils import docx_path from utils import FixtureDocument -import pytest + +from docxcompose.composer import Composer def test_contains_predefined_styles_in_masters_language(merged_styles): style_ids = [s.style_id for s in merged_styles.doc.styles] - assert 'Heading1' in style_ids - assert 'Heading1' in style_ids - assert 'Strong' in style_ids - assert 'Quote' in style_ids + assert "Heading1" in style_ids + assert "Heading1" in style_ids + assert "Strong" in style_ids + assert "Quote" in style_ids def test_does_not_contain_predefined_styles_in_appended_language(merged_styles): style_ids = [s.style_id for s in merged_styles.doc.styles] - assert 'berschrift1' not in style_ids - assert 'berschrift2' not in style_ids - assert 'Fett' not in style_ids - assert 'Zitat' not in style_ids + assert "berschrift1" not in style_ids + assert "berschrift2" not in style_ids + assert "Fett" not in style_ids + assert "Zitat" not in style_ids def test_contains_custom_styles_from_both_docs(merged_styles): style_ids = [s.style_id for s in merged_styles.doc.styles] - assert 'MyStyle1' in style_ids - assert 'MyStyle1Char' in style_ids - assert 'MeineFormatvorlage' in style_ids - assert 'MeineFormatvorlageZchn' in style_ids + assert "MyStyle1" in style_ids + assert "MyStyle1Char" in style_ids + assert "MeineFormatvorlage" in style_ids + assert "MeineFormatvorlageZchn" in style_ids def test_contains_linked_styles(merged_styles): style_ids = [s.style_id for s in merged_styles.doc.styles] - assert 'QuoteChar' in style_ids + assert "QuoteChar" in style_ids def test_merged_styles_de(): doc = FixtureDocument("styles_de.docx") - composed = ComposedDocument( - "styles_de.docx", "styles_en.docx") + composed = ComposedDocument("styles_de.docx", "styles_en.docx") assert composed == doc def test_merged_styles_en(): doc = FixtureDocument("styles_en.docx") - composed = ComposedDocument( - "styles_en.docx", "styles_de.docx") + composed = ComposedDocument("styles_en.docx", "styles_de.docx") assert composed == doc @@ -54,13 +53,15 @@ def test_merged_styles_en(): def test_styles_are_not_switched_for_first_numbering_element(): doc = FixtureDocument("switched_listing_style.docx") composed = ComposedDocument( - "master_switched_listing_style.docx", "switched_listing_style.docx") + "master_switched_listing_style.docx", "switched_listing_style.docx" + ) assert composed == doc + def test_continue_when_no_styles(): """Expects not to throw a type error""" - doc = ComposedDocument("aatmay.docx", "aatmay.docx") + ComposedDocument("aatmay.docx", "aatmay.docx") @pytest.fixture diff --git a/tests/test_table.py b/tests/test_table.py index a348dc8..b324cfe 100644 --- a/tests/test_table.py +++ b/tests/test_table.py @@ -4,7 +4,6 @@ def test_table(): doc = FixtureDocument("table.docx") - composed = ComposedDocument( - "master.docx", "table.docx") + composed = ComposedDocument("master.docx", "table.docx") assert composed == doc diff --git a/tests/utils.py b/tests/utils.py index c6d8aed..f8b5aee 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,23 +1,27 @@ +import os.path +from operator import attrgetter + from docx import Document + from docxcompose.composer import Composer -from operator import attrgetter -import os.path from docxcompose.utils import xpath -XPATH_CACHED_DOCPROPERTY_VALUES = 'w:r[preceding-sibling::w:r/w:fldChar/@w:fldCharType="separate"]/w:t' +XPATH_CACHED_DOCPROPERTY_VALUES = ( + 'w:r[preceding-sibling::w:r/w:fldChar/@w:fldCharType="separate"]/w:t' +) def docx_path(filename): - return os.path.join(os.path.dirname(__file__), 'docs', filename) + return os.path.join(os.path.dirname(__file__), "docs", filename) def simple_field_expression(name): - return u'.//w:fldSimple[contains(@w:instr, \'DOCPROPERTY "{}"\')]//w:t'.format(name) + return ".//w:fldSimple[contains(@w:instr, 'DOCPROPERTY \"{}\"')]//w:t".format(name) def complex_field_expression(name): - return u'.//w:instrText[contains(.,\'DOCPROPERTY "{}"\')]'.format(name) + return ".//w:instrText[contains(.,'DOCPROPERTY \"{}\"')]".format(name) def cached_complex_field_values(element): @@ -27,17 +31,17 @@ def cached_complex_field_values(element): def assert_simple_field_value(expected, element, name): prop_elements = xpath(element, simple_field_expression(name)) - assert len(prop_elements) == 1, u'Could not find simple field "{}"'.format(name) + assert len(prop_elements) == 1, 'Could not find simple field "{}"'.format(name) actual = prop_elements[0].text - assert expected == actual, u'{} == {}'.format(expected, actual) + assert expected == actual, "{} == {}".format(expected, actual) def assert_complex_field_value(expected, element, name): prop_elements = xpath(element, complex_field_expression(name)) - assert len(prop_elements) == 1, u'Could not find complex field "{}"'.format(name) + assert len(prop_elements) == 1, 'Could not find complex field "{}"'.format(name) parent_paragraph = prop_elements[0].getparent().getparent() - actual = u''.join(cached_complex_field_values(parent_paragraph)) - assert expected == actual, u'{} == {}'.format(expected, actual) + actual = "".join(cached_complex_field_values(parent_paragraph)) + assert expected == actual, "{} == {}".format(expected, actual) class ComparableDocument(object): @@ -53,8 +57,7 @@ def __init__(self, doc): self.partnames = [] return - self.parts = sorted( - self.doc.part.package.parts, key=attrgetter('partname')) + self.parts = sorted(self.doc.part.package.parts, key=attrgetter("partname")) self.partnames = sorted(p.partname for p in self.parts) def __eq__(self, other): @@ -84,7 +87,7 @@ class FixtureDocument(ComparableDocument): def __init__(self, composed_filename): self.composed_filename = composed_filename - path = docx_path(os.path.join('composed_fixture', composed_filename)) + path = docx_path(os.path.join("composed_fixture", composed_filename)) doc = Document(path) if os.path.isfile(path) else None super(FixtureDocument, self).__init__(doc) @@ -98,6 +101,7 @@ class ComposedDocument(ComparableDocument): document from the fixture and the assertion failed. """ + def __init__(self, master_filename, filename, *filenames): composer = Composer(Document(docx_path(master_filename))) for filename in (filename,) + filenames: @@ -111,6 +115,5 @@ def post_compare_failed(self, other): """ if isinstance(other, FixtureDocument): - path = docx_path( - os.path.join('composed_debug', other.composed_filename)) + path = docx_path(os.path.join("composed_debug", other.composed_filename)) self.doc.save(path) diff --git a/towncrier.md b/towncrier.md new file mode 100644 index 0000000..db087f1 --- /dev/null +++ b/towncrier.md @@ -0,0 +1,35 @@ + +{{ versiondata.version }} ({{ versiondata.date }}) +{{ top_underline * ((versiondata.version + versiondata.date)|length + 3)}} +{% for section, _ in sections.items() %} +{% set underline = underlines[0] %}{% if section %}{{section}} +{{ underline * section|length }}{% set underline = underlines[1] %} + +{% endif %} + +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section]%} +{{ definitions[category]['name'] }} +{{ underline * definitions[category]['name']|length }} +{% if definitions[category]['showcontent'] %} +{% for text in sections[section][category].keys()|sort %} +- {{ text }} +{% endfor %} + +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% if sections[section][category]|length == 0 %} +No changes. + +{% else %} +{% endif %} + +{% endfor %} +{% else %} +No changes. + + +{% endif %} +{% endfor %} diff --git a/tox.ini b/tox.ini deleted file mode 100644 index bc93ad0..0000000 --- a/tox.ini +++ /dev/null @@ -1,6 +0,0 @@ -[tox] -envlist = py27, py36 - -[testenv] -deps = .[tests] -commands = pytest diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..bf1ec78 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2.0.0.dev0