diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 79c5b58..383ab1e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,7 +52,7 @@ jobs: max-parallel: 1 matrix: python-version: [ "3.11", "3.12" ] - django-version: [ "3.2", "4.2", "5.0" ] + django-version: [ "4.2", "5.1" ] fail-fast: true needs: [ changes ] if: needs.changes.outputs.run_tests || needs.changes.outputs.lint @@ -66,40 +66,47 @@ jobs: python-version: ${{ matrix.python-version }} architecture: 'x64' - - name: Cache virtualenv - uses: actions/cache@v3 + - name: Restore cached venv + id: cache-venv-restore + uses: actions/cache/restore@v4 with: - key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('pdm.lock') }} - path: .venv + path: | + .cache-uv/ + .venv/ + key: ${{ matrix.python-version }}-${{matrix.django-version}}-${{ hashFiles('pyproject.toml') }}-venv + + - uses: yezz123/setup-uv@v4 + with: + python: ${{ matrix.python-version }} - name: Test continue-on-error: true run: | - pip install pdm - pdm venv create -q - pdm install - - - name: lint - if: needs.changes.outputs.lint - run: | - pdm run isort src/ --check-only - pdm run flake8 src/ + uv export -q --no-hashes -o {work_dir}/requirements.txt + pip install -r {work_dir}/requirements.txt + pip install '{env:DJANGO}' + pytest tests --create-db --cov --junit-xml junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml - - name: Test - if: needs.changes.outputs.run_tests - run: | - pdm run pytest tests --create-db --junit-xml junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml + - name: Cache venv + if: steps.cache-venv-restore.outputs.cache-hit != 'true' + id: cache-venv-save + uses: actions/cache/save@v4 + with: + path: | + .cache-uv/ + .venv/ + key: ${{ matrix.python-version }}-${{matrix.django-version}}-${{ hashFiles('pyproject.toml') }}-venv - name: Upload pytest test results uses: actions/upload-artifact@v4 with: name: pytest-results-${{ matrix.python-version }}-${{matrix.django-version}} path: junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml - if: ${{ always() }} + if: matrix.python-version == 3.12 - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 - if: ${{ always() }} + if: matrix.python-version == 3.12 continue-on-error: true with: env_vars: OS,PYTHON diff --git a/.gitignore b/.gitignore index ef8ef44..d3f576e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ dist/ build/ coverage.xml *.sqlite3 +Makefile +uv.lock !.git !.github !.flake8 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ce6e48b..16bfec4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,10 +16,39 @@ repos: hooks: - id: flake8 args: [--config=.flake8] + additional_dependencies: [flake8-bugbear==22.9.23] stages: [ commit ] - repo: https://github.com/PyCQA/bandit - rev: '1.7.8' # Update me! + rev: '1.7.9' # Update me! hooks: - id: bandit args: ["-c", "bandit.yaml"] + - repo: https://github.com/twisted/towncrier + rev: 23.11.0 + hooks: + - id: towncrier-check + + - repo: https://github.com/saxix/pch + rev: '0.1' + hooks: + - id: check-missed-migrations + args: + - src + stages: [ commit ] + additional_dependencies: [ setuptools ] + + - id: check-untracked + args: + - src + - tests + stages: [ push ] + + - id: check-forbidden + args: + - -p + - /\.showbrowser\(/ + - -p + - /print\(111/ + stages: [ commit ] + additional_dependencies: [ setuptools ] diff --git a/README.md b/README.md index c67ea97..980d0da 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,49 @@ -# HOPR FlexFields +# HOPE FlexFields [![Test](https://github.com/unicef/hope-flex-fields/actions/workflows/test.yml/badge.svg)](https://github.com/unicef/hope-flex-fields/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/unicef/hope-flex-fields/graph/badge.svg?token=GSYAH4IEUK)](https://codecov.io/gh/unicef/hope-flex-fields) +This library provides the ability to define a set of fields and related validation rules dynamically. It has been designed as part of the [HOPE](https://github.com/unicef/hct-mis) project to manage user-customizable fields (FlexField). The idea is to have a central business logic repository for data import validation. + + +It provides four classes: + +- FieldDefinition: This represents a collection of reusable pre-configured fields +- FlexField: Instance like representation of `FieldDefinition` inside a `Fieldset` +- Fieldset: Group of FlexField +- DataChecker: Compound of fieldset + +From the design point of view a high level comparison with Django components could be: + +- `FieldDefinition` = `class forms.Field` +- `Fieldset` = `forms.Form` +- `FlexField` = `forms.Field()` +- `DataChecker` = `[forms.Form(),...]` + +... and some utilities + +- Automatic creation of FieldSets inspecting [exiting models](http://localhost:8000/hope_flex_fields/fieldset/create_from_content_type/?) +- Automatic creation of XLS file matching an existing [Datachecker](http://localhost:8000/hope_flex_fields/datachecker/) +- Validate XLS against an existing [Datachecker](http://localhost:8000/hope_flex_fields/datachecker/) + + +```mermaid + +classDiagram + class AbstractField + class FieldDefinition + class FlexField + class Fieldset + class DataChecker + AbstractField <|-- FlexField + AbstractField <|-- FieldDefinition + Fieldset *-- FlexField + FlexField --> FieldDefinition + DataChecker o-- Fieldset + +``` + + ## Install CSP_SCRIPT_SRC = [ ... diff --git a/docs/_theme/css/style.css b/docs/_theme/css/style.css new file mode 100644 index 0000000..b29ac33 --- /dev/null +++ b/docs/_theme/css/style.css @@ -0,0 +1,15 @@ +.align-center { + align-content: center; + text-align: center; + width: 100%; +} + +.md-typeset__table { + width: 100%; +} + +.md-typeset__table table:not([class]) { + display: table; +} + +/*# sourceMappingURL=style.css.map */ diff --git a/docs/_theme/css/style.css.map b/docs/_theme/css/style.css.map new file mode 100644 index 0000000..594fe81 --- /dev/null +++ b/docs/_theme/css/style.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["style.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA;;;AAEF;EACE;;;AAGF;EACE","file":"style.css"} \ No newline at end of file diff --git a/docs/_theme/css/style.scss b/docs/_theme/css/style.scss new file mode 100644 index 0000000..ae341fe --- /dev/null +++ b/docs/_theme/css/style.scss @@ -0,0 +1,12 @@ +.align-center{ + align-content: center; + text-align: center; + width: 100%; +} +.md-typeset__table { + width: 100%; +} + +.md-typeset__table table:not([class]) { + display: table +} diff --git a/docs/_theme/js/address.js b/docs/_theme/js/address.js new file mode 100644 index 0000000..58e9652 --- /dev/null +++ b/docs/_theme/js/address.js @@ -0,0 +1,50 @@ +const clickHandler = function () { + let currentAddr = Cookies.get('address') || "https://127.0.0.1/"; + let addr = prompt("Set your HOPE server address", currentAddr); + Cookies.set('address', addr, currentAddr); + location.reload(); +}; +const setAddress = function () { + let cookieAddr = Cookies.get('address'); + if (!cookieAddr) { + cookieAddr = "[SERVER_ADDRESS]" + } + for (const cell of document.getElementsByTagName('code')) { + cell.innerHTML = cell.innerHTML.replace('[SERVER_ADDRESS]', cookieAddr); + } +}; +// addEventListener('click', function(e) { +// setTimeout(setAddress, 500); +// }) +addEventListener('load', function (e) { + setAddress(); + let btn = document.getElementById("set-address"); + if (btn) { + btn.addEventListener('click', clickHandler); + } +}); + +var open = window.XMLHttpRequest.prototype.open, + send = window.XMLHttpRequest.prototype.send, onReadyStateChange; + +function sendReplacement(data) { + console.warn('Sending HTTP request data : ', data); + + if (this.onreadystatechange) { + this._onreadystatechange = this.onreadystatechange; + } + this.onreadystatechange = onReadyStateChangeReplacement; + return send.apply(this, arguments); +} + +function onReadyStateChangeReplacement() { + if (this.readyState === XMLHttpRequest.DONE) { + setTimeout(setAddress, 100); + } + if (this._onreadystatechange) { + return this._onreadystatechange.apply(this, arguments); + } + +} + +window.XMLHttpRequest.prototype.send = sendReplacement; diff --git a/docs/_theme/js/address.min.js b/docs/_theme/js/address.min.js new file mode 100644 index 0000000..54e3462 --- /dev/null +++ b/docs/_theme/js/address.min.js @@ -0,0 +1 @@ +const clickHandler=function(){let currentAddr=Cookies.get("address")||"https://127.0.0.1/";let addr=prompt("Set your HOPE server address",currentAddr);Cookies.set("address",addr,currentAddr);location.reload()};const setAddress=function(){let cookieAddr=Cookies.get("address");if(!cookieAddr){cookieAddr="[SERVER_ADDRESS]"}for(const cell of document.getElementsByTagName("code")){cell.innerHTML=cell.innerHTML.replace("[SERVER_ADDRESS]",cookieAddr)}};addEventListener("load",function(e){setAddress();let btn=document.getElementById("set-address");if(btn){btn.addEventListener("click",clickHandler)}});var open=window.XMLHttpRequest.prototype.open,send=window.XMLHttpRequest.prototype.send,onReadyStateChange;function sendReplacement(data){console.warn("Sending HTTP request data : ",data);if(this.onreadystatechange){this._onreadystatechange=this.onreadystatechange}this.onreadystatechange=onReadyStateChangeReplacement;return send.apply(this,arguments)}function onReadyStateChangeReplacement(){if(this.readyState===XMLHttpRequest.DONE){setTimeout(setAddress,100)}if(this._onreadystatechange){return this._onreadystatechange.apply(this,arguments)}}window.XMLHttpRequest.prototype.send=sendReplacement; \ No newline at end of file diff --git a/docs/_theme/js/js.cookie.js b/docs/_theme/js/js.cookie.js new file mode 100644 index 0000000..eac6821 --- /dev/null +++ b/docs/_theme/js/js.cookie.js @@ -0,0 +1,59 @@ +/*! js-cookie v3.0.5 | MIT */ +!function (e, t) { + "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = "undefined" != typeof globalThis ? globalThis : e || self, function () { + var n = e.Cookies, o = e.Cookies = t(); + o.noConflict = function () { + return e.Cookies = n, o + } + }()) +}(this, (function () { + "use strict"; + + function e(e) { + for (var t = 1; t < arguments.length; t++) { + var n = arguments[t]; + for (var o in n) e[o] = n[o] + } + return e + } + + var t = function t(n, o) { + function r(t, r, i) { + if ("undefined" != typeof document) { + "number" == typeof (i = e({}, o, i)).expires && (i.expires = new Date(Date.now() + 864e5 * i.expires)), i.expires && (i.expires = i.expires.toUTCString()), t = encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent).replace(/[()]/g, escape); + var c = ""; + for (var u in i) i[u] && (c += "; " + u, !0 !== i[u] && (c += "=" + i[u].split(";")[0])); + return document.cookie = t + "=" + n.write(r, t) + c + } + } + + return Object.create({ + set: r, get: function (e) { + if ("undefined" != typeof document && (!arguments.length || e)) { + for (var t = document.cookie ? document.cookie.split("; ") : [], o = {}, r = 0; r < t.length; r++) { + var i = t[r].split("="), c = i.slice(1).join("="); + try { + var u = decodeURIComponent(i[0]); + if (o[u] = n.read(c, u), e === u) break + } catch (e) { + } + } + return e ? o[e] : o + } + }, remove: function (t, n) { + r(t, "", e({}, n, {expires: -1})) + }, withAttributes: function (n) { + return t(this.converter, e({}, this.attributes, n)) + }, withConverter: function (n) { + return t(e({}, this.converter, n), this.attributes) + } + }, {attributes: {value: Object.freeze(o)}, converter: {value: Object.freeze(n)}}) + }({ + read: function (e) { + return '"' === e[0] && (e = e.slice(1, -1)), e.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent) + }, write: function (e) { + return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent) + } + }, {path: "/"}); + return t +})); diff --git a/docs/_theme/js/js.cookie.min.js b/docs/_theme/js/js.cookie.min.js new file mode 100644 index 0000000..ba3afce --- /dev/null +++ b/docs/_theme/js/js.cookie.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,function(){"use strict";function e(e){for(var t=1;tglossary / {{ page.title }} + {{ super() }} +{%- endblock %} diff --git a/docs/src/.gitignore b/docs/src/.gitignore new file mode 100644 index 0000000..65f559b --- /dev/null +++ b/docs/src/.gitignore @@ -0,0 +1,3 @@ +!**/.pages +!.includes +_theme/.templates diff --git a/docs/src/.pages b/docs/src/.pages new file mode 100644 index 0000000..ee43026 --- /dev/null +++ b/docs/src/.pages @@ -0,0 +1,6 @@ +nav: + - Home: index.md + - install.md + - contributing.md + - reference.md + - examples.md diff --git a/docs/src/best_practices.md b/docs/src/best_practices.md new file mode 100644 index 0000000..39953c2 --- /dev/null +++ b/docs/src/best_practices.md @@ -0,0 +1,44 @@ +# Best Practices + + +!!! note "General best practices" + + - Always return any "readable" value (es. True) + - Uses [Persistent revokes](https://docs.celeryq.dev/en/stable/userguide/workers.html#worker-persistent-revokes) + + +## Display progress + +Inform what is happening inside your task + + @celery.task(bind=True) + def long_task(self)-> bool: + record = 1 + + for entry in Model.objects.all(): + self.update_state(state='PROGRESS', meta={'current': record, 'entry': str(entry)}) + record += 1 + return True + + +## Sentry Integration + +In case you use [Sentry](https://sentry.io/), add some useful information + + from functools import wraps + + def sentry_tags(func: Callable) -> Callable: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + with configure_scope() as scope: + scope.set_tag("celery", True) + scope.set_tag("celery_task", func.__name__) + return func(*args, **kwargs) + + return wrapper + + @celery.task(bind=True) + @sentry_tags + def task(self) -> bool: + ... + return True diff --git a/docs/src/contributing.md b/docs/src/contributing.md new file mode 100644 index 0000000..8aa6980 --- /dev/null +++ b/docs/src/contributing.md @@ -0,0 +1,23 @@ +# Contributing + + +Install [uv](https://docs.astral.sh/uv/) + + + git clone .. + uv venv .venv --python 3.12 + source .venv/bin/activate + uv sync --extra docs + + +## Run tests + pytests tests + + +## Demo Application + + python manage.py migrate + python manage.py demo + python manage.py runserver + +Navigate to http://localhost:8000/admin/ and login using any username/password diff --git a/docs/src/examples.md b/docs/src/examples.md new file mode 100644 index 0000000..0b00f2b --- /dev/null +++ b/docs/src/examples.md @@ -0,0 +1,51 @@ +# Examples + +## Validate json + +Let imagine a simple datastructure like: + + + data = [ + {"name" : "John", "last_name": "Doe", "gender": "M"}, + {"name" : "Jane", "last_name": "Doe", "gender": "F"}, + {"name" : "Andrea", "last_name": "Doe", "gender": "X"}, + {"name" : "Mary", "last_name": "Doe", "gender": "1"} + ] + + +Let start creating validation rules (here in the code, you can use the admin interface otherwise) + + + fs, __ = Fieldset.objects.get_or_create(name="test.xlsx") + + charfield = FieldDefinition.objects.get(field_type=forms.CharField) + choicefield = FieldDefinition.objects.get(field_type=forms.ChoiceField) + + FlexField.objects.get_or_create(name="name", fieldset=fs, field=charfield) + FlexField.objects.get_or_create(name="last_name", fieldset=fs, field=charfield) + FlexField.objects.get_or_create(name="gender", fieldset=fs, field=choicefield, + attrs={"choices": [["M", "M"], ["F", "F"], ["X", "X"]}) + +Validate the file against it + + errors = fs(data) + print(errors) + +```python +{4: {'gender': ['Select a valid choice. 1 is not one of the available choices.']}} + +``` + +## Detect unknown data + +With the example above just uses + + errors = fs.validate(data, fail_if_alien=True) + print(errors) + +```python +{ + 1: {'-': ["Alien values found {'unknown'}"]}, + 4: {'gender': ['Select a valid choice. 1 is not one of the available choices.']} +} +``` diff --git a/docs/src/img/Screenshot 2024-04-14 at 10.25.01.png b/docs/src/img/Screenshot 2024-04-14 at 10.25.01.png new file mode 100644 index 0000000..de26638 Binary files /dev/null and b/docs/src/img/Screenshot 2024-04-14 at 10.25.01.png differ diff --git a/docs/src/img/favicon.ico b/docs/src/img/favicon.ico new file mode 100644 index 0000000..27ff3f1 Binary files /dev/null and b/docs/src/img/favicon.ico differ diff --git a/docs/src/img/logo.png b/docs/src/img/logo.png new file mode 100644 index 0000000..c799e94 Binary files /dev/null and b/docs/src/img/logo.png differ diff --git a/docs/src/img/logo2.png b/docs/src/img/logo2.png new file mode 100644 index 0000000..0b88a18 Binary files /dev/null and b/docs/src/img/logo2.png differ diff --git a/docs/src/img/name.png b/docs/src/img/name.png new file mode 100644 index 0000000..e9c8dfa Binary files /dev/null and b/docs/src/img/name.png differ diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..7ef8b94 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,13 @@ +--- +title: Documentation +--- + +hope-flex-fields provides the ability to define a set of fields and related validation rules dynamically. It has been designed as part of the [HOPE](https://github.com/unicef/hct-mis) project to manage user-customizable fields (FlexField). The idea is to have a central business logic repository for data import validation. + + +It provides four classes: + +- [FieldDefinition][hope_flex_fields.models.DataChecker]: This represents a collection of reusable pre-configured fields +- [FlexField][hope_flex_fields.models.FlexField]: Instance like representation of `FieldDefinition` inside a `Fieldset` +- [Fieldset][hope_flex_fields.models.Fieldset]: Group of FlexField +- [DataChecker][hope_flex_fields.models.DataChecker]: Compound of fieldset diff --git a/docs/src/install.md b/docs/src/install.md new file mode 100644 index 0000000..d620e75 --- /dev/null +++ b/docs/src/install.md @@ -0,0 +1,27 @@ +--- +title: Install +--- + + +## Install + + pip install hope-flex-fields + + +## Configure + +in your `settings.py` + + + CSP_SCRIPT_SRC = [ + ... + "cdnjs.cloudflare.com", + ] + + INSTALLED_APPS = [ + ... + 'admin_extra_buttons', + 'jsoneditor', + 'hope_flex_fields', + + ] diff --git a/docs/src/reference.md b/docs/src/reference.md new file mode 100644 index 0000000..9538efd --- /dev/null +++ b/docs/src/reference.md @@ -0,0 +1,29 @@ +# Reference + +## ::: hope_flex_fields.models.FieldDefinition + options: + show_bases: false + show_bases: false + show_root_heading: true + show_source: true + +## ::: hope_flex_fields.models.Fieldset + options: + show_bases: false + show_source: false + show_root_heading: true + + +## ::: hope_flex_fields.models.FlexField + options: + show_bases: false + show_root_heading: true + show_source: true + + +## :::hope_flex_fields.models.DataChecker + options: + show_bases: false + show_root_heading: true + show_root_toc_entry: true + show_source: true diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..373d6ca --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,69 @@ +copyright: Copyright © 2020-2024 UNICEF. +dev_addr: 127.0.0.1:8001 +docs_dir: docs/src +edit_uri: 'blob/develop/docs/' +repo_url: https://github.com/unicef/hope-flex-fields +site_author: HOPE Team +site_description: "" +site_dir: ./~build/docs +site_name: HOPE Flex Fields +site_url: https://unicef.github.io/unicef/hope-flex-fields/ +strict: false + + +markdown_extensions: + - admonition + +theme: + name: "material" + color_mode: auto + custom_dir: docs/_theme/overrides + favicon: img/favicon.ico + logo: img/logo.png +# highlightjs: true +# hljs_languages: +# - yaml +# - django + user_color_mode_toggle: true + features: + - content.action.edit + - content.code.annotate + - content.code.copy + - content.tooltips + - header.autohidex + - navigation.footer + - navigation.indexes + - navigation.instant + - navigation.instant.prefetch + - navigation.instant.progress + extra: + version: + provider: mike + alias: true + palette: + # Palette toggle for light mode + - scheme: default + primary: light blue + media: "(prefers-color-scheme: light)" + toggle: + icon: material/weather-sunny + name: Switch to dark mode + + # Palette toggle for dark mode + - scheme: slate + primary: light blue + media: "(prefers-color-scheme: dark)" + toggle: + icon: material/weather-night + name: Switch to light mode + +plugins: + - mkdocstrings: + default_handler: python + - awesome-pages + - search + - autorefs + +watch: + - docs/ + - src/ diff --git a/pdm.lock b/pdm.lock deleted file mode 100644 index 832548c..0000000 --- a/pdm.lock +++ /dev/null @@ -1,1698 +0,0 @@ -# This file is @generated by PDM. -# It is not intended for manual editing. - -[metadata] -groups = ["default", "dev"] -strategy = ["cross_platform", "inherit_metadata"] -lock_version = "4.4.2" -content_hash = "sha256:9d962014792bbbe8fcfee5a339c8b23f02afbe148c4891f5a4843736b163e74a" - -[[package]] -name = "annotated-types" -version = "0.7.0" -requires_python = ">=3.8" -summary = "Reusable constraint types to use with typing.Annotated" -groups = ["dev"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.4.0" -requires_python = ">=3.8" -summary = "High level compatibility layer for multiple asynchronous event loop implementations" -groups = ["dev"] -dependencies = [ - "idna>=2.8", - "sniffio>=1.1", -] -files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, -] - -[[package]] -name = "asgiref" -version = "3.8.1" -requires_python = ">=3.8" -summary = "ASGI specs, helper code, and adapters" -groups = ["default", "dev"] -files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, -] - -[[package]] -name = "attrs" -version = "23.2.0" -requires_python = ">=3.7" -summary = "Classes Without Boilerplate" -groups = ["dev"] -files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, -] - -[[package]] -name = "bandit" -version = "1.7.9" -requires_python = ">=3.8" -summary = "Security oriented static analyser for python code." -groups = ["dev"] -dependencies = [ - "PyYAML>=5.3.1", - "colorama>=0.3.9; platform_system == \"Windows\"", - "rich", - "stevedore>=1.20.0", -] -files = [ - {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, - {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.12.3" -requires_python = ">=3.6.0" -summary = "Screen-scraping library" -groups = ["dev"] -dependencies = [ - "soupsieve>1.2", -] -files = [ - {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, - {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, -] - -[[package]] -name = "black" -version = "24.4.2" -requires_python = ">=3.8" -summary = "The uncompromising code formatter." -groups = ["dev"] -dependencies = [ - "click>=8.0.0", - "mypy-extensions>=0.4.3", - "packaging>=22.0", - "pathspec>=0.9.0", - "platformdirs>=2", -] -files = [ - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, -] - -[[package]] -name = "blinker" -version = "1.8.2" -requires_python = ">=3.8" -summary = "Fast, simple object-to-object and broadcast signaling" -groups = ["dev"] -files = [ - {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, - {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, -] - -[[package]] -name = "cachetools" -version = "5.3.3" -requires_python = ">=3.7" -summary = "Extensible memoizing collections and decorators" -groups = ["dev"] -files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, -] - -[[package]] -name = "certifi" -version = "2024.7.4" -requires_python = ">=3.6" -summary = "Python package for providing Mozilla's CA Bundle." -groups = ["default", "dev"] -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -requires_python = ">=3.8" -summary = "Validate configuration and produce human readable error messages." -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "chardet" -version = "5.2.0" -requires_python = ">=3.7" -summary = "Universal encoding detector for Python 3" -groups = ["dev"] -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -requires_python = ">=3.7.0" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -groups = ["default", "dev"] -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "click" -version = "8.1.7" -requires_python = ">=3.7" -summary = "Composable command line interface toolkit" -groups = ["dev"] -dependencies = [ - "colorama; platform_system == \"Windows\"", -] -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Cross-platform colored terminal text." -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 = "coverage" -version = "7.6.0" -requires_python = ">=3.8" -summary = "Code coverage measurement for Python" -groups = ["dev"] -files = [ - {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, - {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, - {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, - {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, - {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, - {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, - {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, -] - -[[package]] -name = "coverage" -version = "7.6.0" -extras = ["toml"] -requires_python = ">=3.8" -summary = "Code coverage measurement for Python" -groups = ["dev"] -dependencies = [ - "coverage==7.6.0", -] -files = [ - {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, - {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, - {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, - {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, - {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, - {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, - {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, -] - -[[package]] -name = "dep-logic" -version = "0.4.2" -requires_python = ">=3.8" -summary = "Python dependency specifications supporting logical operations" -groups = ["dev"] -dependencies = [ - "packaging>=22", -] -files = [ - {file = "dep_logic-0.4.2-py3-none-any.whl", hash = "sha256:37a668add3f66a13e8a2f6511fac871ce3cc40b01c7fa4b1db23eca626b3549a"}, - {file = "dep_logic-0.4.2.tar.gz", hash = "sha256:c2f6e938ec30788952ee3e0c51da90d043e0354460c96b4fa608ac43a5ce566f"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -summary = "Distribution utilities" -groups = ["dev"] -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "django" -version = "5.0.7" -requires_python = ">=3.10" -summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." -groups = ["default", "dev"] -dependencies = [ - "asgiref<4,>=3.7.0", - "sqlparse>=0.3.1", - "tzdata; sys_platform == \"win32\"", -] -files = [ - {file = "Django-5.0.7-py3-none-any.whl", hash = "sha256:f216510ace3de5de01329463a315a629f33480e893a9024fc93d8c32c22913da"}, - {file = "Django-5.0.7.tar.gz", hash = "sha256:bd4505cae0b9bd642313e8fb71810893df5dc2ffcacaa67a33af2d5cd61888f2"}, -] - -[[package]] -name = "django-admin-extra-buttons" -version = "1.5.8" -summary = "Django mixin to easily add buttons to any ModelAdmin" -groups = ["default"] -files = [ - {file = "django_admin_extra_buttons-1.5.8.tar.gz", hash = "sha256:48dc9d470ade3f5f29c80753af4d367c08947d02c9a5757aa7a9223eda6812c5"}, -] - -[[package]] -name = "django-adminactions" -version = "2.3.0" -summary = "Collections of useful actions to use with django.contrib.admin.ModelAdmin" -groups = ["default"] -dependencies = [ - "pytz", - "xlrd>=0.9.2", - "xlwt", -] -files = [ - {file = "django-adminactions-2.3.0.tar.gz", hash = "sha256:461533a8a47f2c260b0669ee14eb1fdf345d066348c8ff573a65ca0e102d81c9"}, -] - -[[package]] -name = "django-csp" -version = "3.8" -summary = "Django Content Security Policy support." -groups = ["dev"] -dependencies = [ - "Django>=3.2", -] -files = [ - {file = "django_csp-3.8-py3-none-any.whl", hash = "sha256:19b2978b03fcd73517d7d67acbc04fbbcaec0facc3e83baa502965892d1e0719"}, - {file = "django_csp-3.8.tar.gz", hash = "sha256:ef0f1a9f7d8da68ae6e169c02e9ac661c0ecf04db70e0d1d85640512a68471c0"}, -] - -[[package]] -name = "django-factory-boy" -version = "1.0.0" -summary = "Uses factory_boy to supply test data factory classes for all stock Django models." -groups = ["dev"] -dependencies = [ - "Django>=1.6", - "factory-boy>=2.6.0", -] -files = [ - {file = "django-factory_boy-1.0.0.tar.gz", hash = "sha256:f1e08a8f6f11f59417f0e7d7cde1f2290ea16b94209cb89b72158c651f8d6d7f"}, - {file = "django_factory_boy-1.0.0-py2.py3-none-any.whl", hash = "sha256:354a8a0d44509efd1067ccaab1139b11ab646734ab0b32a0c2d5339ddafd4847"}, -] - -[[package]] -name = "django-jsoneditor" -version = "0.2.4" -summary = "Django JSON Editor" -groups = ["default"] -dependencies = [ - "packaging", -] -files = [ - {file = "django-jsoneditor-0.2.4.tar.gz", hash = "sha256:1d3dfca28f047feefa6ebc6f9541179eb815fb459b006faf3fb8d0fb2197d2df"}, - {file = "django_jsoneditor-0.2.4-py2.py3-none-any.whl", hash = "sha256:d7a639a7251e376126b5be64ea588c925c7a40d45e0e212f66ef475d2f0f90bb"}, -] - -[[package]] -name = "django-regex" -version = "0.5.0" -summary = "Fields and utilities to work with regular expression in Django" -groups = ["default"] -files = [ - {file = "django-regex-0.5.0.tar.gz", hash = "sha256:6af1add11ae5232f133a42754c9291f9113996b1294b048305d9f1a427bca27c"}, -] - -[[package]] -name = "django-regex-field" -version = "3.1.0" -summary = "Django Regex Field" -groups = ["default"] -dependencies = [ - "Django>=3.2", -] -files = [ - {file = "django-regex-field-3.1.0.tar.gz", hash = "sha256:e17e7296e8d8c6fb68f30342eb6c88ec0336d811a22f7f180717f71e6c1e3fd6"}, - {file = "django_regex_field-3.1.0-py2.py3-none-any.whl", hash = "sha256:8d4f5c8dd60f187cf45e11ea01011b86fc18bc15ff31de91a7fd438e2b1037a2"}, -] - -[[package]] -name = "django-strategy-field" -version = "3.1.0" -summary = "" -groups = ["default"] -dependencies = [ - "pytz", -] -files = [ - {file = "django-strategy-field-3.1.0.tar.gz", hash = "sha256:a355aa9b944da488644cfb7a0c7e96a155ddf241c65adb22003789df02a1c030"}, -] - -[[package]] -name = "django-webtest" -version = "1.9.11" -summary = "Instant integration of Ian Bicking's WebTest (http://docs.pylonsproject.org/projects/webtest/) with Django's testing framework." -groups = ["dev"] -dependencies = [ - "webtest>=1.3.3", -] -files = [ - {file = "django-webtest-1.9.11.tar.gz", hash = "sha256:9597d26ced599bc5d4d9366bb451469fc9707b4779f79543cdf401ae6c5aeb35"}, - {file = "django_webtest-1.9.11-py3-none-any.whl", hash = "sha256:e29baf8337e7fe7db41ce63ca6661f7b5c77fe56f506f48b305e09313f5475b4"}, -] - -[[package]] -name = "djangorestframework" -version = "3.15.2" -requires_python = ">=3.8" -summary = "Web APIs for Django, made easy." -groups = ["default"] -dependencies = [ - "django>=4.2", -] -files = [ - {file = "djangorestframework-3.15.2-py3-none-any.whl", hash = "sha256:2b8871b062ba1aefc2de01f773875441a961fefbf79f5eed1e32b2f096944b20"}, - {file = "djangorestframework-3.15.2.tar.gz", hash = "sha256:36fe88cd2d6c6bec23dca9804bab2ba5517a8bb9d8f47ebc68981b56840107ad"}, -] - -[[package]] -name = "duckdb" -version = "1.0.0" -requires_python = ">=3.7.0" -summary = "DuckDB in-process database" -groups = ["default"] -files = [ - {file = "duckdb-1.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:75586791ab2702719c284157b65ecefe12d0cca9041da474391896ddd9aa71a4"}, - {file = "duckdb-1.0.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:83bb415fc7994e641344f3489e40430ce083b78963cb1057bf714ac3a58da3ba"}, - {file = "duckdb-1.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:bee2e0b415074e84c5a2cefd91f6b5ebeb4283e7196ba4ef65175a7cef298b57"}, - {file = "duckdb-1.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa5a4110d2a499312609544ad0be61e85a5cdad90e5b6d75ad16b300bf075b90"}, - {file = "duckdb-1.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa389e6a382d4707b5f3d1bc2087895925ebb92b77e9fe3bfb23c9b98372fdc"}, - {file = "duckdb-1.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ede6f5277dd851f1a4586b0c78dc93f6c26da45e12b23ee0e88c76519cbdbe0"}, - {file = "duckdb-1.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0b88cdbc0d5c3e3d7545a341784dc6cafd90fc035f17b2f04bf1e870c68456e5"}, - {file = "duckdb-1.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd1693cdd15375156f7fff4745debc14e5c54928589f67b87fb8eace9880c370"}, - {file = "duckdb-1.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c65a7fe8a8ce21b985356ee3ec0c3d3b3b2234e288e64b4cfb03356dbe6e5583"}, - {file = "duckdb-1.0.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:e5a8eda554379b3a43b07bad00968acc14dd3e518c9fbe8f128b484cf95e3d16"}, - {file = "duckdb-1.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:a1b6acdd54c4a7b43bd7cb584975a1b2ff88ea1a31607a2b734b17960e7d3088"}, - {file = "duckdb-1.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a677bb1b6a8e7cab4a19874249d8144296e6e39dae38fce66a80f26d15e670df"}, - {file = "duckdb-1.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:752e9d412b0a2871bf615a2ede54be494c6dc289d076974eefbf3af28129c759"}, - {file = "duckdb-1.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aadb99d098c5e32d00dc09421bc63a47134a6a0de9d7cd6abf21780b678663c"}, - {file = "duckdb-1.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83b7091d4da3e9301c4f9378833f5ffe934fb1ad2b387b439ee067b2c10c8bb0"}, - {file = "duckdb-1.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:6a8058d0148b544694cb5ea331db44f6c2a00a7b03776cc4dd1470735c3d5ff7"}, - {file = "duckdb-1.0.0.tar.gz", hash = "sha256:a2a059b77bc7d5b76ae9d88e267372deff19c291048d59450c431e166233d453"}, -] - -[[package]] -name = "factory-boy" -version = "3.3.0" -requires_python = ">=3.7" -summary = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby." -groups = ["dev"] -dependencies = [ - "Faker>=0.7.0", -] -files = [ - {file = "factory_boy-3.3.0-py2.py3-none-any.whl", hash = "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c"}, - {file = "factory_boy-3.3.0.tar.gz", hash = "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1"}, -] - -[[package]] -name = "faker" -version = "26.0.0" -requires_python = ">=3.8" -summary = "Faker is a Python package that generates fake data for you." -groups = ["dev"] -dependencies = [ - "python-dateutil>=2.4", -] -files = [ - {file = "Faker-26.0.0-py3-none-any.whl", hash = "sha256:886ee28219be96949cd21ecc96c4c742ee1680e77f687b095202c8def1a08f06"}, - {file = "Faker-26.0.0.tar.gz", hash = "sha256:0f60978314973de02c00474c2ae899785a42b2cf4f41b7987e93c132a2b8a4a9"}, -] - -[[package]] -name = "fancycompleter" -version = "0.9.1" -summary = "colorful TAB completion for Python prompt" -groups = ["dev"] -dependencies = [ - "pyreadline; platform_system == \"Windows\"", - "pyrepl>=0.8.2", -] -files = [ - {file = "fancycompleter-0.9.1-py3-none-any.whl", hash = "sha256:dd076bca7d9d524cc7f25ec8f35ef95388ffef9ef46def4d3d25e9b044ad7080"}, - {file = "fancycompleter-0.9.1.tar.gz", hash = "sha256:09e0feb8ae242abdfd7ef2ba55069a46f011814a80fe5476be48f51b00247272"}, -] - -[[package]] -name = "filelock" -version = "3.15.4" -requires_python = ">=3.8" -summary = "A platform independent file lock." -groups = ["dev"] -files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, -] - -[[package]] -name = "findpython" -version = "0.6.1" -requires_python = ">=3.8" -summary = "A utility to find python versions on your system" -groups = ["dev"] -dependencies = [ - "packaging>=20", -] -files = [ - {file = "findpython-0.6.1-py3-none-any.whl", hash = "sha256:1fb4d709205de185b0561900267dfff64a841c910fe28d6038b2394ff925a81a"}, - {file = "findpython-0.6.1.tar.gz", hash = "sha256:56e52b409a92bcbd495cf981c85acf137f3b3e51cc769b46eba219bb1ab7533c"}, -] - -[[package]] -name = "flake8" -version = "7.1.0" -requires_python = ">=3.8.1" -summary = "the modular source code checker: pep8 pyflakes and co" -groups = ["dev"] -dependencies = [ - "mccabe<0.8.0,>=0.7.0", - "pycodestyle<2.13.0,>=2.12.0", - "pyflakes<3.3.0,>=3.2.0", -] -files = [ - {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, - {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, -] - -[[package]] -name = "flake8-html" -version = "0.4.3" -summary = "Generate HTML reports of flake8 violations" -groups = ["dev"] -dependencies = [ - "flake8>=3.3.0", - "jinja2>=3.1.0", - "pygments>=2.2.0", -] -files = [ - {file = "flake8-html-0.4.3.tar.gz", hash = "sha256:8b870299620cc4a06f73644a1b4d457799abeca1cc914c62ae71ec5bf65c79a5"}, - {file = "flake8_html-0.4.3-py2.py3-none-any.whl", hash = "sha256:8f126748b1b0edd6cd39e87c6192df56e2f8655b0aa2bb00ffeac8cf27be4325"}, -] - -[[package]] -name = "freezegun" -version = "1.5.1" -requires_python = ">=3.7" -summary = "Let your Python tests travel through time" -groups = ["dev"] -dependencies = [ - "python-dateutil>=2.7", -] -files = [ - {file = "freezegun-1.5.1-py3-none-any.whl", hash = "sha256:bf111d7138a8abe55ab48a71755673dbaa4ab87f4cff5634a4442dfec34c15f1"}, - {file = "freezegun-1.5.1.tar.gz", hash = "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9"}, -] - -[[package]] -name = "h11" -version = "0.14.0" -requires_python = ">=3.7" -summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -groups = ["dev"] -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "hishel" -version = "0.0.30" -requires_python = ">=3.8" -summary = "Persistent cache implementation for httpx and httpcore" -groups = ["dev"] -dependencies = [ - "httpx>=0.22.0", - "typing-extensions>=4.8.0", -] -files = [ - {file = "hishel-0.0.30-py3-none-any.whl", hash = "sha256:0c73a779a6b554b52dff75e5962057df25764fd798c31b9435ce6398b1b171c8"}, - {file = "hishel-0.0.30.tar.gz", hash = "sha256:656393ee77e9c39a0d6c527c74810e15d96e598dcb9b191f20a788608ceaca99"}, -] - -[[package]] -name = "httpcore" -version = "1.0.5" -requires_python = ">=3.8" -summary = "A minimal low-level HTTP client." -groups = ["dev"] -dependencies = [ - "certifi", - "h11<0.15,>=0.13", -] -files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, -] - -[[package]] -name = "httpx" -version = "0.27.0" -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["dev"] -dependencies = [ - "anyio", - "certifi", - "httpcore==1.*", - "idna", - "sniffio", -] -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[[package]] -name = "httpx" -version = "0.27.0" -extras = ["socks"] -requires_python = ">=3.8" -summary = "The next generation HTTP client." -groups = ["dev"] -dependencies = [ - "httpx==0.27.0", - "socksio==1.*", -] -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[[package]] -name = "identify" -version = "2.6.0" -requires_python = ">=3.8" -summary = "File identification library for Python" -groups = ["dev"] -files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, -] - -[[package]] -name = "idna" -version = "3.7" -requires_python = ">=3.5" -summary = "Internationalized Domain Names in Applications (IDNA)" -groups = ["default", "dev"] -files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -requires_python = ">=3.7" -summary = "brain-dead simple config-ini parsing" -groups = ["dev"] -files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, -] - -[[package]] -name = "installer" -version = "0.7.0" -requires_python = ">=3.7" -summary = "A library for installing Python wheels." -groups = ["dev"] -files = [ - {file = "installer-0.7.0-py3-none-any.whl", hash = "sha256:05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53"}, - {file = "installer-0.7.0.tar.gz", hash = "sha256:a26d3e3116289bb08216e0d0f7d925fcef0b0194eedfa0c944bcaaa106c4b631"}, -] - -[[package]] -name = "isort" -version = "5.13.2" -requires_python = ">=3.8.0" -summary = "A Python utility / library to sort Python imports." -groups = ["dev"] -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[[package]] -name = "jinja2" -version = "3.1.4" -requires_python = ">=3.7" -summary = "A very fast and expressive template engine." -groups = ["dev"] -dependencies = [ - "MarkupSafe>=2.0", -] -files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, -] - -[[package]] -name = "jsonpickle" -version = "3.2.2" -requires_python = ">=3.7" -summary = "Python library for serializing arbitrary object graphs into JSON" -groups = ["default"] -files = [ - {file = "jsonpickle-3.2.2-py3-none-any.whl", hash = "sha256:87cd82d237fd72c5a34970e7222dddc0accc13fddf49af84111887ed9a9445aa"}, - {file = "jsonpickle-3.2.2.tar.gz", hash = "sha256:d425fd2b8afe9f5d7d57205153403fbf897782204437882a477e8eed60930f8c"}, -] - -[[package]] -name = "markdown-it-py" -version = "3.0.0" -requires_python = ">=3.8" -summary = "Python port of markdown-it. Markdown parsing, done right!" -groups = ["dev"] -dependencies = [ - "mdurl~=0.1", -] -files = [ - {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, - {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, -] - -[[package]] -name = "markupsafe" -version = "2.1.5" -requires_python = ">=3.7" -summary = "Safely add untrusted strings to HTML/XML markup." -groups = ["dev"] -files = [ - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -requires_python = ">=3.6" -summary = "McCabe checker, plugin for flake8" -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" -requires_python = ">=3.7" -summary = "Markdown URL utilities" -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 = "mini-racer" -version = "0.12.4" -requires_python = ">=3.8" -summary = "Minimal, modern embedded V8 for Python." -groups = ["default"] -files = [ - {file = "mini_racer-0.12.4-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:bce8a3cee946575a352f5e65335903bc148da42c036d0c738ac67e931600e455"}, - {file = "mini_racer-0.12.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:56c832e6ac2db6a304d1e8e80030615297aafbc6940f64f3479af4ba16abccd5"}, - {file = "mini_racer-0.12.4-py3-none-manylinux_2_31_aarch64.whl", hash = "sha256:b82c4bd2976e280ed0a72c9c2de01b13f18ccfbe6f4892cbc22aae04410fac3c"}, - {file = "mini_racer-0.12.4-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:69a1c44d02a9069b881684cef15a2d747fe0743df29eadc881fda7002aae5fd2"}, - {file = "mini_racer-0.12.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:499dbc267dfe60e954bc1b6c3787f7b10fc41fe1975853c9a6ddb55eb83dc4d9"}, - {file = "mini_racer-0.12.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:231f949f5787d18351939f1fe59e5a6fe134bccb5ecf8f836b9beab69d91c8d9"}, - {file = "mini_racer-0.12.4-py3-none-win_amd64.whl", hash = "sha256:9446e3bd6a4eb9fbedf1861326f7476080995a31c9b69308acef17e5b7ecaa1b"}, - {file = "mini_racer-0.12.4.tar.gz", hash = "sha256:84c67553ce9f3736d4c617d8a3f882949d37a46cfb47fe11dab33dd6704e62a4"}, -] - -[[package]] -name = "msgpack" -version = "1.0.8" -requires_python = ">=3.8" -summary = "MessagePack serializer" -groups = ["dev"] -files = [ - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad"}, - {file = "msgpack-1.0.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85"}, - {file = "msgpack-1.0.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b"}, - {file = "msgpack-1.0.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce"}, - {file = "msgpack-1.0.8-cp311-cp311-win32.whl", hash = "sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305"}, - {file = "msgpack-1.0.8-cp311-cp311-win_amd64.whl", hash = "sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b"}, - {file = "msgpack-1.0.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc"}, - {file = "msgpack-1.0.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04"}, - {file = "msgpack-1.0.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543"}, - {file = "msgpack-1.0.8-cp312-cp312-win32.whl", hash = "sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c"}, - {file = "msgpack-1.0.8-cp312-cp312-win_amd64.whl", hash = "sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd"}, - {file = "msgpack-1.0.8.tar.gz", hash = "sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3"}, -] - -[[package]] -name = "mypy" -version = "1.10.1" -requires_python = ">=3.8" -summary = "Optional static typing for Python" -groups = ["dev"] -dependencies = [ - "mypy-extensions>=1.0.0", - "typing-extensions>=4.1.0", -] -files = [ - {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, - {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, - {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, - {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, - {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, - {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, - {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, - {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, - {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, - {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, - {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -requires_python = ">=3.5" -summary = "Type system extensions for programs checked with the mypy type checker." -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -summary = "Node.js virtual environment builder" -groups = ["dev"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "packaging" -version = "24.1" -requires_python = ">=3.8" -summary = "Core utilities for Python packages" -groups = ["default", "dev"] -files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -requires_python = ">=3.8" -summary = "Utility library for gitignore style pattern matching of file paths." -groups = ["dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pbr" -version = "6.0.0" -requires_python = ">=2.6" -summary = "Python Build Reasonableness" -groups = ["dev"] -files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, -] - -[[package]] -name = "pbs-installer" -version = "2024.4.24" -requires_python = ">=3.8" -summary = "Installer for Python Build Standalone" -groups = ["dev"] -files = [ - {file = "pbs_installer-2024.4.24-py3-none-any.whl", hash = "sha256:f8291f0231003d279d0de8fde88fa87b7c6d7fabc2671235113cf67513ff74f5"}, - {file = "pbs_installer-2024.4.24.tar.gz", hash = "sha256:19224733068b0ffa39b53afbb61544bee8ecb9503e7222ba034f07b9913e2c1c"}, -] - -[[package]] -name = "pdbpp" -version = "0.10.3" -summary = "pdb++, a drop-in replacement for pdb" -groups = ["dev"] -dependencies = [ - "fancycompleter>=0.8", - "pygments", - "wmctrl", -] -files = [ - {file = "pdbpp-0.10.3-py2.py3-none-any.whl", hash = "sha256:79580568e33eb3d6f6b462b1187f53e10cd8e4538f7d31495c9181e2cf9665d1"}, - {file = "pdbpp-0.10.3.tar.gz", hash = "sha256:d9e43f4fda388eeb365f2887f4e7b66ac09dce9b6236b76f63616530e2f669f5"}, -] - -[[package]] -name = "pdm" -version = "2.16.1" -requires_python = ">=3.8" -summary = "A modern Python package and dependency manager supporting the latest PEP standards" -groups = ["dev"] -dependencies = [ - "blinker", - "dep-logic<1.0,>=0.2.0", - "filelock>=3.13", - "findpython<1.0.0a0,>=0.6.0", - "hishel<0.1.0,>=0.0.24", - "httpx[socks]<1,>0.20", - "installer<0.8,>=0.7", - "msgpack>=1.0", - "packaging!=22.0,>=20.9", - "pbs-installer>=2024.4.18", - "platformdirs", - "pyproject-hooks", - "python-dotenv>=0.15", - "resolvelib>=1.0.1", - "rich>=12.3.0", - "shellingham>=1.3.2", - "tomlkit<1,>=0.11.1", - "truststore; python_version >= \"3.10\"", - "unearth>=0.15.0", - "virtualenv>=20", -] -files = [ - {file = "pdm-2.16.1-py3-none-any.whl", hash = "sha256:f3473ff12433bcc42cc1e7540c2c84d028290619542c61431fe722e54536c3e3"}, - {file = "pdm-2.16.1.tar.gz", hash = "sha256:b8680028b3aff3af8e15b483467da36bb9f02fcd402cf939da8ab6375d955131"}, -] - -[[package]] -name = "pdm-bump" -version = "0.9.0" -requires_python = ">=3.9" -summary = "A plugin for PDM providing the ability to modify the version according to PEP440" -groups = ["dev"] -dependencies = [ - "annotated-types>=0.2.0", - "pdm-pfsc>=0.10.0", - "pdm>=2.00", - "pyproject-metadata>=0.6.1", - "tomli-w>=1.0.0", - "tomlkit>=0.11.6", -] -files = [ - {file = "pdm_bump-0.9.0-py3-none-any.whl", hash = "sha256:0de179203adb5db6fc5a8e9f23d829a59ff207099d9b8599d5f6f2beed514bbc"}, - {file = "pdm_bump-0.9.0.tar.gz", hash = "sha256:7ce9534035ad2e7e16c47a0b7c8031303ccc1c3cdb88ef7dcedd74b683a5397a"}, -] - -[[package]] -name = "pdm-pfsc" -version = "0.11.3" -requires_python = ">=3.9" -summary = "Core functionPlug-In Foundation Service Collection)" -groups = ["dev"] -dependencies = [ - "pdm>=2.12.4", - "pyproject-metadata>=0.7.1", - "tomli-w>=1.0.0", -] -files = [ - {file = "pdm_pfsc-0.11.3-py3-none-any.whl", hash = "sha256:97cd9a2e56fbf09f0016c786f169d7b7cf517302c4331e090192c5b9a5640b46"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -requires_python = ">=3.8" -summary = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -groups = ["dev"] -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -requires_python = ">=3.8" -summary = "plugin and hook calling mechanisms for python" -groups = ["dev"] -files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, -] - -[[package]] -name = "pre-commit" -version = "3.7.1" -requires_python = ">=3.9" -summary = "A framework for managing and maintaining multi-language pre-commit hooks." -groups = ["dev"] -dependencies = [ - "cfgv>=2.0.0", - "identify>=1.0.0", - "nodeenv>=0.11.1", - "pyyaml>=5.1", - "virtualenv>=20.10.0", -] -files = [ - {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, - {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, -] - -[[package]] -name = "pycodestyle" -version = "2.12.0" -requires_python = ">=3.8" -summary = "Python style guide checker" -groups = ["dev"] -files = [ - {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, - {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, -] - -[[package]] -name = "pyflakes" -version = "3.2.0" -requires_python = ">=3.8" -summary = "passive checker of Python programs" -groups = ["dev"] -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - -[[package]] -name = "pygments" -version = "2.18.0" -requires_python = ">=3.8" -summary = "Pygments is a syntax highlighting package written in Python." -groups = ["dev"] -files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, -] - -[[package]] -name = "pyproject-api" -version = "1.7.1" -requires_python = ">=3.8" -summary = "API to interact with the python pyproject.toml based projects" -groups = ["dev"] -dependencies = [ - "packaging>=24.1", -] -files = [ - {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, - {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, -] - -[[package]] -name = "pyproject-hooks" -version = "1.1.0" -requires_python = ">=3.7" -summary = "Wrappers to call pyproject.toml-based build backend hooks." -groups = ["dev"] -files = [ - {file = "pyproject_hooks-1.1.0-py3-none-any.whl", hash = "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2"}, - {file = "pyproject_hooks-1.1.0.tar.gz", hash = "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965"}, -] - -[[package]] -name = "pyproject-metadata" -version = "0.8.0" -requires_python = ">=3.7" -summary = "PEP 621 metadata parsing" -groups = ["dev"] -dependencies = [ - "packaging>=19.0", -] -files = [ - {file = "pyproject_metadata-0.8.0-py3-none-any.whl", hash = "sha256:ad858d448e1d3a1fb408ac5bac9ea7743e7a8bbb472f2693aaa334d2db42f526"}, - {file = "pyproject_metadata-0.8.0.tar.gz", hash = "sha256:376d5a00764ac29440a54579f88e66b7d9cb7e629d35c35a1c7248bfebc9b455"}, -] - -[[package]] -name = "pyreadline" -version = "2.1" -summary = "A python implmementation of GNU readline." -groups = ["dev"] -marker = "platform_system == \"Windows\"" -files = [ - {file = "pyreadline-2.1.zip", hash = "sha256:4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1"}, -] - -[[package]] -name = "pyrepl" -version = "0.9.0" -summary = "A library for building flexible command line interfaces" -groups = ["dev"] -files = [ - {file = "pyrepl-0.9.0.tar.gz", hash = "sha256:292570f34b5502e871bbb966d639474f2b57fbfcd3373c2d6a2f3d56e681a775"}, -] - -[[package]] -name = "pytest" -version = "8.2.2" -requires_python = ">=3.8" -summary = "pytest: simple powerful testing with Python" -groups = ["dev"] -dependencies = [ - "colorama; sys_platform == \"win32\"", - "iniconfig", - "packaging", - "pluggy<2.0,>=1.5", -] -files = [ - {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, - {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, -] - -[[package]] -name = "pytest-cov" -version = "5.0.0" -requires_python = ">=3.8" -summary = "Pytest plugin for measuring coverage." -groups = ["dev"] -dependencies = [ - "coverage[toml]>=5.2.1", - "pytest>=4.6", -] -files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, -] - -[[package]] -name = "pytest-django" -version = "4.8.0" -requires_python = ">=3.8" -summary = "A Django plugin for pytest." -groups = ["dev"] -dependencies = [ - "pytest>=7.0.0", -] -files = [ - {file = "pytest-django-4.8.0.tar.gz", hash = "sha256:5d054fe011c56f3b10f978f41a8efb2e5adfc7e680ef36fb571ada1f24779d90"}, - {file = "pytest_django-4.8.0-py3-none-any.whl", hash = "sha256:ca1ddd1e0e4c227cf9e3e40a6afc6d106b3e70868fd2ac5798a22501271cd0c7"}, -] - -[[package]] -name = "python-calamine" -version = "0.2.2" -requires_python = ">=3.8" -summary = "Python binding for Rust's library for reading excel and odf file - calamine" -groups = ["default"] -files = [ - {file = "python_calamine-0.2.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:78202dc78824ef714b48b5a842dac090458669db852fa2c587ff75bf194b3cc4"}, - {file = "python_calamine-0.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eba61e33cf472de35d278c9c2e9279b82b62d3f0a77c84a717566d56f8c6f54c"}, - {file = "python_calamine-0.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90de5cd327e33927f84ba5077e87f863745f4cec531af92c1594262b074ed459"}, - {file = "python_calamine-0.2.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:356cc3842e5321c7d9b5ece162ab5e897755aa0a0dba85ec7f02f28bba0ca333"}, - {file = "python_calamine-0.2.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:316ca85177a93df8c6f7d67d12f3727776f21420ddfe6dcac85525a433e5df86"}, - {file = "python_calamine-0.2.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:068a2a66c0c36cd1651ad0bdcc6bc0b7e7cb94a62f25ba17246b5258b0b2c7b6"}, - {file = "python_calamine-0.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc73e7f3f7e70bef3da0c58f206b5134c9bb1252428b7303c542813699a06c04"}, - {file = "python_calamine-0.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b9e0a6b2d872a8143d1f15c529c0beeb49d7491d7377a3c6eed9a15e3c347c5"}, - {file = "python_calamine-0.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d96881a6a41f53b8745c6e3ddde3f729d0c4ee653ea1da8f0afa1bd54b94cf66"}, - {file = "python_calamine-0.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:86d9512b769c3f837486defbbe4ea263e1a7974bca2676d0082fde7f6226c54c"}, - {file = "python_calamine-0.2.2-cp311-none-win32.whl", hash = "sha256:d886886a48f19f261e7af4bbbbc2c8470816d80038c5adca326347546e4a0977"}, - {file = "python_calamine-0.2.2-cp311-none-win_amd64.whl", hash = "sha256:4217e3f2d378c8cfae8cd34ac780da6c126955988e899f9eaad08c0e1f0e0fc2"}, - {file = "python_calamine-0.2.2-cp311-none-win_arm64.whl", hash = "sha256:0dbdddf6e665335e0e5de273d46f7f163e906255886466f85a9a064e004e9ad6"}, - {file = "python_calamine-0.2.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ed2965f17feb15b745efe54e5a279e666dddee78f5e927106ea019b77c4438cd"}, - {file = "python_calamine-0.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa38999c4e70b9e05b8287c47537f40889630d39e1fe25909a461fbb56c2a71c"}, - {file = "python_calamine-0.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d658f161c55177c738b33de26465f80c5c3f2a85e01488731c78236d82576933"}, - {file = "python_calamine-0.2.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd2c63170715ca2b5abb3b74906583f1ab2d8304fe49da7dc6a539161b2276c2"}, - {file = "python_calamine-0.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8185353be901077154f16cf956afbaa245a937a0e13e24345abcaf1eee78b4c4"}, - {file = "python_calamine-0.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a23d286c0a1102b18b1dbd1c4c555c850c7832694ce17115cc602c95cf1a2b08"}, - {file = "python_calamine-0.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f4762cb4d40d9bdae4fa80c96d7f5ba9c8bee71b9c869a7cb89ff3406bd0c2a"}, - {file = "python_calamine-0.2.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd73c0b8cbc085e5e0e665330eed6468f249610a5c9263a6dad9d5af40da2315"}, - {file = "python_calamine-0.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a0db6045fff1fe57fa43815f9d9d509ed5b2e8131f2c9d149a5b01b93bd45b1c"}, - {file = "python_calamine-0.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c99ecfe73eec8c7b6152b5d5f5a5171a270f28fce4cafa349ff8290ac2705f4c"}, - {file = "python_calamine-0.2.2-cp312-none-win32.whl", hash = "sha256:c74051d5f9e7444f1c7ca00fe505c140b35172c137385a5a5f3ecfa03ecca502"}, - {file = "python_calamine-0.2.2-cp312-none-win_amd64.whl", hash = "sha256:191511c30e4de3c43dd8c5af2c2d6a37f48aea75ee1e221916ebfad240d674cb"}, - {file = "python_calamine-0.2.2-cp312-none-win_arm64.whl", hash = "sha256:75fe2f20b524e4fa34db0968a84f17313cedb361c17551e3240358c8fdc6909f"}, - {file = "python_calamine-0.2.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:976b11627b5065438af40acaaf8eb60ef07b92a67bd1af80fe02740c4e5dfa8e"}, - {file = "python_calamine-0.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc2ee5aa0fd14e4aeb9c6c78e4f12afdf75a446d4ea57fb75121f25e1c4b866c"}, - {file = "python_calamine-0.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09611173a50748f5cdff0fbc1aa623aa1698f0c2cc6962baa67a781cf4d3bd89"}, - {file = "python_calamine-0.2.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1bc243d91cf0dbaaa5aa297f73bab778c821beeb76e1db2d379cc6ac50166250"}, - {file = "python_calamine-0.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d736a2efdc1a82c2775d007e56b8ec2028b875754c7c9bc547298b4953aa6858"}, - {file = "python_calamine-0.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9d05ddeb3568834a99bf225e5ea2d03e37557ec6f063af1a0dc1e481efc1adf"}, - {file = "python_calamine-0.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:621b62b061bf4ba6589a21a82f27064a3b15997cd77b14ed9dac69d3b41343b9"}, - {file = "python_calamine-0.2.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ea1228fcfb09031934a9469f76b6aed300481abfd2ea9f601eea98cb1eee5f7"}, - {file = "python_calamine-0.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:8256eacf892f2cc7aeba57940b891bf963c7f5f0097426714a5df739a67dd7fb"}, - {file = "python_calamine-0.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:4a3bb7e483fd261ee2b9c71a127f6b2dba2772370c7f5d60535b789cd87e2b4f"}, - {file = "python_calamine-0.2.2-cp313-none-win32.whl", hash = "sha256:5264ac774418f2eef2e2a589b23f84033298a209806ff263eb2b6c685f92926b"}, - {file = "python_calamine-0.2.2-cp313-none-win_amd64.whl", hash = "sha256:69c4c76dfdc5a29241349ba6be32c99ad7938060345d1393515cb599fb5e2595"}, - {file = "python_calamine-0.2.2-cp313-none-win_arm64.whl", hash = "sha256:ceb501aa7e414c77e2356ac5382bebfbea42c1720e0fb37dcd3b8758fd8fd3c5"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b3ed76a81deee5d2230ee6d56e3899f3ff28c92e7038154a9c948af708bcbe25"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:56c505a70728ae3a33efa3a58b960571b283c05725f83c567146d143227cc924"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce6d654b6210a4a99dd38641ec1629bc8a6cffade355b3e2b0e51d0b834b1375"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2667fe2ebb1d5745742b89c1072c1c09c7f3954b9336c9e86f83b39ca1eaf30d"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0f542a6ed743fba27c9b24f55d6c35ebdc1635cb63a7e761997a778f8e63ef9"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e4bca6fe6605c599ed60ac5dd564530783762e3c04140753097860ea3491974f"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d2055a757a75ebd6e279a3de6b4e55c735e9e881610277ba2203383c5b5acde3"}, - {file = "python_calamine-0.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb779d518ddbfdefbe9a19b8dbf0aad789b2b24903e71f8595a7f2ef0b25c955"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d548c002fe17bf37cb4389ade0d3e6a6cd6d56db61d3db90db9b2d7bb9662c49"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:cd2058840529891a062b420f02cba1027bc354d5a51cc1b19ffe767edc72f4fe"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c06ea6cea0cd6a453a904132d521046f6a70d97db5ab5b434a43e585ad43a82d"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cbef0a14380ca271a212cebf82a6c2f229ed7b1fa42da7b02bb48fed7eba061"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdffa889ff0e151eab692bcf38183ddd03dd8674b32687f2bb5aea49f6c9d563"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3d25249f3c13d9dc87725c1aade38d52093ac85ea9458d304fe272f1cdae1ed4"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:6e1dfb3cb6c597277c44ff4cb29922b5e02623fff700f8bed46d7881f7e2c21a"}, - {file = "python_calamine-0.2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:91597f7fa60f26177118457e02b4c9da88f3ee49551a703d4d8eef1cc22171c7"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4240d540d971a2c181d7499695ceecc535835611c7f96aa8b720875f23d93c52"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c347d80edb229549ca22c932f8cf16b13921e1ea9b2ad5e1b4a6e215bde7d0c"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78533987f254f3867464b86c5b1c492fd18cb1579c8581be4e9b75d2900eb235"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90800d4d7e4d5cd277df8c5095eb85887579f29716484592cb9326bc1f689483"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8fbf39a84d50ab53c74682e8121810e8c4aa217540604fee6eba8358865832f2"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f5283fc9069a301a8b0aa8ffa1155cde4f1b163541b58fcc73083ed24173db19"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c1ce686abc6ef419e443e4e9abac07434f999d546e833e151713c354f44c6f6d"}, - {file = "python_calamine-0.2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e86fd5b1e85ffdc90cfc78078c9f980732200c9b3be71a9a05e60106744df150"}, - {file = "python_calamine-0.2.2.tar.gz", hash = "sha256:c7f327c4778d4478e76003d9f75c9e0bf3c697f3e64c4382b393ea319aa649cd"}, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -summary = "Extensions to the standard Python datetime module" -groups = ["dev"] -dependencies = [ - "six>=1.5", -] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -requires_python = ">=3.8" -summary = "Read key-value pairs from a .env file and set them as environment variables" -groups = ["dev"] -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[[package]] -name = "pytz" -version = "2024.1" -summary = "World timezone definitions, modern and historical" -groups = ["default"] -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.1" -requires_python = ">=3.6" -summary = "YAML parser and emitter for Python" -groups = ["default", "dev"] -files = [ - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -requires_python = ">=3.8" -summary = "Python HTTP for Humans." -groups = ["default", "dev"] -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<4,>=2", - "idna<4,>=2.5", - "urllib3<3,>=1.21.1", -] -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[[package]] -name = "resolvelib" -version = "1.0.1" -summary = "Resolve abstract dependencies into concrete ones" -groups = ["dev"] -files = [ - {file = "resolvelib-1.0.1-py2.py3-none-any.whl", hash = "sha256:d2da45d1a8dfee81bdd591647783e340ef3bcb104b54c383f70d422ef5cc7dbf"}, - {file = "resolvelib-1.0.1.tar.gz", hash = "sha256:04ce76cbd63fded2078ce224785da6ecd42b9564b1390793f64ddecbe997b309"}, -] - -[[package]] -name = "responses" -version = "0.25.3" -requires_python = ">=3.8" -summary = "A utility library for mocking out the `requests` Python library." -groups = ["default", "dev"] -dependencies = [ - "pyyaml", - "requests<3.0,>=2.30.0", - "urllib3<3.0,>=1.25.10", -] -files = [ - {file = "responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb"}, - {file = "responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba"}, -] - -[[package]] -name = "rich" -version = "13.7.1" -requires_python = ">=3.7.0" -summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -groups = ["dev"] -dependencies = [ - "markdown-it-py>=2.2.0", - "pygments<3.0.0,>=2.13.0", -] -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[[package]] -name = "shellingham" -version = "1.5.4" -requires_python = ">=3.7" -summary = "Tool to Detect Surrounding Shell" -groups = ["dev"] -files = [ - {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, - {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, -] - -[[package]] -name = "six" -version = "1.16.0" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -summary = "Python 2 and 3 compatibility utilities" -groups = ["dev"] -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -requires_python = ">=3.7" -summary = "Sniff out which async library your code is running under" -groups = ["dev"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "socksio" -version = "1.0.0" -requires_python = ">=3.6" -summary = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." -groups = ["dev"] -files = [ - {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, - {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, -] - -[[package]] -name = "soupsieve" -version = "2.5" -requires_python = ">=3.8" -summary = "A modern CSS selector implementation for Beautiful Soup." -groups = ["dev"] -files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, -] - -[[package]] -name = "sqlparse" -version = "0.5.0" -requires_python = ">=3.8" -summary = "A non-validating SQL parser." -groups = ["default", "dev"] -files = [ - {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"}, - {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"}, -] - -[[package]] -name = "stevedore" -version = "5.2.0" -requires_python = ">=3.8" -summary = "Manage dynamic plugins for Python applications" -groups = ["dev"] -dependencies = [ - "pbr!=2.1.0,>=2.0.0", -] -files = [ - {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, - {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, -] - -[[package]] -name = "tomli-w" -version = "1.0.0" -requires_python = ">=3.7" -summary = "A lil' TOML writer" -groups = ["dev"] -files = [ - {file = "tomli_w-1.0.0-py3-none-any.whl", hash = "sha256:9f2a07e8be30a0729e533ec968016807069991ae2fd921a78d42f429ae5f4463"}, - {file = "tomli_w-1.0.0.tar.gz", hash = "sha256:f463434305e0336248cac9c2dc8076b707d8a12d019dd349f5c1e382dd1ae1b9"}, -] - -[[package]] -name = "tomlkit" -version = "0.13.0" -requires_python = ">=3.8" -summary = "Style preserving TOML library" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, -] - -[[package]] -name = "tox" -version = "4.16.0" -requires_python = ">=3.8" -summary = "tox is a generic virtualenv management and test command line tool" -groups = ["dev"] -dependencies = [ - "cachetools>=5.3.3", - "chardet>=5.2", - "colorama>=0.4.6", - "filelock>=3.15.4", - "packaging>=24.1", - "platformdirs>=4.2.2", - "pluggy>=1.5", - "pyproject-api>=1.7.1", - "virtualenv>=20.26.3", -] -files = [ - {file = "tox-4.16.0-py3-none-any.whl", hash = "sha256:61e101061b977b46cf00093d4319438055290ad0009f84497a07bf2d2d7a06d0"}, - {file = "tox-4.16.0.tar.gz", hash = "sha256:43499656f9949edb681c0f907f86fbfee98677af9919d8b11ae5ad77cb800748"}, -] - -[[package]] -name = "truststore" -version = "0.9.1" -requires_python = ">=3.10" -summary = "Verify certificates using native system trust stores" -groups = ["dev"] -marker = "python_version >= \"3.10\"" -files = [ - {file = "truststore-0.9.1-py3-none-any.whl", hash = "sha256:7f5b447d68318d966428131fc1c00442cca3a2d581a3986143558f007efba0b4"}, - {file = "truststore-0.9.1.tar.gz", hash = "sha256:8f7312d70cc33e9003b748a80a04ead1fcb2ed856a7c6c9ca5a02482901a90be"}, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -requires_python = ">=3.8" -summary = "Backported and Experimental Type Hints for Python 3.8+" -groups = ["dev"] -files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, -] - -[[package]] -name = "tzdata" -version = "2024.1" -requires_python = ">=2" -summary = "Provider of IANA time zone data" -groups = ["default", "dev"] -marker = "sys_platform == \"win32\"" -files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, -] - -[[package]] -name = "unearth" -version = "0.16.1" -requires_python = ">=3.8" -summary = "A utility to fetch and download python packages" -groups = ["dev"] -dependencies = [ - "httpx<1,>=0.27.0", - "packaging>=20", -] -files = [ - {file = "unearth-0.16.1-py3-none-any.whl", hash = "sha256:5a598ac1a3f185144fadc9de47f1043bff805c36118ffc40f81ef98ff22e8e37"}, - {file = "unearth-0.16.1.tar.gz", hash = "sha256:988a43418fa0b78aeb628a15f6a3b02152c1787f63fe6d254c7f4e2ccf8db0a7"}, -] - -[[package]] -name = "urllib3" -version = "2.2.2" -requires_python = ">=3.8" -summary = "HTTP library with thread-safe connection pooling, file post, and more." -groups = ["default", "dev"] -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[[package]] -name = "virtualenv" -version = "20.26.3" -requires_python = ">=3.7" -summary = "Virtual Python Environment builder" -groups = ["dev"] -dependencies = [ - "distlib<1,>=0.3.7", - "filelock<4,>=3.12.2", - "platformdirs<5,>=3.9.1", -] -files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, -] - -[[package]] -name = "waitress" -version = "3.0.0" -requires_python = ">=3.8.0" -summary = "Waitress WSGI server" -groups = ["dev"] -files = [ - {file = "waitress-3.0.0-py3-none-any.whl", hash = "sha256:2a06f242f4ba0cc563444ca3d1998959447477363a2d7e9b8b4d75d35cfd1669"}, - {file = "waitress-3.0.0.tar.gz", hash = "sha256:005da479b04134cdd9dd602d1ee7c49d79de0537610d653674cc6cbde222b8a1"}, -] - -[[package]] -name = "webob" -version = "1.8.7" -requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" -summary = "WSGI request and response object" -groups = ["dev"] -files = [ - {file = "WebOb-1.8.7-py2.py3-none-any.whl", hash = "sha256:73aae30359291c14fa3b956f8b5ca31960e420c28c1bec002547fb04928cf89b"}, - {file = "WebOb-1.8.7.tar.gz", hash = "sha256:b64ef5141be559cfade448f044fa45c2260351edcb6a8ef6b7e00c7dcef0c323"}, -] - -[[package]] -name = "webtest" -version = "3.0.0" -requires_python = ">=3.6, <4" -summary = "Helper to test WSGI applications" -groups = ["dev"] -dependencies = [ - "WebOb>=1.2", - "beautifulsoup4", - "waitress>=0.8.5", -] -files = [ - {file = "WebTest-3.0.0-py3-none-any.whl", hash = "sha256:2a001a9efa40d2a7e5d9cd8d1527c75f41814eb6afce2c3d207402547b1e5ead"}, - {file = "WebTest-3.0.0.tar.gz", hash = "sha256:54bd969725838d9861a9fa27f8d971f79d275d94ae255f5c501f53bb6d9929eb"}, -] - -[[package]] -name = "wmctrl" -version = "0.5" -requires_python = ">=2.7" -summary = "A tool to programmatically control windows inside X" -groups = ["dev"] -dependencies = [ - "attrs", -] -files = [ - {file = "wmctrl-0.5-py2.py3-none-any.whl", hash = "sha256:ae695c1863a314c899e7cf113f07c0da02a394b968c4772e1936219d9234ddd7"}, - {file = "wmctrl-0.5.tar.gz", hash = "sha256:7839a36b6fe9e2d6fd22304e5dc372dbced2116ba41283ea938b2da57f53e962"}, -] - -[[package]] -name = "xlrd" -version = "2.0.1" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -summary = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" -groups = ["default"] -files = [ - {file = "xlrd-2.0.1-py2.py3-none-any.whl", hash = "sha256:6a33ee89877bd9abc1158129f6e94be74e2679636b8a205b43b85206c3f0bbdd"}, - {file = "xlrd-2.0.1.tar.gz", hash = "sha256:f72f148f54442c6b056bf931dbc34f986fd0c3b0b6b5a58d013c9aef274d0c88"}, -] - -[[package]] -name = "xlsxwriter" -version = "3.2.0" -requires_python = ">=3.6" -summary = "A Python module for creating Excel XLSX files." -groups = ["default"] -files = [ - {file = "XlsxWriter-3.2.0-py3-none-any.whl", hash = "sha256:ecfd5405b3e0e228219bcaf24c2ca0915e012ca9464a14048021d21a995d490e"}, - {file = "XlsxWriter-3.2.0.tar.gz", hash = "sha256:9977d0c661a72866a61f9f7a809e25ebbb0fb7036baa3b9fe74afcfca6b3cb8c"}, -] - -[[package]] -name = "xlwt" -version = "1.3.0" -summary = "Library to create spreadsheet files compatible with MS Excel 97/2000/XP/2003 XLS files, on any platform, with Python 2.6, 2.7, 3.3+" -groups = ["default"] -files = [ - {file = "xlwt-1.3.0-py2.py3-none-any.whl", hash = "sha256:a082260524678ba48a297d922cc385f58278b8aa68741596a87de01a9c628b2e"}, - {file = "xlwt-1.3.0.tar.gz", hash = "sha256:c59912717a9b28f1a3c2a98fd60741014b06b043936dcecbc113eaaada156c88"}, -] diff --git a/pyproject.toml b/pyproject.toml index 76cf37e..d370196 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,30 +5,59 @@ description = "" authors = [ {name = "sax", email = "s.apostolico@gmail.com"}, ] + +requires-python = ">=3.11" +readme = "README.md" +license = {text = "MIT"} + dependencies = [ "django-strategy-field>=3.1.0", "django-regex>=0.5.0", "django-regex-field>=3.1.0", - "django-admin-extra-buttons>=1.5.8", + "django-admin-extra-buttons>=1.6.0", "django-jsoneditor>=0.2.4", "djangorestframework>=3.15.1", "mini-racer>=0.12.4", "jsonpickle>=3.2.2", "xlsxwriter>=3.2.0", "django-adminactions>=2.3.0", - "duckdb>=1.0.0", "python-calamine>=0.2.0", "requests>=2.32.3", "responses>=0.25.3", + "jmespath>=1.0.1", + "deepdiff>=8.0.1", +] +[project.optional-dependencies] +docs = [ + "mkdocs>=1.6.1", + "mkdocs-material>=9.5.36", + "mkdocs-awesome-pages-plugin>=2.9.3", + "mkdocstrings-python", ] -requires-python = ">=3.11" -readme = "README.md" -license = {text = "MIT"} +[tool.uv] +dev-dependencies = [ + "bandit>=1.7.9", + "black>=24.4.2", + "bump2version>=1.0.1", + "django-csp>=3.8", + "django-factory-boy>=1.0.0", + "django-webtest>=1.9.11", + "django>=3", + "flake8-html>=0.4.3", + "flake8>=7.1.0", + "freezegun>=1.5.1", + "isort>=5.13.2", + "mypy>=1.10.1", + "pdbpp>=0.10.3", + "pre-commit>=3.7.1", + "pytest-cov>=5.0.0", + "pytest-django>=4.8.0", + "pytest>=8.2.2", + "responses>=0.25.3", + "tox>=4.16.0", +] -[build-system] -requires = ["pdm-backend"] -build-backend = "pdm.backend" [tool.black] line-length = 88 @@ -59,33 +88,3 @@ skip = ["migrations", "snapshots", ".venv"] [tool.django-stubs] django_settings_module = "hope_dedup_engine.config.settings" - -[tool.pdm] -distribution = true - -[tool.pdm.dev-dependencies] -dev = [ - "django>=3", - "pytest>=8.2.2", - "django-factory-boy>=1.0.0", - "black>=24.4.2", - "flake8>=7.1.0", - "flake8-html>=0.4.3", - "isort>=5.13.2", - "pre-commit>=3.7.1", - "freezegun>=1.5.1", - "mypy>=1.10.1", - "pdbpp>=0.10.3", - "pytest-django>=4.8.0", - "pytest-cov>=5.0.0", - "django-webtest>=1.9.11", - "responses>=0.25.3", - "django-csp>=3.8", - "bandit>=1.7.9", - "tox>=4.16.0", - "pdm-bump>=0.9.0", -] - -[tool.pdm.scripts] -coverage = "pytest tests/ --cov -n auto --create-db -c pytest.ini" -act = "act" diff --git a/src/hope_flex_fields/__init__.py b/src/hope_flex_fields/__init__.py index e69de29..428345e 100644 --- a/src/hope_flex_fields/__init__.py +++ b/src/hope_flex_fields/__init__.py @@ -0,0 +1 @@ +VERSION = __version__ = "0.1.0" diff --git a/src/hope_flex_fields/admin/datachecker.py b/src/hope_flex_fields/admin/datachecker.py index b0f9c6c..a4d2955 100644 --- a/src/hope_flex_fields/admin/datachecker.py +++ b/src/hope_flex_fields/admin/datachecker.py @@ -40,8 +40,26 @@ class FileForm(forms.Form): ) +class DataCheckerFieldsetFormset(forms.models.BaseInlineFormSet): + def clean(self): + all_fields = set() + fs: Fieldset + for form in self.forms: + if fs := form.cleaned_data.get("fieldset"): + prefix: str = form.cleaned_data["prefix"] + # fs_fields = {f"{prefix}{f}" for f in fs.get_fieldnames()} + if "%s" in prefix: + fs_fields = {prefix % f for f in fs.get_fieldnames()} + else: + fs_fields = {f"{prefix}{f}" for f in fs.get_fieldnames()} + if all_fields.intersection(fs_fields): + raise forms.ValidationError("Field names are not unique") + all_fields.update(fs_fields) + + class DataCheckerFieldsetTabularInline(TabularInline): model = DataCheckerFieldset + formset = DataCheckerFieldsetFormset fields = ("fieldset", "prefix", "order") def get_ordering(self, request): @@ -67,7 +85,7 @@ def validate(self, request, pk): dc: DataChecker = ctx["original"] f = form.cleaned_data["file"] parser = HANDLERS[Path(f.name).suffix] - ret = dc.validate(parser(f), True) + ret = dc.validate(parser(f), include_success=True) ctx["results"] = ret self.message_user(request, "Data looks valid", messages.SUCCESS) else: @@ -92,8 +110,8 @@ def create_xls_importer(self, request, pk): @button() def test(self, request, pk): ctx = self.get_common_context(request, pk, title="Test") - fs: Fieldset = ctx["original"] - form_class = fs.get_form() + dc: DataChecker = ctx["original"] + form_class = dc.get_form() if request.method == "POST": form = form_class(request.POST) if form.is_valid(): diff --git a/src/hope_flex_fields/admin/definition.py b/src/hope_flex_fields/admin/definition.py index eecd086..c39e901 100644 --- a/src/hope_flex_fields/admin/definition.py +++ b/src/hope_flex_fields/admin/definition.py @@ -16,8 +16,8 @@ from admin_extra_buttons.mixins import ExtraButtonsMixin from ..forms import FieldDefinitionForm -from ..models import FieldDefinition, get_default_attrs -from ..utils import dumpdata_to_buffer, loaddata_from_buffer +from ..models import FieldDefinition +from ..utils import dumpdata_to_buffer, get_default_attrs, loaddata_from_buffer @deconstructible @@ -50,6 +50,7 @@ class FieldDefinitionAdmin(ExtraButtonsMixin, ModelAdmin): list_filter = ("field_type",) search_fields = ("name", "description") form = FieldDefinitionForm + readonly_fields = ("system_data", "content_type") fieldsets = ( ("", {"fields": (("name", "field_type"), "description")}), ( @@ -59,6 +60,13 @@ class FieldDefinitionAdmin(ExtraButtonsMixin, ModelAdmin): "fields": ("regex", "attrs", "validation"), }, ), + ( + "Advanced", + { + "classes": ("collapse", "open"), + "fields": ("content_type", "system_data"), + }, + ), ) def field_type_(self, obj): @@ -101,7 +109,12 @@ def get_changeform_initial_data(self, request): def test(self, request, pk): ctx = self.get_common_context(request, pk, title="Test") fd: FieldDefinition = ctx["original"] - field = fd.get_field() + try: + field = fd.get_field() + except Exception as e: + self.message_user(request, str(e)) + field = fd.get_field({}) + form_class_attrs = { fd.name: field, } diff --git a/src/hope_flex_fields/admin/fieldset.py b/src/hope_flex_fields/admin/fieldset.py index 2a4ae0e..b9eb490 100644 --- a/src/hope_flex_fields/admin/fieldset.py +++ b/src/hope_flex_fields/admin/fieldset.py @@ -2,72 +2,45 @@ from django.contrib import messages from django.contrib.admin import ModelAdmin, register from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.db.transaction import atomic -from django.forms import modelform_factory +from django.http import HttpResponseRedirect from django.shortcuts import render from admin_extra_buttons.decorators import button from admin_extra_buttons.mixins import ExtraButtonsMixin from ..forms import FieldsetForm -from ..models import FieldDefinition, Fieldset, FlexField -from ..utils import get_kwargs_from_formfield +from ..models import Fieldset from .flexfield import FieldsetFieldTabularInline class FieldSetForm(forms.Form): - content_type = forms.ModelChoiceField(queryset=ContentType.objects.all()) + name = forms.CharField() + content_type = forms.ModelChoiceField( + queryset=ContentType.objects.all(), required=True + ) + + def clean_name(self): + if Fieldset.objects.filter(name__iexact=self.cleaned_data["name"]).exists(): + raise ValidationError("Fieldset with this name already exists") + return self.cleaned_data["name"] - def analyse(self): + def save(self): ct: ContentType = self.cleaned_data["content_type"] - model_class = ct.model_class() - model_form = modelform_factory( - model_class, exclude=(model_class._meta.pk.name,) - ) - errors = [] - fields = [] - config = {} - for name, field in model_form().fields.items(): - try: - fd = FieldDefinition.objects.get(name=type(field).__name__) - fld = FlexField( - name=name, field=fd, attrs=get_kwargs_from_formfield(field) - ) - fld.attrs = fld.get_merged_attrs() - fields.append(fld) - config["name"] = {"definition": fd.name, "attrs": fld.attrs} - fld.get_field() - except FieldDefinition.DoesNotExist: - errors.append( - { - "name": name, - "error": f"Field definition for '{type(field).__name__}' does not exist", - } - ) - return { - "fields": fields, - "errors": errors, - "config": config, - "content_type": ct, - } + return Fieldset.objects.inspect_content_type(ct) class FieldSetForm2(forms.Form): + name = forms.CharField(widget=forms.HiddenInput) + config = forms.JSONField(widget=forms.HiddenInput) content_type = forms.ModelChoiceField( queryset=ContentType.objects.all(), widget=forms.HiddenInput ) - config = forms.JSONField(widget=forms.HiddenInput) def save(self): with atomic(): - ct: ContentType = self.cleaned_data["content_type"] - model_class = ct.model_class() - fs, __ = Fieldset.objects.get_or_create( - name=f"{model_class._meta.app_label}_{model_class._meta.model_name}" - ) - for name, info in self.cleaned_data["config"].items(): - fd = FieldDefinition.objects.get(name=info["definition"]) - fs.fields.get_or_create(name=name, field=fd, attrs=info["attrs"]) + return Fieldset.objects.create_from_content_type(**self.cleaned_data) class inspect_field: @@ -78,7 +51,8 @@ class inspect_field: class FieldsetAdmin(ExtraButtonsMixin, ModelAdmin): list_select_related = True search_fields = ("name",) - list_display = ("name", "extends") + list_display = ("name", "extends", "content_type") + list_filter = ("content_type",) inlines = [FieldsetFieldTabularInline] form = FieldsetForm @@ -86,21 +60,24 @@ class FieldsetAdmin(ExtraButtonsMixin, ModelAdmin): def create_from_content_type(self, request): ctx = self.get_common_context(request, title="Create from ContentType") if request.method == "POST": - if "config" in request.POST: - form = FieldSetForm2(request.POST, request.FILES) - form.is_valid() - form.save() - else: + if "analyse" in request.POST: form = FieldSetForm(request.POST, request.FILES) if form.is_valid(): - result = form.analyse() + result = form.save() ctx.update(**result) form = FieldSetForm2( initial={ "content_type": result["content_type"], + "name": form.cleaned_data["name"], "config": result["config"], - } + }, ) + else: + # elif "create" in request.POST: + form = FieldSetForm2(request.POST, request.FILES) + form.is_valid() + form.save() + return HttpResponseRedirect("..") else: form = FieldSetForm() ctx["form"] = form @@ -111,6 +88,13 @@ def inspect(self, request, pk): ctx = self.get_common_context(request, pk, title="Inspect") return render(request, "flex_fields/inspect.html", ctx) + @button(enabled=lambda s: s.context["original"].content_type) + def detect_changes(self, request, pk): + ctx = self.get_common_context(request, pk, title="Differences") + fs: Fieldset = ctx["original"] + ctx["diff"] = fs.diff_content_type() + return render(request, "flex_fields/fieldset/diff.html", ctx) + @button() def test(self, request, pk): ctx = self.get_common_context(request, pk, title="Test") diff --git a/src/hope_flex_fields/admin/flexfield.py b/src/hope_flex_fields/admin/flexfield.py index 498c6de..a4e7e6b 100644 --- a/src/hope_flex_fields/admin/flexfield.py +++ b/src/hope_flex_fields/admin/flexfield.py @@ -13,10 +13,7 @@ class FieldsetFieldTabularInline(TabularInline): model = FlexField show_change_link = True - fields = ( - "name", - "field", - ) + fields = ("name", "field", "attrs") @register(FlexField) @@ -37,15 +34,6 @@ class FieldsetFieldAdmin(ExtraButtonsMixin, ModelAdmin): ), ) - # - # formfield_overrides = { - # JSONField: { - # "widget": JSONEditor( - # init_options={"mode": "code", "modes": ["text", "code", "tree"]}, - # ace_options={"readOnly": False}, - # ) - # } - # } def get_changeform_initial_data(self, request): initial = super().get_changeform_initial_data(request) initial["attrs"] = "{}" @@ -55,7 +43,12 @@ def get_changeform_initial_data(self, request): def test(self, request, pk): ctx = self.get_common_context(request, pk, title="Test") fd: FieldDefinition = ctx["original"] - field = fd.get_field() + try: + field = fd.get_field() + except Exception as e: + self.message_user(request, str(e)) + field = fd.get_field({}) + form_class_attrs = { fd.name: field, } diff --git a/src/hope_flex_fields/apps.py b/src/hope_flex_fields/apps.py index 40faf93..f144ed6 100644 --- a/src/hope_flex_fields/apps.py +++ b/src/hope_flex_fields/apps.py @@ -2,12 +2,19 @@ from django.db.models.signals import post_migrate -def create_default_fields(sender, **kwargs): - from hope_flex_fields.models import FieldDefinition - from hope_flex_fields.registry import field_registry +def sync_content_types(sender, **kwargs): + from hope_flex_fields.models import Fieldset - for fld in field_registry: - FieldDefinition.objects.get_from_django_field(fld) + fs: Fieldset + changed = {} + for fs in Fieldset.objects.exclude(content_type__isnull=True): + current_attrs = Fieldset.objects.inspect_content_type(fs.content_type) + diff = fs.diff_content_type() + for field_name, w in diff.items(): + if ff := fs.get_field(field_name): + changed[field_name] = w + ff.attrs = current_attrs["config"][field_name]["attrs"] + ff.save() class Config(AppConfig): @@ -15,4 +22,4 @@ class Config(AppConfig): verbose_name = "Flex Fields" def ready(self): - post_migrate.connect(create_default_fields, sender=self) + post_migrate.connect(sync_content_types, sender=self) diff --git a/src/hope_flex_fields/fields.py b/src/hope_flex_fields/fields.py index 455d437..eaaeb05 100644 --- a/src/hope_flex_fields/fields.py +++ b/src/hope_flex_fields/fields.py @@ -1,5 +1,19 @@ +from typing import TYPE_CHECKING + from django import forms +if TYPE_CHECKING: + from hope_flex_fields.models import FlexField + class FlexFormMixin(forms.Field): - pass + flex_field: "FlexField" = None + + +# +# class OptionField(forms.ChoiceField): +# def __init__(self, *, endpoint=None, **kwargs): +# self.endpoint_nane = endpoint +# self.endpoint = Endpoint.objects.get(name=endpoint) +# kwargs.pop('choices', None) +# super().__init__(**kwargs) diff --git a/src/hope_flex_fields/migrations/0001_initial.py b/src/hope_flex_fields/migrations/0001_initial.py index 22d289a..bce48ba 100644 --- a/src/hope_flex_fields/migrations/0001_initial.py +++ b/src/hope_flex_fields/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.25 on 2024-07-09 10:49 +# Generated by Django 3.2.25 on 2024-07-09 17:33 from django.db import migrations, models import django.db.models.deletion diff --git a/src/hope_flex_fields/migrations/0002_auto_20240708_1316.py b/src/hope_flex_fields/migrations/0002_auto_20240708_1316.py deleted file mode 100644 index 665bcd3..0000000 --- a/src/hope_flex_fields/migrations/0002_auto_20240708_1316.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 3.2.25 on 2024-07-08 13:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hope_flex_fields', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='datachecker', - name='last_modified', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='datacheckerfieldset', - name='last_modified', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='fielddefinition', - name='last_modified', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='fieldset', - name='last_modified', - field=models.DateTimeField(auto_now=True), - ), - migrations.AddField( - model_name='flexfield', - name='last_modified', - field=models.DateTimeField(auto_now=True), - ), - migrations.AlterField( - model_name='datacheckerfieldset', - name='prefix', - field=models.CharField(blank=True, default='', max_length=30), - ), - ] diff --git a/src/hope_flex_fields/migrations/0002_fieldset_content_type.py b/src/hope_flex_fields/migrations/0002_fieldset_content_type.py new file mode 100644 index 0000000..5fd4614 --- /dev/null +++ b/src/hope_flex_fields/migrations/0002_fieldset_content_type.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.25 on 2024-07-13 17:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('hope_flex_fields', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='fieldset', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + ] diff --git a/src/hope_flex_fields/migrations/0003_alter_datachecker_id_alter_datacheckerfieldset_id_and_more.py b/src/hope_flex_fields/migrations/0003_alter_datachecker_id_alter_datacheckerfieldset_id_and_more.py new file mode 100644 index 0000000..0d69bdf --- /dev/null +++ b/src/hope_flex_fields/migrations/0003_alter_datachecker_id_alter_datacheckerfieldset_id_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.1 on 2024-10-06 16:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hope_flex_fields", "0002_fieldset_content_type"), + ] + + operations = [ + migrations.AlterField( + model_name="datachecker", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="datacheckerfieldset", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="fielddefinition", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="fieldset", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="flexfield", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/src/hope_flex_fields/migrations/0004_auto_20241009_0643.py b/src/hope_flex_fields/migrations/0004_auto_20241009_0643.py new file mode 100644 index 0000000..2d4af77 --- /dev/null +++ b/src/hope_flex_fields/migrations/0004_auto_20241009_0643.py @@ -0,0 +1,50 @@ +# Generated by Django 3.2.25 on 2024-10-09 06:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('hope_flex_fields', '0003_alter_datachecker_id_alter_datacheckerfieldset_id_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='fielddefinition', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='fielddefinition', + name='system_data', + field=models.JSONField(blank=True, default=dict, editable=False, null=True), + ), + migrations.AlterField( + model_name='datachecker', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='datacheckerfieldset', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='fielddefinition', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='fieldset', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + migrations.AlterField( + model_name='flexfield', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/src/hope_flex_fields/migrations/0005_remove_fielddefinition_unique_name_and_more.py b/src/hope_flex_fields/migrations/0005_remove_fielddefinition_unique_name_and_more.py new file mode 100644 index 0000000..da7ebe6 --- /dev/null +++ b/src/hope_flex_fields/migrations/0005_remove_fielddefinition_unique_name_and_more.py @@ -0,0 +1,56 @@ +# Generated by Django 5.1.2 on 2024-10-09 09:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ("hope_flex_fields", "0004_auto_20241009_0643"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="fielddefinition", + name="unique_name", + ), + migrations.AlterUniqueTogether( + name="flexfield", + unique_together=set(), + ), + migrations.AddField( + model_name="fielddefinition", + name="slug", + field=models.SlugField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="flexfield", + name="slug", + field=models.SlugField(blank=True, editable=False, null=True), + ), + migrations.AddConstraint( + model_name="fielddefinition", + constraint=models.UniqueConstraint( + fields=("name",), name="fielddefinition_unique_name" + ), + ), + migrations.AddConstraint( + model_name="fielddefinition", + constraint=models.UniqueConstraint( + fields=("slug",), name="fielddefinition_unique_slug" + ), + ), + migrations.AddConstraint( + model_name="flexfield", + constraint=models.UniqueConstraint( + fields=("name", "fieldset"), name="flexfield_unique_name" + ), + ), + migrations.AddConstraint( + model_name="flexfield", + constraint=models.UniqueConstraint( + fields=("slug", "fieldset"), name="flexfield_unique_slug" + ), + ), + ] diff --git a/src/hope_flex_fields/migrations/0006_alter_datachecker_id_alter_datacheckerfieldset_id_and_more.py b/src/hope_flex_fields/migrations/0006_alter_datachecker_id_alter_datacheckerfieldset_id_and_more.py new file mode 100644 index 0000000..51f3504 --- /dev/null +++ b/src/hope_flex_fields/migrations/0006_alter_datachecker_id_alter_datacheckerfieldset_id_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.1 on 2024-10-09 10:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("hope_flex_fields", "0005_remove_fielddefinition_unique_name_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="datachecker", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="datacheckerfieldset", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="fielddefinition", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="fieldset", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + migrations.AlterField( + model_name="flexfield", + name="id", + field=models.AutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/src/hope_flex_fields/migrations/0007_create_default_fields.py b/src/hope_flex_fields/migrations/0007_create_default_fields.py new file mode 100644 index 0000000..789e241 --- /dev/null +++ b/src/hope_flex_fields/migrations/0007_create_default_fields.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.2 on 2024-10-09 10:15 + +from django.db import migrations +from typing import TYPE_CHECKING + +from strategy_field.utils import fqn + +from hope_flex_fields.utils import get_kwargs_from_field_class, get_default_attrs + +if TYPE_CHECKING: + from hope_flex_fields.models import FieldDefinition + + +def create_default_fields(apps, schema_editor): + from hope_flex_fields.registry import field_registry + + fd: "FieldDefinition" = apps.get_model("hope_flex_fields", "FieldDefinition") + + for fld in field_registry: + name = fld.__name__ + fd.objects.get_or_create(name=name, + field_type=fqn(fld), + defaults={"attrs": get_kwargs_from_field_class(fld, get_default_attrs())} + ) + + +def remove_default_fields(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + dependencies = [ + ( + "hope_flex_fields", + "0006_alter_datachecker_id_alter_datacheckerfieldset_id_and_more", + ), + ] + + operations = [ + migrations.RunPython(create_default_fields, remove_default_fields), + ] diff --git a/src/hope_flex_fields/models/__init__.py b/src/hope_flex_fields/models/__init__.py index f9d9c3f..c7b294a 100644 --- a/src/hope_flex_fields/models/__init__.py +++ b/src/hope_flex_fields/models/__init__.py @@ -1,4 +1,3 @@ -from .base import get_default_attrs # noqa from .datachecker import DataChecker, DataCheckerFieldset # noqa from .definition import FieldDefinition # noqa from .fieldset import Fieldset # noqa diff --git a/src/hope_flex_fields/models/base.py b/src/hope_flex_fields/models/base.py index 87231fd..440a1c0 100644 --- a/src/hope_flex_fields/models/base.py +++ b/src/hope_flex_fields/models/base.py @@ -1,20 +1,17 @@ import logging -from types import GeneratorType -from typing import Iterable +from typing import Generator, Iterable from django import forms from django.db import models +from django.utils.text import slugify from django_regex.fields import RegexField from django_regex.validators import RegexValidator +# logger = logging.getLogger(__name__) -def get_default_attrs(): - return {"required": False, "help_text": ""} - - class FlexForm(forms.Form): fieldset = None @@ -26,32 +23,100 @@ class AbstractField(models.Model): attrs = models.JSONField(default=dict, blank=True, null=False) regex = RegexField(blank=True, null=True, validators=[RegexValidator()]) validation = models.TextField(blank=True, null=True, default="") + slug = models.SlugField(blank=True, null=True, editable=False) class Meta: abstract = True + def save( + self, + *args, + force_insert=False, + force_update=False, + using=None, + update_fields=None, + ): + if not self.slug: + self.slug = slugify(self.name) + super().save( + *args, + force_insert=force_insert, + force_update=force_update, + using=using, + update_fields=update_fields, + ) + class ValidatorMixin: + def __init__(self, *args, **kwargs): + self._primary_key_field_name = None + self._master_fieldset = None + self.primary_keys = set() + # self._collect_values = [] + self._collected_values = {} + super().__init__(*args, **kwargs) + + def set_primary_key_col(self, name: str): + self._primary_key_field_name = name + + def set_master(self, fs: "ValidatorMixin", col_name: str): + self._master_fieldset = fs + self._master_fieldset_col = col_name + + def collect(self, *fields): + for field_name in fields: + self._collected_values[field_name] = [] + + def collected(self, field_name): + return self._collected_values[field_name] + + def is_duplicate(self, form): + if self._primary_key_field_name: + if pk := form.cleaned_data[self._primary_key_field_name]: + if pk in self.primary_keys: + return f"{pk} duplicated" + self.primary_keys.add(str(pk).strip()) + + def is_valid_foreignkey(self, form): + if self._master_fieldset: + fk = form.cleaned_data[self._master_fieldset_col] + if fk not in self._master_fieldset.primary_keys: + return f"'{fk}' not found in master" def validate( - self, data: Iterable, include_success: bool = False, fail_if_alien: bool = False + self, + data: Iterable, + *, + include_success: bool = False, + fail_if_alien: bool = False, ): - if not isinstance(data, (list, tuple, GeneratorType)): + if not isinstance(data, (list, tuple, Generator)): data = [data] + self.primary_keys = set() form_class: type[FlexForm] = self.get_form() known_fields = set(sorted(form_class.declared_fields.keys())) ret = {} for i, row in enumerate(data, 1): form: "FlexForm" = form_class(data=row) posted_fields = set(sorted(row.keys())) - row_errors = {} + fields_errors = {} + row_errors = [] if fail_if_alien and (diff := posted_fields.difference(known_fields)): - row_errors["-"] = [f"Alien values found {diff}"] + row_errors.append(f"Alien values found {diff}") if not form.is_valid(): - row_errors.update(**form.errors) + fields_errors.update(**form.errors) + + if err := self.is_duplicate(form): + row_errors.append(err) + if err := self.is_valid_foreignkey(form): + row_errors.append(err) + for field_name in self._collected_values.keys(): + self._collected_values[field_name].append(form.cleaned_data[field_name]) if row_errors: - ret[i] = row_errors + fields_errors["-"] = row_errors + if fields_errors: + ret[i] = fields_errors elif include_success: ret[i] = "Ok" return ret diff --git a/src/hope_flex_fields/models/datachecker.py b/src/hope_flex_fields/models/datachecker.py index d216815..7ccf479 100644 --- a/src/hope_flex_fields/models/datachecker.py +++ b/src/hope_flex_fields/models/datachecker.py @@ -11,7 +11,7 @@ from .fieldset import Fieldset if TYPE_CHECKING: - from .flexfield import FLexField + from .flexfield import FlexField def create_xls_importer(dc: "DataChecker"): @@ -79,6 +79,8 @@ class DataCheckerFieldset(models.Model): class DataChecker(ValidatorMixin, models.Model): + """Used for complex validations to combine different fieldsets""" + last_modified = models.DateTimeField(auto_now=True) name = models.CharField(max_length=255, unique=True) description = models.TextField(blank=True) @@ -102,11 +104,16 @@ def get_fields(self): def get_form(self) -> "type[FlexForm]": fields: dict[str, forms.Field] = {} - field: "FLexField" + field: "FlexField" for fs in self.members.all(): for field in fs.fieldset.fields.filter(): fld: FlexFormMixin = field.get_field() - fld.label = f"{fs.prefix}_{field.name}" - fields[f"{fs.prefix}{field.name}"] = fld + fld.label = f"{fs.prefix}{field.name}" + if "%s" in fs.prefix: + full_name = fs.prefix % field.name + else: + full_name = f"{fs.prefix}{field.name}" + + fields[full_name] = fld form_class_attrs = {"DataChecker": self, **fields} return type(f"{self.name}DataChecker", (FlexForm,), form_class_attrs) diff --git a/src/hope_flex_fields/models/definition.py b/src/hope_flex_fields/models/definition.py index 11edcda..da1ebe8 100644 --- a/src/hope_flex_fields/models/definition.py +++ b/src/hope_flex_fields/models/definition.py @@ -2,6 +2,7 @@ from inspect import isclass from django import forms +from django.contrib.contenttypes.models import ContentType from django.db import models from django.db.models import UniqueConstraint from django.utils.translation import gettext as _ @@ -13,8 +14,9 @@ from ..fields import FlexFormMixin from ..registry import field_registry +from ..utils import get_default_attrs from ..validators import JsValidator, ReValidator -from .base import AbstractField, get_default_attrs +from .base import AbstractField logger = logging.getLogger(__name__) @@ -40,13 +42,24 @@ def get_from_django_field(self, django_field: "forms.Field|type[forms.Field]"): class FieldDefinition(AbstractField): + """This class is the equivalent django.forms.Field class, used to create reusable field types""" + field_type = StrategyClassField(registry=field_registry) + content_type = models.ForeignKey( + ContentType, on_delete=models.CASCADE, null=True, blank=True + ) + system_data = models.JSONField(default=dict, blank=True, editable=False, null=True) + # protected = models.BooleanField(default=False, help_text="If true the field can be deleted only by superusers") + objects = FieldDefinitionManager() class Meta: verbose_name = _("Field Definition") verbose_name_plural = _("Field Definitions") - constraints = (UniqueConstraint(fields=("name",), name="unique_name"),) + constraints = ( + UniqueConstraint(fields=("name",), name="fielddefinition_unique_name"), + UniqueConstraint(fields=("slug",), name="fielddefinition_unique_slug"), + ) def __str__(self): return self.name @@ -75,9 +88,12 @@ def set_default_arguments(self): def required(self): return self.attrs.get("required", False) - def get_field(self): + def get_field(self, override_attrs=None): try: - kwargs = dict(self.attrs) + if override_attrs is not None: + kwargs = dict(override_attrs) + else: + kwargs = dict(self.attrs) validators = [] if self.validation: validators.append(JsValidator(self.validation)) @@ -91,5 +107,7 @@ def get_field(self): fld = field_class(**kwargs) except Exception as e: # pragma: no cover logger.exception(e) - raise + raise TypeError( + f"Error creating field for FieldDefinition {self.name}: {e}" + ) return fld diff --git a/src/hope_flex_fields/models/fieldset.py b/src/hope_flex_fields/models/fieldset.py index 2c1cb69..b57fadf 100644 --- a/src/hope_flex_fields/models/fieldset.py +++ b/src/hope_flex_fields/models/fieldset.py @@ -1,27 +1,110 @@ -from typing import TYPE_CHECKING +import logging +from typing import TYPE_CHECKING, Any, Optional, TypedDict from django import forms +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ValidationError from django.db import models +from django.forms import modelform_factory from django.utils.translation import gettext as _ +from deepdiff import DeepDiff + +from ..utils import get_kwargs_from_formfield from .base import FlexForm, ValidatorMixin if TYPE_CHECKING: - from ..forms import FieldDefinitionForm - from .flexfield import FlexField + from ..models import FlexField + +ContentTypeConfig = TypedDict( + "ContentTypeConfig", + { + "fields": list, + "errors": list, + "config": dict[str, Any], + "content_type": type[ContentType], + }, +) +# { +# "fields": fields, +# "errors": errors, +# "config": config, +# "content_type": ct, +# } + +logger = logging.getLogger(__name__) class FieldsetManager(models.Manager): def get_by_natural_key(self, name): return self.get(name=name) + def inspect_content_type(self, ct: ContentType) -> ContentTypeConfig: + from hope_flex_fields.models import FieldDefinition, FlexField + + model_class = ct.model_class() + model_form = modelform_factory( + model_class, exclude=(model_class._meta.pk.name,) + ) + errors = [] + fields = [] + config = {} + for name, field in model_form().fields.items(): + try: + fd = FieldDefinition.objects.get(name=type(field).__name__) + fld = FlexField( + name=name, field=fd, attrs=get_kwargs_from_formfield(field) + ) + fld.attrs = fld.get_merged_attrs() + fld.get_field() + config[name] = {"definition": fd.name, "attrs": fld.attrs} + fields.append(fld) + except FieldDefinition.DoesNotExist: + errors.append( + { + "name": name, + "error": f"Field definition for '{type(field).__name__}' does not exist", + } + ) + except Exception as e: + logger.exception(e) + errors.append( + { + "name": name, + "error": f"Unable to create field {name}", + } + ) + return { + "fields": fields, + "errors": errors, + "config": config, + "content_type": ct, + } + + def create_from_content_type( + self, name: str, content_type: ContentType, config: Optional[dict] = None + ) -> "Fieldset": + from hope_flex_fields.models import FieldDefinition, Fieldset + + if config is None: + inspection = Fieldset.objects.inspect_content_type(content_type) + config = inspection["config"] + fs, __ = Fieldset.objects.get_or_create(name=name, content_type=content_type) + for name, info in config.items(): + fd = FieldDefinition.objects.get(name=info["definition"]) + fs.fields.get_or_create(name=name, field=fd, attrs=info["attrs"]) + return fs + class Fieldset(ValidatorMixin, models.Model): last_modified = models.DateTimeField(auto_now=True) name = models.CharField(max_length=255, unique=True) description = models.TextField(blank=True) extends = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE) + content_type = models.ForeignKey( + ContentType, on_delete=models.CASCADE, blank=True, null=True + ) + objects = FieldsetManager() class Meta: @@ -31,12 +114,25 @@ class Meta: def __str__(self): return self.name + def diff_content_type(self) -> dict: + attrs = Fieldset.objects.inspect_content_type(self.content_type) + result = {} + for field_name, w in attrs["config"].items(): + if ff := self.get_field(field_name): + dd = DeepDiff(ff.attrs, w["attrs"]) + if dd: + result[field_name] = dd + return result + def natural_key(self): return (self.name,) def get_field(self, name) -> "FlexField": ff = [f for f in self.get_fields() if f.name == name] - return ff[0] + return ff[0] if ff else None + + def get_fieldnames(self): + return [f.name for f in self.get_fields()] def get_fields(self): local_names = [f.name for f in self.fields.all()] @@ -47,7 +143,7 @@ def get_fields(self): for f in self.fields.all(): yield f - def get_form(self) -> "type[FieldDefinitionForm]": + def get_form(self) -> "type": fields: dict[str, forms.Field] = {} field: "FlexField" @@ -61,12 +157,3 @@ def clean(self): super().clean() if self.extends == self: raise ValidationError({"extends": "Cannot extends itself"}) - - # def validate(self, data): - # form_class = self.get_form() - # form: "FieldDefinitionForm" = form_class(data=data) - # if form.is_valid(): - # return True - # else: - # self.errors = form.errors - # raise ValidationError(form.errors) diff --git a/src/hope_flex_fields/models/flexfield.py b/src/hope_flex_fields/models/flexfield.py index 57fabc3..ee7271e 100644 --- a/src/hope_flex_fields/models/flexfield.py +++ b/src/hope_flex_fields/models/flexfield.py @@ -2,6 +2,7 @@ from django.core.exceptions import ValidationError from django.db import models +from django.db.models import UniqueConstraint from django.utils.translation import gettext as _ from ..fields import FlexFormMixin @@ -30,9 +31,12 @@ class FlexField(AbstractField): objects = FieldsetFieldManager() class Meta: - unique_together = ("fieldset", "name") verbose_name = _("Flex Field") verbose_name_plural = _("flex Fields") + constraints = ( + UniqueConstraint(fields=("name", "fieldset"), name="flexfield_unique_name"), + UniqueConstraint(fields=("slug", "fieldset"), name="flexfield_unique_slug"), + ) def __str__(self): return self.name @@ -59,10 +63,13 @@ def get_merged_attrs(self): attrs.update(self.attrs) return attrs - def get_field(self, **extra) -> "FlexFormMixin": + def get_field(self, override_attrs=None, **extra) -> "FlexFormMixin": try: - kwargs = self.get_merged_attrs() - kwargs.update(extra) + if override_attrs is not None: + kwargs = dict(override_attrs) + else: + kwargs = self.get_merged_attrs() + kwargs.update(extra) validators = [] if self.validation: validators.append(JsValidator(self.validation)) @@ -76,10 +83,12 @@ def get_field(self, **extra) -> "FlexFormMixin": kwargs["validators"] = validators field_class = type( - f"{self.name}Field", (FlexFormMixin, self.field.field_type), {} + f"{self.name}Field", + (FlexFormMixin, self.field.field_type), + {"flex_field": self}, ) fld = field_class(**kwargs) except Exception as e: # pragma: no cover logger.exception(e) - raise + raise TypeError(f"Error creating field for FlexField {self.name}: {e}") return fld diff --git a/src/hope_flex_fields/registry.py b/src/hope_flex_fields/registry.py index 65355de..e2f179f 100644 --- a/src/hope_flex_fields/registry.py +++ b/src/hope_flex_fields/registry.py @@ -3,6 +3,8 @@ from strategy_field.registry import Registry from strategy_field.utils import fqn +# from .fields import OptionField + class FieldRegistry(Registry): def get_name(self, entry): @@ -35,3 +37,4 @@ def as_choices(self): field_registry.register(forms.URLField) field_registry.register(forms.UUIDField) field_registry.register(forms.JSONField) +# field_registry.register(OptionField) diff --git a/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html b/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html index d5d3887..3322863 100644 --- a/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html +++ b/src/hope_flex_fields/templates/flex_fields/fieldset/analyse.html @@ -1,6 +1,5 @@ {% extends "admin_extra_buttons/action_page.html" %} {% block action-content %} - {{ original }} {% if fields %}
{% csrf_token %} @@ -35,7 +34,7 @@

Fields

{% csrf_token %} {{ form }} - +
{% endif %} diff --git a/src/hope_flex_fields/templates/flex_fields/fieldset/diff.html b/src/hope_flex_fields/templates/flex_fields/fieldset/diff.html new file mode 100644 index 0000000..987e1be --- /dev/null +++ b/src/hope_flex_fields/templates/flex_fields/fieldset/diff.html @@ -0,0 +1,12 @@ +{% extends "admin_extra_buttons/action_page.html" %} +{% block action-content %} + + {% for field_name, diff in diff %} + + + + + {% endfor %} +
{{ field_name }}{{ diff }}
+ +{% endblock %} diff --git a/src/hope_flex_fields/utils.py b/src/hope_flex_fields/utils.py index 334a46c..bf70875 100644 --- a/src/hope_flex_fields/utils.py +++ b/src/hope_flex_fields/utils.py @@ -14,6 +14,10 @@ def namefy(value): return slugify(value).replace("-", "_") +def get_default_attrs(): + return {"required": False, "help_text": ""} + + def get_kwargs_from_field_class(field, extra: dict | None = None): sig: inspect.Signature = inspect.signature(field) arguments = extra or {} diff --git a/tests/admin/test_admin_checker.py b/tests/admin/test_admin_checker.py index a329a3f..881a9c4 100644 --- a/tests/admin/test_admin_checker.py +++ b/tests/admin/test_admin_checker.py @@ -7,6 +7,8 @@ from testutils.factories import DataCheckerFactory from webtest import Upload +from hope_flex_fields.models import Fieldset + pytestmark = [pytest.mark.admin, pytest.mark.smoke, pytest.mark.django_db] @@ -27,7 +29,7 @@ def record(db): FlexFieldFactory(name="int2", field=fd2, fieldset=fs2, attrs={"required": True}) dc = DataCheckerFactory() - DataCheckerFieldsetFactory(checker=dc, fieldset=fs1, prefix="fs1_") + DataCheckerFieldsetFactory(checker=dc, fieldset=fs1, prefix="fs1_%s") DataCheckerFieldsetFactory(checker=dc, fieldset=fs2, prefix="fs2_") return dc @@ -313,6 +315,19 @@ def test_datachecker_inspect(app, record): assert res +def test_datachecker_unique_field(app, record): + url = reverse("admin:hope_flex_fields_datachecker_add") + res = app.get(url) + res.forms["datachecker_form"]["name"] = "DC #1" + res.forms["datachecker_form"]["members-0-fieldset"] = Fieldset.objects.first().pk + res.forms["datachecker_form"]["members-0-prefix"] = "pr_" + res.forms["datachecker_form"]["members-1-fieldset"] = Fieldset.objects.first().pk + res.forms["datachecker_form"]["members-1-prefix"] = "pr_%s" + res = res.forms["datachecker_form"].submit() + assert res.status_code == 200 + assert b"Field names are not unique" in res.content + + def test_datachecker_xls_importer(app, dc): url = reverse( "admin:hope_flex_fields_datachecker_create_xls_importer", args=[dc.pk] diff --git a/tests/admin/test_admin_fieldset.py b/tests/admin/test_admin_fieldset.py index 9ca647a..282f7ec 100644 --- a/tests/admin/test_admin_fieldset.py +++ b/tests/admin/test_admin_fieldset.py @@ -25,6 +25,18 @@ def record(db): return fs +@pytest.fixture +def record2(db): + ct = ContentType.objects.get_for_model(User) + fs = Fieldset.objects.create_from_content_type("Test", ct) + return fs + + +def test_detect_changes(app, record2): + url = reverse("admin:hope_flex_fields_fieldset_detect_changes", args=[record2.pk]) + app.get(url) + + def test_fieldset_test(app, record): url = reverse("admin:hope_flex_fields_fieldset_test", args=[record.pk]) res = app.get(url) @@ -39,24 +51,34 @@ def test_fieldset_test(app, record): assert messages == ["Valid"] +def test_fieldset_unique_name(app, record): + url = reverse("admin:hope_flex_fields_fieldset_add") + res = app.get(url) + res.forms["fieldset_form"]["name"] = record.name + res = res.forms["fieldset_form"].submit() + assert res.status_code == 200 + assert b"Fieldset with this Name already exists." in res.content + + @pytest.mark.parametrize( "model_class", [ User, ], ) -def test_fieldset_create_from_content_type(app, model_class): +def test_fieldset_create_from_content_type(app, record, model_class): url = reverse("admin:hope_flex_fields_fieldset_create_from_content_type") res = app.get(url) - res = res.forms["analyse-form"].submit() + res.forms["analyse-form"]["name"] = record.name + res = res.forms["analyse-form"].submit("analyse") assert res.status_code == 200 + res.forms["analyse-form"]["name"] = "FS #1" res.forms["analyse-form"]["content_type"] = ContentType.objects.get_for_model( model_class ).pk - res = res.forms["analyse-form"].submit() - res.forms["create-form"].submit() - fs = Fieldset.objects.filter( - name=f"{model_class._meta.app_label}_{model_class._meta.model_name}" - ).first() + res = res.forms["analyse-form"].submit("analyse") + + res.forms["create-form"].submit("create") + fs = Fieldset.objects.filter(name="FS #1").first() assert fs assert fs.fields.exists() diff --git a/tests/admin/test_admin_flexfield.py b/tests/admin/test_admin_flexfield.py index 3915741..bb2b451 100644 --- a/tests/admin/test_admin_flexfield.py +++ b/tests/admin/test_admin_flexfield.py @@ -46,7 +46,7 @@ def test_fields_create(app): fs = FieldsetFactory() url = reverse("admin:hope_flex_fields_flexfield_add") res = app.get(url) - form = res.forms[1] + form = res.forms["flexfield_form"] form["name"] = "int" form["field"] = fd.pk form["fieldset"] = fs.pk @@ -62,7 +62,7 @@ def test_fields_create_and_update(app, record): url = reverse("admin:hope_flex_fields_flexfield_add") res = app.get(url) - form = res.forms[1] + form = res.forms["flexfield_form"] form["name"] = "int2" form["field"] = fd.pk form["fieldset"] = fs.pk @@ -70,7 +70,7 @@ def test_fields_create_and_update(app, record): res = form.submit("_continue") assert res.status_code == 302, res.context["adminform"].form.errors res = res.follow() - form = res.forms[1] + form = res.forms["flexfield_form"] assert form["attrs"].value == '"{}"' form["attrs"] = ( diff --git a/tests/admin/test_admin_smoke.py b/tests/admin/test_admin_smoke.py index 351c89a..ea74068 100644 --- a/tests/admin/test_admin_smoke.py +++ b/tests/admin/test_admin_smoke.py @@ -181,7 +181,9 @@ def test_admin_delete(app, modeladmin, record, monkeypatch): pytest.skip("No 'delete' permission") -@pytest.mark.skip_buttons("security.UserAdmin:link_user_data") +@pytest.mark.skip_buttons( + "security.UserAdmin:link_user_data", "hope_flex_fields.FieldsetAdmin:detect_changes" +) def test_admin_buttons(app, modeladmin, button_handler, record, monkeypatch): from admin_extra_buttons.handlers import LinkHandler diff --git a/tests/extra/testutils/factories.py b/tests/extra/testutils/factories.py index fa58a34..0ef8414 100644 --- a/tests/extra/testutils/factories.py +++ b/tests/extra/testutils/factories.py @@ -1,5 +1,6 @@ from django import forms from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType import factory.fuzzy from factory.base import FactoryMetaClass @@ -86,6 +87,15 @@ def _create(cls, model_class, *args, **kwargs): return super()._create(model_class, *args, **kwargs) +class ContentTypeFactory(AutoRegisterModelFactory): + app_label = "auth" + model = "user" + + class Meta: + model = ContentType + django_get_or_create = ("app_label", "model") + + class FieldsetFactory(AutoRegisterModelFactory): name = factory.Sequence(lambda d: "Fieldset-%s" % d) extends = None diff --git a/tests/test_choices.py b/tests/test_choices.py new file mode 100644 index 0000000..dba0136 --- /dev/null +++ b/tests/test_choices.py @@ -0,0 +1,35 @@ +from django.contrib.contenttypes.models import ContentType + +import pytest +from testutils.factories import ContentTypeFactory, FieldsetFactory, FlexFieldFactory + +from hope_flex_fields.apps import sync_content_types +from hope_flex_fields.models import Fieldset + + +@pytest.fixture +def data(db): + ct = ContentType.objects.get(app_label="auth", model="user") + data = Fieldset.objects.inspect_content_type(ct) + Fieldset.objects.create_from_content_type( + name="FS #2", content_type=ct, config=data["config"] + ) + + +def test_sync_content_types(data): + fs = FieldsetFactory(content_type=ContentTypeFactory()) + attrs = Fieldset.objects.inspect_content_type(fs.content_type) + FlexFieldFactory(fieldset=fs, field__name="CharField", name="username") + FlexFieldFactory( + fieldset=fs, + field__name="CharField", + name="password", + attrs=attrs["config"]["password"], + ) + FlexFieldFactory( + fieldset=fs, + field__name="CharField", + name="password", + attrs=attrs["config"]["password"], + ) + sync_content_types(None) diff --git a/tests/test_sample_code.py b/tests/test_sample_code.py new file mode 100644 index 0000000..e9a3eb0 --- /dev/null +++ b/tests/test_sample_code.py @@ -0,0 +1,42 @@ +# mypy: disable-error-code="no-untyped-def" +from django import forms + +from hope_flex_fields.models import Fieldset +from hope_flex_fields.models.definition import FieldDefinition +from hope_flex_fields.models.flexfield import FlexField + + +def test_sample_code(db): + data = [ + {"name": "John", "last_name": "Doe", "gender": "M", "unknown": "??"}, + {"name": "Jane", "last_name": "Doe", "gender": "F"}, + {"name": "Andrea", "last_name": "Doe", "gender": "X"}, + {"name": "Mary", "last_name": "Doe", "gender": "1"}, + ] + + fs, __ = Fieldset.objects.get_or_create(name="test.xlsx") + + charfield = FieldDefinition.objects.get(field_type=forms.CharField) + choicefield = FieldDefinition.objects.get(field_type=forms.ChoiceField) + + FlexField.objects.get_or_create(name="name", fieldset=fs, field=charfield) + FlexField.objects.get_or_create(name="last_name", fieldset=fs, field=charfield) + FlexField.objects.get_or_create( + name="gender", + fieldset=fs, + field=choicefield, + attrs={"choices": [["M", "M"], ["F", "F"], ["X", "X"]]}, + ) + + errors = fs.validate(data) + assert errors == { + 4: {"gender": ["Select a valid choice. 1 is not one of the available choices."]} + } + + errors = fs.validate(data, fail_if_alien=True) + assert errors == { + 1: {"-": ["Alien values found {'unknown'}"]}, + 4: { + "gender": ["Select a valid choice. 1 is not one of the available choices."] + }, + } diff --git a/tests/test_usage.py b/tests/test_usage.py index ae7f5bc..7421466 100644 --- a/tests/test_usage.py +++ b/tests/test_usage.py @@ -37,7 +37,7 @@ def test_validate_fs_json(config): {"int": 2, "float": 2.2}, {"int": -3, "float": 2.1}, ] - result = fs.validate(data, True) + result = fs.validate(data, include_success=True) assert result == { 1: {"float": ["Insert an odd number"]}, 2: "Ok", @@ -52,7 +52,7 @@ def test_validate_dc_json(config): {"aaa_int": 2, "aaa_float": 2.2}, {"aaa_int": -3, "aaa_float": 2.1}, ] - result = dc.validate(data, True) + result = dc.validate(data, include_success=True) assert result == { 1: {"aaa_float": ["Insert an odd number"]}, 2: "Ok", diff --git a/tox.ini b/tox.ini index f4e8e7b..dd090a9 100644 --- a/tox.ini +++ b/tox.ini @@ -1,51 +1,55 @@ [tox] -envlist = d{32,42,50} +envlist = d{42,51} envtmpdir={toxinidir}/build/{envname}/tmp envlogdir={toxinidir}/build/{envname}/log [testenv] basepython=python3.11 +skip_install = true passenv = PYTHONDONTWRITEBYTECODE USER PYTHONPATH DATABASE_URL DATABASE_HOPE_URL - SECRET_KEY - AZURE_CLIENT_ID - AZURE_CLIENT_SECRET +deps = + uv setenv = PYTHONDONTWRITEBYTECODE=true PYTHONPATH={toxinidir}/src -extras = - test -deps = - d32: django==3.2.* - d42: django==4.2.* - d50: django==5.0.* - -allowlist_externals = - flake8 - isort - black - mkdir - pytest + d42: DJANGO = django>=4,<5 + d51: DJANGO = django>=5,<6 + d42: LOCK = "uv4.lock" + d51: LOCK = "uv5.lock" commands = - mkdir -p {toxinidir}/~build/flake {toxinidir}/build/results - flake8 src/ tests/ --format=html --htmldir=~build/flake - isort src/ tests/ --check-only - black --check src/ tests/ - pytest tests \ - -q \ - --create-db \ - --cov-report=html \ - --cov-report=term \ - --cov-config={toxinidir}/tests/.coveragerc \ - --cov=hope_dedup_engine + uv export -q --no-hashes -o {work_dir}/requirements.txt + pip install -r {work_dir}/requirements.txt + pip install '{env:DJANGO}' + pytest + + [testenv:report] commands = pip install coverage coverage html + + +[testenv:package] +skip_install = true + +deps= + build + twine + + +setenv = + TWINE_USERNAME = {env:TWINE_TEST_USERNAME:__token__} + TWINE_PASSWORD = {env:TWINE_TEST_PASSWORD} + +commands = + python -c "import shutil; shutil.rmtree('{toxinidir}/dist', ignore_errors=True)" + python -m build --outdir {toxinidir}/dist + pip install hope-flex-fields --use-pep517 --no-deps --no-cache-dir --find-links file://{toxinidir}/dist/