diff --git a/Makefile b/Makefile index 7924b9cf..693575a6 100644 --- a/Makefile +++ b/Makefile @@ -9,8 +9,8 @@ GREEN=`tput setaf 2` RESET=`tput sgr0` YELLOW=`tput setaf 3` -TOP_LEVEL_TEMPLATES = add-ons/backend add-ons/frontend projects/monorepo projects/classic -SUB_TEMPLATES = sub/cache sub/frontend_project sub/project_settings +TOP_LEVEL_TEMPLATES = add-ons/backend add-ons/frontend add-ons/monorepo projects/monorepo projects/classic +SUB_TEMPLATES = ci/github sub/cache sub/frontend_project sub/addon_settings sub/classic_project_settings sub/project_settings sub/vscode # Python checks UV?=uv @@ -88,3 +88,7 @@ report-keys-usage: $(VENV_FOLDER) ## Generate a report of usage of context keys @echo "$(GREEN)==> Generate a report of usage of context keys$(RESET)" @uv run .scripts/report_keys_usage.py +.PHONY: run +run: $(VENV_FOLDER) ## Run cookieplone + @echo "$(GREEN)==> Run cookieplone$(RESET)" + COOKIEPLONE_REPOSITORY=$(CURRENT_DIR) uv run cookieplone diff --git a/cookiecutter.json b/cookiecutter.json index 94a6a747..b6088b0b 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -3,25 +3,31 @@ "project": { "path": "./templates/projects/monorepo", "title": "Volto Project", - "description": "Create a new Plone project that uses the Volto frontend", + "description": "Plone project that uses the Volto frontend", "hidden": false }, "classic_project": { "path": "./templates/projects/classic", "title": "Classic UI Project", - "description": "Create a new Plone project that uses Classic UI", + "description": "Plone project that uses Classic UI", + "hidden": false + }, + "monorepo_addon": { + "path": "./templates/add-ons/monorepo", + "title": "Add-on for Plone (with backend and frontend)", + "description": "Codebase for a Plone add-on with backend and frontend parts", "hidden": false }, "backend_addon": { "path": "./templates/add-ons/backend", "title": "Backend Add-on for Plone", - "description": "Create a new Python package to be used with Plone", + "description": "Python package to be used with Plone", "hidden": false }, "frontend_addon": { "path": "./templates/add-ons/frontend", "title": "Frontend Add-on for Plone", - "description": "Create a new Node package to be used with Volto", + "description": "Node package to be used with Volto", "hidden": false }, "sub/cache": { @@ -36,6 +42,12 @@ "description": "Subtemplate with configuration used in container images for frontend project.", "hidden": true }, + "sub/addon_settings": { + "path": "./templates/sub/addon_settings", + "title": "Settings to be applied on top of a mono repo add-on codebase", + "description": "Subtemplate with configuration and settings for a mono repo add-on.", + "hidden": true + }, "sub/project_settings": { "path": "./templates/sub/project_settings", "title": "Project settings to be applied on top of a mono repo project", @@ -48,11 +60,29 @@ "description": "Subtemplate with configuration and settings for a Classic UI project.", "hidden": true }, + "sub/vscode": { + "path": "./templates/sub/vscode", + "title": "VSCode settings", + "description": "Subtemplate with configuration and settings for VSCode.", + "hidden": true + }, "documentation_starter": { "path": "./templates/docs/starter", "title": "Documentation scaffold for Plone projects", "description": "Create a new documentation scaffold for Plone projects", "hidden": false + }, + "ci_github": { + "path": "./templates/ci/github", + "title": "GitHub CI configuration for Plone projects", + "description": "Create a new GitHub CI configuration for Plone projects", + "hidden": true + }, + "devops_ansible": { + "path": "./templates/devops/ansible", + "title": "Ansible Playbooks for Plone", + "description": "Ansible setup to manage a Docker Swarm cluster for Plone hosting.", + "hidden": true } } } diff --git a/news/+monorepo-calver.feature b/news/+monorepo-calver.feature new file mode 100644 index 00000000..2f7f51cf --- /dev/null +++ b/news/+monorepo-calver.feature @@ -0,0 +1 @@ +Project monorepo uses calendar versioning. @ericof diff --git a/news/+monorepo-gha.feature b/news/+monorepo-gha.feature new file mode 100644 index 00000000..b30269bf --- /dev/null +++ b/news/+monorepo-gha.feature @@ -0,0 +1 @@ +Refactored Github actions for monorepo projects. @ericof diff --git a/news/+monorepo.feature b/news/+monorepo.feature new file mode 100644 index 00000000..bc5e00a8 --- /dev/null +++ b/news/+monorepo.feature @@ -0,0 +1 @@ +Refactor monorepo project to use uv and repoplone. @ericof diff --git a/pyproject.toml b/pyproject.toml index 6b64ec4f..82975ec1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ description = "Collection of templates for Plone integrators to use through Cook readme = "README.md" requires-python = ">=3.10" dependencies = [ - "cookieplone>=0.9.7", + "cookieplone>=0.9.10", "gitpython>=3.1.43", "pytest>=8.3.5", "pytest-cookies>=0.7.0", - "pytest-jsonschema>=1.0.0b1", + "pytest-jsonschema>=1.0.0", "pytest-md-report>=0.6.3", "tomli>=2.0.1", ] diff --git a/templates/add-ons/backend/cookiecutter.json b/templates/add-ons/backend/cookiecutter.json index 4057566d..3416ad77 100644 --- a/templates/add-ons/backend/cookiecutter.json +++ b/templates/add-ons/backend/cookiecutter.json @@ -8,6 +8,8 @@ "plone_version": "{{ cookiecutter.use_prerelease_versions | latest_plone }}", "python_package_name": "{{ cookiecutter.github_organization|lower }}.{{ cookiecutter.title|replace(' ', '')|replace('-', '_')|replace('.', '')|lower }}", "__year": "{% now 'local', '%Y' %}", + "initial_version": "1.0.0a0", + "configuration_version": "1000", "feature_headless": ["1", "0"], "initialize_documentation": ["1", "0"], "__project_slug": "{{ cookiecutter.python_package_name }}", @@ -28,8 +30,8 @@ "__gha_plone_version": "{{ cookiecutter.plone_version }}", "__python_version_identifier": "{{ cookiecutter.__supported_versions_python[0] | replace('.', '') }}", "__profile_language": "en", - "__version_package": "1.0.0a0", - "__profile_version": "1000", + "__version_package": "{{ cookiecutter.initial_version }}", + "__profile_version": "{{ cookiecutter.configuration_version }}", "__backend_addon_git_initialize": "1", "__backend_addon_format": "1", "__prompts__": { @@ -41,6 +43,8 @@ "python_package_name": "Python package name", "author": "Author", "email": "Author E-mail", + "initial_version": "Package version", + "configuration_version": "Configuration version", "feature_headless": { "__prompt__": "Support headless Plone?", "1": "Yes", diff --git a/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/.gitignore b/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/.gitignore index a39c756d..d2260f20 100644 --- a/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/.gitignore +++ b/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/.gitignore @@ -40,5 +40,6 @@ var/ /reports/ /sources/ /venv/ +.mxdev_cache constraints*.txt requirements*.txt \ No newline at end of file diff --git a/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/pyproject.toml b/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/pyproject.toml index 6ec70b85..e7be2c06 100644 --- a/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/pyproject.toml +++ b/templates/add-ons/backend/{{ cookiecutter.__folder_name }}/pyproject.toml @@ -53,7 +53,7 @@ test = [ "plone.restapi[test]", "pytest", "pytest-cov", - "pytest-plone>=0.5.0", + "pytest-plone>=1.0.0a2", ] release = [ "zest.releaser[recommended]", diff --git a/templates/add-ons/frontend/cookiecutter.json b/templates/add-ons/frontend/cookiecutter.json index 84f648bb..fb3b6a87 100644 --- a/templates/add-ons/frontend/cookiecutter.json +++ b/templates/add-ons/frontend/cookiecutter.json @@ -17,8 +17,11 @@ "__version_frontend_package": "1.0.0-alpha.0", "__version_plone_volto": "{{ cookiecutter.volto_version }}", "__version_mrs_developer": "^2.2.0", - "__version_pnpm": "9.1.1", - "__version_release_it": "^17.1.1", + "__version_pnpm": "{{ '10.20.0' if cookiecutter.volto_version >= '19' else '9.1.1' }}", + "__version_release_it": "^19.0.5", + "__version_testing_library_react": "^16.2.0", + "__version_typescript": "^5.7.3", + "__version_vitest": "^3.1.2", "__node_version": "{{ cookiecutter.volto_version | node_version_for_volto }}", "__gha_version_node": "{{ cookiecutter.__node_version }}", "__gha_version_checkout": "v4", @@ -27,6 +30,7 @@ "__gha_version_background_action": "v1", "__gha_version_upload_artifact": "v4", "__gha_version_pages_deploy": "v4", + "__test_framework": "{{ 'vitest' if cookiecutter.volto_version >= '19' else 'jest'}}", "__prompts__": { "frontend_addon_name": "Add-on (Short name of the addon)", "title": "Add-on Title", diff --git a/templates/add-ons/frontend/hooks/post_gen_project.py b/templates/add-ons/frontend/hooks/post_gen_project.py index e8007327..f33b128d 100644 --- a/templates/add-ons/frontend/hooks/post_gen_project.py +++ b/templates/add-ons/frontend/hooks/post_gen_project.py @@ -32,6 +32,18 @@ def generate_docs_starter(context, output_dir): files.remove_files(output_dir / folder_name, DOCUMENTATION_STARTER_REMOVE) +def remove_conditional_files(context, output_dir): + if context["__test_framework"] == "jest": + ( + output_dir + / "packages" + / context["frontend_addon_name"] + / "vitest.config.mjs" + ).unlink() + else: + (output_dir / "jest-addon.config.js").unlink() + + def main(): """Final fixes.""" @@ -53,6 +65,8 @@ def main(): console.print(f" -> {title}") func(new_context, output_dir) + remove_conditional_files(context, output_dir) + msg = """ [bold blue]{{ cookiecutter.frontend_addon_name }}[/bold blue] diff --git a/templates/add-ons/frontend/hooks/pre_prompt.py b/templates/add-ons/frontend/hooks/pre_prompt.py index 01da2f4b..cbe14a4e 100644 --- a/templates/add-ons/frontend/hooks/pre_prompt.py +++ b/templates/add-ons/frontend/hooks/pre_prompt.py @@ -18,6 +18,7 @@ "20", "21", "22", + "24", ] diff --git a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/Makefile b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/Makefile index 4df4b131..e68872a1 100644 --- a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/Makefile +++ b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/Makefile @@ -98,8 +98,12 @@ test: ## Run unit tests .PHONY: ci-test ci-test: ## Run unit tests in CI # Unit Tests need the i18n to be built - VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto i18n - CI=1 RAZZLE_JEST_CONFIG=$(CURRENT_DIR)/jest-addon.config.js pnpm --filter @plone/volto test -- --passWithNoTests + VOLTOCONFIG=$(CURRENT_DIR)/volto.config.js pnpm --filter @plone/volto i18n +{%- if cookiecutter.__test_framework == 'jest' %} + CI=1 RAZZLE_JEST_CONFIG=$(CURRENT_DIR)/jest-addon.config.js pnpm run --filter @plone/volto test --passWithNoTests +{%- else %} + CI=1 pnpm run test --passWithNoTests +{%- endif %} .PHONY: backend-docker-start backend-docker-start: ## Starts a Docker-based backend for development diff --git a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/cypress/support/index.ts b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/cypress/support/index.ts new file mode 100644 index 00000000..1e6033a7 --- /dev/null +++ b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/cypress/support/index.ts @@ -0,0 +1,43 @@ +namespace Cypress { + export interface Chainable { + /** + * Custom command to select DOM element by data-cy attribute. + * @example cy.dataCy('greeting') + */ + navigate(value: string): Chainable>; + getSlateEditorAndType(value: string): Chainable>; + setSlateSelection(value: string): Chainable>; + clickSlateButton(value: string): Chainable>; + autologin(): Chainable>; + createContent({ + contentType, + contentId, + contentTitle, + path, + bodyModifier, + preview_image_link, + }: { + contentType: string; + contentId: string; + contentTitle: string; + path?: string; + bodyModifier?: (body: any) => any; + preview_image_link?: { '@id': string }; + }): Chainable>; + addNewBlock(value: string): Chainable>; + matchImage(): Chainable>; + injectAxe({ + axeCorePath, + }: { + axeCorePath: string; + }): Chainable>; + checkA11y( + context?: Object | string, + options?: Object, + violationCallback?: (violations: any[]) => void, + skipFailures?: boolean, + ): Chainable>; + checkAccessibility(): Chainable>; + getSlate(): Chainable>; + } +} diff --git a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/cypress/tsconfig.json b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/cypress/tsconfig.json new file mode 100644 index 00000000..74b7cb33 --- /dev/null +++ b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/cypress/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["es5", "dom"], + "types": ["cypress", "@testing-library/cypress", "node"] + }, + "include": ["**/*.ts"] +} diff --git a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/package.json b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/package.json index f4367a82..4346e399 100644 --- a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/package.json +++ b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/package.json @@ -18,7 +18,11 @@ "build": "VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build", "build:deps": "pnpm --filter @plone/registry --filter @plone/components build", "i18n": "pnpm --filter {{ cookiecutter.__npm_package_name }} i18n && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto i18n", +{%- if cookiecutter.__test_framework == 'jest' %} "test": "RAZZLE_JEST_CONFIG=$(pwd)/jest-addon.config.js pnpm --filter @plone/volto test -- --passWithNoTests", +{%- else %} + "test": "pnpm --filter {{ cookiecutter.__npm_package_name }} exec vitest", +{%- endif %} "lint": "VOLTOCONFIG=$(pwd)/volto.config.js eslint --max-warnings=0 'packages/**/src/**/*.{js,jsx,ts,tsx}'", "lint:fix": "VOLTOCONFIG=$(pwd)/volto.config.js eslint --fix 'packages/**/src/**/*.{js,jsx,ts,tsx}'", "prettier": "prettier --check 'packages/**/src/**/*.{js,jsx,ts,tsx}'", @@ -44,7 +48,21 @@ "overrides": { "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", "react-refresh": "^0.14.2" - } + }{%- if cookiecutter.volto_version >= '19' %}, + "ignoredBuiltDependencies": [ + "@parcel/watcher" + ], + "onlyBuiltDependencies": [ + "core-js", + "core-js-pure", + "cypress", + "es5-ext", + "esbuild", + "full-icu", + "lightningcss-cli", + "unrs-resolver" + ] + {%- endif %} }, "packageManager": "pnpm@{{ cookiecutter.__version_pnpm }}" } diff --git a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/package.json b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/package.json index 46b26955..1e3136c2 100644 --- a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/package.json +++ b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/package.json @@ -29,13 +29,22 @@ "addons": [], "dependencies": {}, "peerDependencies": { - "react": "18.2.0", - "react-dom": "18.2.0", - "@plone/registry": "workspace:*", - "@plone/types": "workspace:*" + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { + "@plone/registry": "workspace:*", "@plone/scripts": "workspace:*", + "@plone/types": "workspace:*", + "@types/react": "^18.3.1", + "@types/react-dom": "^18.3.1", + "typescript": "{{ cookiecutter.__version_typescript }}", +{%- if cookiecutter.__test_framework == 'jest' %} "release-it": "{{ cookiecutter.__version_release_it }}" +{%- else %} + "@testing-library/react": "{{ cookiecutter.__version_testing_library_react }}", + "release-it": "{{ cookiecutter.__version_release_it }}", + "vitest": "{{ cookiecutter.__version_vitest }}" +{%- endif %} } } diff --git a/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/vitest.config.mjs b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/vitest.config.mjs new file mode 100644 index 00000000..4c5bdb5b --- /dev/null +++ b/templates/add-ons/frontend/{{ cookiecutter.__folder_name }}/packages/{{ cookiecutter.frontend_addon_name }}/vitest.config.mjs @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; +import voltoVitestConfig from '@plone/volto/vitest.config.mjs'; +import path from 'path'; + +export default defineConfig({ + ...voltoVitestConfig, + resolve: { + alias: { + '@plone/volto': path.resolve(__dirname, '../../core/packages/volto/src'), // Add paths accordingly + // 'promise-file-reader': require.resolve('promise-file-reader') // Add to identify dependency from package + }, + }, +}); diff --git a/templates/add-ons/monorepo/.gitignore b/templates/add-ons/monorepo/.gitignore new file mode 100644 index 00000000..4a7290c1 --- /dev/null +++ b/templates/add-ons/monorepo/.gitignore @@ -0,0 +1 @@ +addon \ No newline at end of file diff --git a/templates/add-ons/monorepo/Makefile b/templates/add-ons/monorepo/Makefile new file mode 100644 index 00000000..eef34576 --- /dev/null +++ b/templates/add-ons/monorepo/Makefile @@ -0,0 +1,56 @@ +SHELL := /bin/bash +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +.PHONY: all +all: build + +BASE_FOLDER = $(shell git rev-parse --show-toplevel) +VENV_FOLDER = ${BASE_FOLDER}/.venv +BIN_FOLDER = ${VENV_FOLDER}/bin + +TEMPLATE = monorepo_addon +PROJECT_FOLDER_NAME = addon + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: ## Clean + rm -rf $(PROJECT_FOLDER_NAME) + +$(VENV_FOLDER): ## cookieplone installation + $(MAKE) -C $(BASE_FOLDER) sync + +.PHONY: format +format: $(VENV_FOLDER)## Format code + @echo "$(GREEN)==> Formatting $(TEMPLATE) codebase $(RESET)" + @uv run ruff format --config $(BASE_FOLDER)/pyproject.toml hooks + @uv run ruff check --select I --fix --config $(BASE_FOLDER)/pyproject.toml hooks + +.PHONY: generate +generate: $(VENV_FOLDER) ## Create a sample package + @echo "$(GREEN)==> Creating new test package$(RESET)" + rm -rf $(PROJECT_FOLDER_NAME) + COOKIEPLONE_REPOSITORY=$(BASE_FOLDER) uv run cookieplone $(TEMPLATE) --no-input + + +.PHONY: test +test: $(VENV_FOLDER)## Create a sample package and tests it + @echo "$(GREEN)==> Creating new test package$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/projects/monorepo + +.PHONY: test-pdb +test-pdb: $(VENV_FOLDER)## Stop on the first failed test + @echo "$(GREEN)==> Test template, stop on first error$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/projects/monorepo -x --pdb diff --git a/templates/add-ons/monorepo/README.md b/templates/add-ons/monorepo/README.md new file mode 100644 index 00000000..505306a0 --- /dev/null +++ b/templates/add-ons/monorepo/README.md @@ -0,0 +1,154 @@ +# Cookieplone Add-on Monorepo 🌟 + +[![Cookieplone Templates: CI](https://github.com/plone/cookieplone-templates/actions/workflows/main.yml/badge.svg)](https://github.com/plone/cookieplone-templates/blob/main/.github/workflows/main.yml) +[![Built with Cookieplone](https://img.shields.io/badge/built%20with-Cookiecutter-ff69b4.svg?logo=cookiecutter)](https://github.com/plone/cookieplone-templates/) +[![License](https://img.shields.io/github/license/plone/cookieplone-templates)](../../../LICENSE) +[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +Welcome to **Cookieplone Add-on Monorepo**! +Your one-stop solution to kickstart [Plone](https://plone.org/) 6 projects with ease and efficiency. +Powered by [Cookieplone](https://github.com/plone/cookieplone) and [Cookiecutter](https://github.com/cookiecutter/cookiecutter), these templates are designed to save you time and ensure that you get started on the right foot. 🚀 + + +## Prerequisites + +- [uv](https://docs.astral.sh/uv/) is the recommended tool for managing Python versions and project dependencies. +- Node.js and pnpm: essential for managing and running JavaScript packages. + + +### uv + +To install uv, use the following command, or visit the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/) for alternative methods. + +```shell +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + + +### Node.js and pnpm + +Follow the [Plone documentation](https://6.docs.plone.org/install/install-from-packages.html#pre-requisites-for-installation) for detailed instructions. + + +## Generate your Plone 6 project 🎉 + +```shell +uvx cookieplone project +``` + + +### Use options to avoid prompts + +Cookieplone will ask a lot of questions, as described under [Project generation options](#project-generation-options). +You can use some of its options to avoid repeatedly entering the same values. + + +#### `--no-input` + +You can use the [`--no-input`](https://cookiecutter.readthedocs.io/en/latest/cli_options.html#cmdoption-cookiecutter-no-input) option to make cookieplone not prompt for parameters and only use `cookiecutter.json` file content. + + +#### `--replay` and `--replay-file` + +You can use the option [`--replay-file`](https://cookiecutter.readthedocs.io/en/latest/cli_options.html#cmdoption-cookiecutter-replay-file) to not prompt for parameters and only use information entered previously or from a specified file. +See [Replay Project Generation](https://cookiecutter.readthedocs.io/en/latest/advanced/replay.html) in the cookiecutter documentation for more information. + + +## Develop your project + +This section provides commands that you will frequently use during development. + + +### Initial build + +```shell +make install +``` + + +### Start servers + +Start the backend server with the following command. + +```shell +make backend-start +``` + +Start the frontend server with the following command. + +```shell +make frontend-start +``` + + +### Rebuild after changes + +After you make changes to your code, you will need to install the changes and restart the servers. + +```shell +make install +make backend-start +make frontend-start +``` + + +## Project generation options 🛠️ + +The table below describes the options you can customize using the [Cookiecutter CLI](https://github.com/cookiecutter/cookiecutter) during the generation process. + +| Option | Description | Example | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | +| `title` | Your project's human-readable name, capitals and spaces allowed. | `Plone Site` | +| `description` | Describes your project and gets used in places like ``README.md`` and other files. | `New site for our company.` | +| `project_slug` | Your project's slug without spaces. Used to name your repository and Docker images. | `plone-site` | +| `hostname` | Hostname where the project will be deployed. | `site.plone.org` | +| `author` | This is you! Its value goes into places like ``LICENSE``, ``pyproject.toml`` and other files. | `Our Company` | +| `email` | The email address to contact the project maintainer. | `email@example.com` | +| `use_prerelease_versions` | Use pre-release versions of Plone and Volto, including alpha and beta releases. The default value could also be set via `USE_PRERELEASE` environment variable. | `Yes` | +| `plone_version` | Plone version to be used. This queries for the latest available Plone 6 version and presents it to you as the default value. | `6.0.0` | +| `volto_version` | Volto (Plone Frontend) version to be used. This queries for the latest available Volto version and presents it to you as the default value. | `16.4.1` | +| `python_package_name` | Name of the Python package used to configure your project. It needs to be Python-importable, so no dashes, spaces or special characters are allowed. | `plone_site` | +| `frontend_addon_name` | Name of the Volto addon package used to configure your frontend project. No spaces or special characters are allowed. | `volto-plone-site` | +| `language_code` | Language to be used on the site. | `pt-br` | +| `organization` | Used for GitHub, GitLab, and Docker repositories. GitHub or GitLab username or organization slug from URL. | `collective` | +| `container_registry` | Container registry to be used. | `github` | +| `devops_storage` | Storage backend to be used in the deployment stack. | `relstorage` | +| `devops_ansible` | Should we create an Ansible playbook to bootstrap and deploy this project? | `Yes` | + + + +## Dive into your project's structure 🏗️ + +Your generated project will have a well-organized structure, ensuring that both development and maintenance are a breeze. +It includes separate sections for backend, frontend, and devops, each tailored for its specific role. + +(Include the Structure and Reasoning section from the previous README.md here, as it provides a good overview of the project structure) + + +## Code quality assurance 🧐 + +Your project comes equipped with linters to ensure code quality. +Run the following command to automatically format your code. + +```shell +make format +``` + + +## Internationalization 🌐 + +Generate translation files with the following command. + +```shell +make i18n +``` + + +## License 📜 + +This project is licensed under the [MIT License](/LICENSE). + + +## Let's get building! 🚀 + +Happy coding! diff --git a/templates/add-ons/monorepo/cookiecutter.json b/templates/add-ons/monorepo/cookiecutter.json new file mode 100644 index 00000000..331c96f3 --- /dev/null +++ b/templates/add-ons/monorepo/cookiecutter.json @@ -0,0 +1,95 @@ +{ + "title": "Addon", + "description": "A new add-on for Plone.", + "project_slug": "{{ cookiecutter.title | slugify }}", + "organization": "collective", + "repository_url": "https://github.com/{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "author": "Plone Foundation", + "email": "collective@plone.org", + "use_prerelease_versions": "{{ 'No' | use_prerelease_versions }}", + "plone_version": "{{ cookiecutter.use_prerelease_versions | latest_plone }}", + "volto_version": "{{ cookiecutter.use_prerelease_versions | latest_volto }}", + "python_package_name": "{{ cookiecutter.project_slug|replace(' ', '')|replace('-', '.') }}", + "frontend_addon_name": "volto-{{ cookiecutter.python_package_name|replace('_', '-')|replace('.', '-') }}", + "container_registry": ["github", "docker_hub", "gitlab"], + "initialize_documentation": ["1", "0"], + "__project_slug": "{{ cookiecutter.project_slug }}", + "__repository_url": "{{ cookiecutter.repository_url }}", + "__repository_git": "git@github.com:{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}", + "__version_package": "1.0.0a0", + "__version_frontend_package": "1.0.0-alpha.0", + "__feature_headless": "1", + "__feature_distribution": "0", + "__backend_managed_by_uv": "false", + "__npm_package_name": "{{ cookiecutter.frontend_addon_name }}", + "__devops_compose_name": "{{ cookiecutter.project_slug | replace('_', '-') }}", + "__devops_traefik_version": "v2.11", + "__devops_traefik_local_include_ui": "yes", + "__devops_postgres_version": "18.1", + "__devops_db_password": "{{ random_ascii_string(12) }}", + "__devops_zeo_version": "6.0.0", + "__folder_name": "{{ cookiecutter.project_slug }}", + "__python_package_name_upper": "{{ cookiecutter.python_package_name | pascal_case }}", + "__python_version": "3.12", + "__node_version": "{{ cookiecutter.volto_version | node_version_for_volto }}", + "__version_plone_volto": "{{ cookiecutter.volto_version }}", + "__version_pnpm": "{{ '10.20.0' if cookiecutter.volto_version >= '19' else '9.1.1' }}", + "__container_registry_prefix": "{{ cookiecutter.container_registry | image_prefix }}", + "__container_image_prefix": "{{ cookiecutter.__container_registry_prefix }}{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "__backend_addon_git_initialize": "0", + "__backend_addon_format": "1", + "__project_git_initialize": "1", + "__prompts__": { + "title": "Project Title", + "project_slug": "Project Slug (Used for repository id)", + "hostname": "Project URL (without protocol)", + "description": "Project Description", + "author": "Author", + "email": "Author E-mail", + "python_package_name": "Python Package Name", + "frontend_addon_name": "Volto Addon Name", + "organization": "GitHub or GitLab username or organization slug from URL", + "repository_url": "URL to the repository", + "container_registry": { + "__prompt__": "Container Registry", + "github": "GitHub Container Registry", + "docker_hub": "Docker Hub", + "gitlab": "GitLab" + }, + "initialize_documentation": { + "__prompt__": "Would you like to add a documentation scaffold to your project?", + "1": "Yes", + "0": "No" + } + }, + + "_copy_without_render": [ + "news/.changelog_template.jinja" + ], + "_extensions": [ + "cookieplone.filters.use_prerelease_versions", + "cookieplone.filters.node_version_for_volto", + "cookieplone.filters.image_prefix", + "cookieplone.filters.pascal_case", + "cookieplone.filters.latest_volto", + "cookieplone.filters.latest_plone" + ], + "__cookieplone_subtemplates": [ + ["add-ons/backend", "Setup Backend", "1"], + ["add-ons/frontend", "Setup Frontend", "1"], + ["ci/github", "Setup CI", "1"], + [ + "docs/starter", + "Setup Documentation", + "{{cookiecutter.initialize_documentation}}" + ], + ["sub/vscode", "Setup VSCode", "1"], + ["sub/addon_settings", "Setup Add-on", "1"] + ], + "__cookieplone_repository_path": "", + "__cookieplone_template": "project", + "__generator_sha": "", + "__generator_template_url": "https://github.com/plone/cookieplone-templates/tree/main/{{ cookiecutter.__cookieplone_template }}", + "__generator_date_long": "{% now 'utc', '%Y-%m-%d %H:%M:%S' %}", + "__generator_signature": "This was generated by [the cookieplone-templates {{ cookiecutter.__cookieplone_template }} template]({{ cookiecutter.__generator_template_url }}) on {{ cookiecutter.__generator_date_long }}" +} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/keys/.gitkeep b/templates/add-ons/monorepo/hooks/__init__.py similarity index 100% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/keys/.gitkeep rename to templates/add-ons/monorepo/hooks/__init__.py diff --git a/templates/add-ons/monorepo/hooks/post_gen_project.py b/templates/add-ons/monorepo/hooks/post_gen_project.py new file mode 100644 index 00000000..2bafc893 --- /dev/null +++ b/templates/add-ons/monorepo/hooks/post_gen_project.py @@ -0,0 +1,294 @@ +"""Post generation hook.""" + +import json +import subprocess +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path + +from cookieplone import generator +from cookieplone.utils import console, files, git, npm, plone + +context: OrderedDict = {{cookiecutter}} + +initial_version = "1.0.0a0" +configuration_version = "1000" + +BACKEND_ADDON_REMOVE = [ + ".github", + ".git", +] + +DOCUMENTATION_STARTER_REMOVE = [ + ".github", + ".git", +] + +FRONTEND_ADDON_REMOVE = [ + ".github", + ".git", +] + + +POST_GEN_TO_REMOVE = { + "docs-0": [ + ".github/workflows/docs.yml", + ".github/workflows/rtd-pr-preview.yml", + ], + "docs-1": ["docs/LICENSE.md"], +} + +TEMPLATES_FOLDER = "templates" + + +def _fix_frontend_addon_name(context: OrderedDict) -> OrderedDict: + """Fix frontend_addon_name if it is a scoped package.""" + frontend_addon_name: str = context["frontend_addon_name"] + if frontend_addon_name.startswith("@") and "/" in frontend_addon_name: + npm_package_name = frontend_addon_name + frontend_addon_name = npm.unscoped_package_name(npm_package_name) + context["npm_package_name"] = npm_package_name + context["frontend_addon_name"] = frontend_addon_name + return context + + +def handle_version(context: OrderedDict, output_dir: Path): + """Update version.txt.""" + version_path = output_dir / "version.txt" + version_path.write_text(initial_version) + + +def handle_docs_cleanup(context: OrderedDict, output_dir: Path): + """Clean up GitHub Actions deploy.""" + answer = context.get("initialize_documentation") + key = f"docs-{answer}" + files.remove_files(output_dir, POST_GEN_TO_REMOVE[key]) + + +def handle_docs_setup(context: OrderedDict, output_dir: Path): + """Move files from /docs to the root.""" + files_to_move = [ + ["docs/.readthedocs.yaml", ".readthedocs.yml"], + ] + for src_path, dst_path in files_to_move: + src = output_dir / src_path + src.rename(output_dir / dst_path) + + +def handle_git_initialization(context: OrderedDict, output_dir: Path): + """Initialize a GIT repository for the project codebase.""" + git.initialize_repository(output_dir) + + +def generate_addons_backend(context, output_dir): + """Run Plone Addon generator.""" + output_dir = output_dir + folder_name = "backend" + context["initial_version"] = f"{initial_version}" + context["configuration_version"] = f"{configuration_version}" + # Headless + context["feature_headless"] = "1" + context["initialize_documentation"] = "0" + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/add-ons/backend", + output_dir, + folder_name, + context, + BACKEND_ADDON_REMOVE, + ) + files.remove_files(output_dir / folder_name, BACKEND_ADDON_REMOVE) + + +def generate_addons_frontend(context, output_dir): + """Run volto generator.""" + folder_name = "frontend" + # Handle packages inside an organization + context = _fix_frontend_addon_name(context) + frontend_addon_name = context["frontend_addon_name"] + context["initial_version"] = f"{initial_version}.0" + context["initialize_documentation"] = "0" + path = generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/add-ons/frontend", + output_dir, + folder_name, + context, + FRONTEND_ADDON_REMOVE, + ) + # Handle .release-it.json + release_it_path = path / "packages" / frontend_addon_name / ".release-it.json" + if release_it_path.is_file(): + data = json.loads(release_it_path.read_text()) + # Disable GitHub releases + data["github"]["release"] = False + # Disable plonePrePublish + data["plonePrePublish"]["publish"] = False + # Disable npm + data["npm"]["publish"] = False + # Update file + release_it_path.write_text(json.dumps(data, indent=2)) + + +def generate_docs_starter(context, output_dir): + """Generate documentation scaffold""" + output_dir = output_dir + folder_name = "docs" + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/docs/starter", + output_dir, + folder_name, + context, + DOCUMENTATION_STARTER_REMOVE, + ) + files.remove_files(output_dir / folder_name, DOCUMENTATION_STARTER_REMOVE) + + +def generate_sub_addon_settings(context: OrderedDict, output_dir: Path): + """Configure add-on settings.""" + # Use the same base folder + folder_name = output_dir.name + output_dir = output_dir.parent + context = _fix_frontend_addon_name(context) + context["cookieplone_template"] = f"{context['__cookieplone_template']}" + context["generator_sha"] = f"{context['__generator_sha']}" + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/sub/addon_settings", output_dir, folder_name, context + ) + + +def generate_sub_vscode(context: OrderedDict, output_dir: Path): + """Configure VSCode settings.""" + # Use the same base folder + output_dir = output_dir + folder_name = ".vscode" + context["folder_name"] = folder_name + context["cookieplone_template"] = f"{context['__cookieplone_template']}" + context["generator_sha"] = f"{context['__generator_sha']}" + context["has_frontend"] = "1" + context["has_backend"] = "1" + context["has_github"] = "1" + context["is_monorepo"] = "1" + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/sub/vscode", output_dir, folder_name, context + ) + + +def generate_ci_github(context, output_dir): + """Run CI - GitHub.""" + output_dir = output_dir + folder_name = ".github" + + context["folder_name"] = ".github" + context["has_backend"] = "1" + context["has_frontend"] = "1" + context["has_docs"] = "{{ cookiecutter.initialize_documentation }}" + context["has_varnish"] = "0" + context["has_gha_deploy"] = "0" + context["cookieplone_template"] = "{{ cookiecutter.__cookieplone_template }}" + + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/ci/github", + output_dir, + folder_name, + context, + [], + ) + + +def run_actions(actions: list, output_dir: Path): + for func, title, enabled in actions: + if not int(enabled): + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + +def main(): + """Final fixes.""" + output_dir = Path().cwd() + + initialize_git = bool( + int(context.get("__project_git_initialize")) + ) # {{ cookiecutter.__project_git_initialize }} + backend_format = bool( + int(context.get("__backend_addon_format")) + ) # {{ cookiecutter.__backend_addon_format }} + + subtemplates = context.get( + "__cookieplone_subtemplates", [] + ) # {{ cookiecutter.__cookieplone_subtemplates }} + funcs = {k: v for k, v in globals().items() if k.startswith("generate_")} + for template_id, title, enabled in subtemplates: + # Convert sub/cache -> generate_sub_cache + template_slug = template_id.replace("/", "_").replace("-", "") + func_name = f"generate_{template_slug}" + func = funcs.get(func_name) + if not func: + raise ValueError(f"No handler available for sub_template {template_id}") + elif not int(enabled): + console.print(f" -> Ignoring ({title})") + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + # Create namespace packages + plone.create_namespace_packages( + output_dir / "backend/src/packagename", + context.get("python_package_name"), + style="pkgutil", + ) + + # Run format + if backend_format: + backend_folder = output_dir / "backend" + # Run make format in the backend folder + cmd = f"make -C {backend_folder} format" + subprocess.call(cmd, shell=True) # noQA: S602 + + # Cleanup / Git + actions = [ + [ + handle_version, + "Update version.txt", + True, + ], + [ + handle_docs_setup, + "Organize documentation files", + int( + context.get("initialize_documentation") + ), # {{ cookiecutter.initialize_documentation }} + ], + [handle_docs_cleanup, "Remove unneeded documentation files", "1"], + [ + handle_git_initialization, + "Initialize Git repository", + initialize_git, + ], + ] + run_actions(actions, output_dir) + + # Do a second run add newly created files + if initialize_git: + repo = git.repo_from_path(output_dir) + repo.git.add(output_dir) + + msg = """ + [bold blue]{{ cookiecutter.title }}[/bold blue] + + Now, code it, create a git repository, push to your organization. + + Sorry for the convenience, + The Plone Community. + """ + console.panel( + title="New project was generated", + subtitle="", + msg=msg, + url="https://plone.org/", + ) + + +if __name__ == "__main__": + main() diff --git a/templates/add-ons/monorepo/hooks/pre_gen_project.py b/templates/add-ons/monorepo/hooks/pre_gen_project.py new file mode 100644 index 00000000..ae0473c5 --- /dev/null +++ b/templates/add-ons/monorepo/hooks/pre_gen_project.py @@ -0,0 +1,67 @@ +"""Pre generation hook.""" + +import sys +from collections import OrderedDict +from pathlib import Path +from textwrap import dedent + +from cookieplone import data +from cookieplone.utils import console, validators + +output_path = Path().resolve() + +context: OrderedDict = {{cookiecutter}} + + +def check_errors(context: dict) -> data.ContextValidatorResult: + """Check for errors in the provided data.""" + validations = [ + data.ItemValidator("plone_version", validators.validate_plone_version), + data.ItemValidator("volto_version", validators.validate_volto_version), + data.ItemValidator( + "python_package_name", validators.validate_python_package_name + ), + data.ItemValidator("frontend_addon_name", validators.validate_npm_package_name), + ] + result = validators.run_context_validations(context, validations) + return result + + +def main(): + """Validate context.""" + validation_result = check_errors(context) + success = validation_result.status + if not success: + msg = dedent( + """ + [bold red]Error[/bold red] + It will not be possible to generate the add-on codebase. + + Please review the errors: + """ + ) + for validation in validation_result.validations: + if validation.status: + continue + label = "red" + msg = ( + f"{msg}\n - {validation.key}: [{label}]{validation.message}[/{label}]" + ) + else: + msg = dedent( + f""" + Summary: + + - Plone version: [bold blue]{{ cookiecutter.plone_version }}[/bold blue] + - Volto version: [bold blue]{{ cookiecutter.volto_version }}[/bold blue] + - Output folder: [bold blue]{output_path}[/bold blue] + + """ + ) + console.panel(title="{{ cookiecutter.title }} generation", msg=msg) + if not success: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/templates/add-ons/monorepo/hooks/pre_prompt.py b/templates/add-ons/monorepo/hooks/pre_prompt.py new file mode 100644 index 00000000..b1cd3b91 --- /dev/null +++ b/templates/add-ons/monorepo/hooks/pre_prompt.py @@ -0,0 +1,76 @@ +"""Pre Prompt hook.""" + +import sys + +try: + from cookieplone import __version__ as cookieplone_version + from cookieplone import data + from cookieplone.utils import commands, console, sanity +except ModuleNotFoundError: + print("This template should be run with cookieplone") + sys.exit(1) +from packaging.version import Version + +SUPPORTED_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13"] +MIN_COOKIEPLONE = "0.9.6" +COOKIEPLONE_INSTALLATION = ( + "https://github.com/plone/cookieplone/blob/main/README.md#installation-" +) + + +def sanity_check() -> data.SanityCheckResults: + """Run sanity checks on the system.""" + checks = [ + data.SanityCheck( + "Cookieplone", + lambda: ( + "" + if Version(cookieplone_version) >= Version(MIN_COOKIEPLONE) + else ( + f"This template requires Cookieplone {MIN_COOKIEPLONE} " + "or higher. Upgrade information available " + f"at {COOKIEPLONE_INSTALLATION}." + ) + ), + [], + "error", + ), + data.SanityCheck( + "uv", + commands.check_command_is_available, + ["uv"], + "error", + ), + data.SanityCheck( + "Node", + commands.check_node_version, + [], + "error", + ), + data.SanityCheck("git", commands.check_command_is_available, ["git"], "error"), + data.SanityCheck( + "Docker (optional)", commands.check_docker_version, ["26"], "warning" + ), + ] + return sanity.run_sanity_checks(checks) + + +def main(): + """Validate context.""" + + msg = """ +Creating a new Plone Project + +Sanity check results: +""" + check_results = sanity_check() + for check in check_results.checks: + label = "green" if check.status else "red" + msg = f"{msg}\n - {check.name}: [{label}]{check.message}[/{label}]" + console.panel(title="Plone Project", msg=f"{msg}\n") + if not check_results.status: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/.editorconfig b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/.editorconfig new file mode 100644 index 00000000..f8ae1e55 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/.editorconfig @@ -0,0 +1,35 @@ + +# EditorConfig Configurtaion file, for more details see: +# https://EditorConfig.org +# EditorConfig is a convention description, that could be interpreted +# by multiple editors to enforce common coding conventions for specific +# file types + +# top-most EditorConfig file: +# Will ignore other EditorConfig files in Home directory or upper tree level. +root = true + + +[*] # For All Files +# Unix-style newlines with a newline ending every file +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +# Set default charset +charset = utf-8 +# Indent style default +indent_style = space + +[*.{py,cfg,ini}] +# 4 space indentation +indent_size = 4 + +[*.{html,dtml,pt,zpt,xml,zcml,js,json,ts,less,scss,css,sass,yml,yaml}] +# 2 space indentation +indent_size = 2 + +[{Makefile,.gitmodules}] +# Tab indentation (no size specified, but view as 4 spaces) +indent_style = tab +indent_size = unset +tab_width = unset diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/.gitignore b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/.gitignore new file mode 100644 index 00000000..90993858 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/.gitignore @@ -0,0 +1,5 @@ +!.vscode +node_modules +/core +/public +*.mo diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/CHANGELOG.md b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/CHANGELOG.md new file mode 100644 index 00000000..8b94ec07 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/CHANGELOG.md @@ -0,0 +1,9 @@ +# Change log + + + + diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/Makefile b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/Makefile new file mode 100644 index 00000000..368a3235 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/Makefile @@ -0,0 +1,243 @@ +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-xeu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +GIT_FOLDER=$(CURRENT_DIR)/.git + +REPOSITORY_SETTINGS := $(shell uvx repoplone settings dump) + +PROJECT_NAME := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.name') + +VOLTO_VERSION := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.frontend.volto_version') +PLONE_VERSION := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.backend.base_package_version') + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +.PHONY: all +all: install + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: debug-settings +debug-settings: ## Debug settings + @echo "Debug settings" + @echo "PROJECT_NAME: $(PROJECT_NAME)" + @echo "VOLTO_VERSION: $(VOLTO_VERSION)" + @echo "PLONE_VERSION: $(PLONE_VERSION)" + +########################################### +# Frontend +########################################### +.PHONY: frontend-install +frontend-install: ## Install React Frontend + $(MAKE) -C "./frontend/" install + +.PHONY: frontend-build +frontend-build: ## Build React Frontend + $(MAKE) -C "./frontend/" build + +.PHONY: frontend-start +frontend-start: ## Start React Frontend + $(MAKE) -C "./frontend/" start + +.PHONY: frontend-test +frontend-test: ## Test frontend codebase + @echo "Test frontend" + $(MAKE) -C "./frontend/" test + +########################################### +# Backend +########################################### +.PHONY: backend-install +backend-install: ## Create virtualenv and install Plone + $(MAKE) -C "./backend/" install + $(MAKE) backend-create-site + +.PHONY: backend-build +backend-build: ## Build Backend + $(MAKE) -C "./backend/" install + +.PHONY: backend-create-site +backend-create-site: ## Create a Plone site with default content + $(MAKE) -C "./backend/" create-site + +.PHONY: backend-update-example-content +backend-update-example-content: ## Export example content inside package + $(MAKE) -C "./backend/" update-example-content + +.PHONY: backend-start +backend-start: ## Start Plone Backend + $(MAKE) -C "./backend/" start + +.PHONY: backend-test +backend-test: ## Test backend codebase + @echo "Test backend" + $(MAKE) -C "./backend/" test + +########################################### +# Environment +########################################### +.PHONY: install +install: ## Install + @echo "Install Backend & Frontend" + $(MAKE) backend-install + $(MAKE) frontend-install + +.PHONY: clean +clean: ## Clean installation + @echo "Clean installation" + $(MAKE) -C "./backend/" clean + $(MAKE) -C "./frontend/" clean + +########################################### +# QA +########################################### +.PHONY: format +format: ## Format codebase + @echo "Format the codebase" + $(MAKE) -C "./backend/" format + $(MAKE) -C "./frontend/" format + +.PHONY: lint +lint: ## Format codebase + @echo "Lint the codebase" + $(MAKE) -C "./backend/" lint + $(MAKE) -C "./frontend/" lint + +.PHONY: check +check: format lint ## Lint and Format codebase + +########################################### +# i18n +########################################### +.PHONY: i18n +i18n: ## Update locales + @echo "Update locales" + $(MAKE) -C "./backend/" i18n + $(MAKE) -C "./frontend/" i18n + +########################################### +# Testing +########################################### +.PHONY: test +test: backend-test frontend-test ## Test codebase + +########################################### +# Container images +########################################### +.PHONY: build-images +build-images: ## Build container images + @echo "Build" + $(MAKE) -C "./backend/" build-image + $(MAKE) -C "./frontend/" build-image + +########################################### +# Local Stack +########################################### +.PHONY: stack-create-site +stack-create-site: ## Local Stack: Create a new site + @echo "Create a new site in the local Docker stack" + @echo "(Stack must not be running already.)" + VOLTO_VERSION=$(VOLTO_VERSION) PLONE_VERSION=$(PLONE_VERSION) docker compose -f docker-compose.yml run --build backend ./docker-entrypoint.sh create-site + +.PHONY: stack-start +stack-start: ## Local Stack: Start Services + @echo "Start local Docker stack" + VOLTO_VERSION=$(VOLTO_VERSION) PLONE_VERSION=$(PLONE_VERSION) docker compose -f docker-compose.yml up -d --build + @echo "Now visit: http://{{ cookiecutter.__project_slug }}.localhost" + +.PHONY: stack-status +stack-status: ## Local Stack: Check Status + @echo "Check the status of the local Docker stack" + @docker compose -f docker-compose.yml ps + +.PHONY: stack-stop +stack-stop: ## Local Stack: Stop Services + @echo "Stop local Docker stack" + @docker compose -f docker-compose.yml stop + +.PHONY: stack-rm +stack-rm: ## Local Stack: Remove Services and Volumes + @echo "Remove local Docker stack" + @docker compose -f docker-compose.yml down + @echo "Remove local volume data" + @docker volume rm $(PROJECT_NAME)_vol-site-data + +########################################### +# Acceptance +########################################### +.PHONY: acceptance-backend-dev-start +acceptance-backend-dev-start: + @echo "Start acceptance backend" + $(MAKE) -C "./backend/" acceptance-backend-start + +.PHONY: acceptance-frontend-dev-start +acceptance-frontend-dev-start: + @echo "Start acceptance frontend" + $(MAKE) -C "./frontend/" acceptance-frontend-dev-start + +.PHONY: acceptance-test +acceptance-test: + @echo "Start acceptance tests in interactive mode" + $(MAKE) -C "./frontend/" acceptance-test + +# Build Docker images +.PHONY: acceptance-frontend-image-build +acceptance-frontend-image-build: + @echo "Build acceptance frontend image" + @docker build frontend -t {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-frontend:acceptance -f frontend/Dockerfile --build-arg VOLTO_VERSION=$(VOLTO_VERSION) + +.PHONY: acceptance-backend-image-build +acceptance-backend-image-build: + @echo "Build acceptance backend image" + @docker build backend -t {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-backend:acceptance -f backend/Dockerfile.acceptance --build-arg PLONE_VERSION=$(PLONE_VERSION) + +.PHONY: acceptance-images-build +acceptance-images-build: ## Build Acceptance frontend/backend images + $(MAKE) acceptance-backend-image-build + $(MAKE) acceptance-frontend-image-build + +.PHONY: acceptance-frontend-container-start +acceptance-frontend-container-start: + @echo "Start acceptance frontend" + @docker run --rm -p 3000:3000 --name {{ cookiecutter.__project_slug }}-frontend-acceptance --link {{ cookiecutter.__project_slug }}-backend-acceptance:backend -e RAZZLE_API_PATH=http://localhost:55001/plone -e RAZZLE_INTERNAL_API_PATH=http://backend:55001/plone -d {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-frontend:acceptance + +.PHONY: acceptance-backend-container-start +acceptance-backend-container-start: + @echo "Start acceptance backend" + @docker run --rm -p 55001:55001 --name {{ cookiecutter.__project_slug }}-backend-acceptance -d {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-backend:acceptance + +.PHONY: acceptance-containers-start +acceptance-containers-start: ## Start Acceptance containers + $(MAKE) acceptance-backend-container-start + $(MAKE) acceptance-frontend-container-start + +.PHONY: acceptance-containers-stop +acceptance-containers-stop: ## Stop Acceptance containers + @echo "Stop acceptance containers" + @docker stop {{ cookiecutter.__project_slug }}-frontend-acceptance + @docker stop {{ cookiecutter.__project_slug }}-backend-acceptance + +.PHONY: ci-acceptance-test +ci-acceptance-test: + @echo "Run acceptance tests in CI mode" + $(MAKE) acceptance-containers-start + pnpm dlx wait-on --httpTimeout 20000 http-get://localhost:55001/plone http://localhost:3000 + $(MAKE) -C "./frontend/" ci-acceptance-test + $(MAKE) acceptance-containers-stop diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/README.md b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/README.md new file mode 100644 index 00000000..2163d509 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/README.md @@ -0,0 +1,148 @@ +# {{ cookiecutter.title }} 🚀 + +[![Built with Cookieplone](https://img.shields.io/badge/built%20with-Cookieplone-0083be.svg?logo=cookiecutter)](https://github.com/plone/cookieplone-templates/) +[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![CI]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml/badge.svg)]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml) + +{{ cookiecutter.description }} + +## Quick Start 🏁 + +### Prerequisites ✅ + +- An [operating system](https://6.docs.plone.org/install/create-project-cookieplone.html#prerequisites-for-installation) that runs all the requirements mentioned. +- [uv](https://6.docs.plone.org/install/create-project-cookieplone.html#uv) +- [nvm](https://6.docs.plone.org/install/create-project-cookieplone.html#nvm) +- [Node.js and pnpm](https://6.docs.plone.org/install/create-project.html#node-js) {{ cookiecutter.__node_version }} +- [Make](https://6.docs.plone.org/install/create-project-cookieplone.html#make) +- [Git](https://6.docs.plone.org/install/create-project-cookieplone.html#git) +- [Docker](https://docs.docker.com/get-started/get-docker/) (optional) + + +### Installation 🔧 + +1. Clone this repository, then change your working directory. + + ```shell + git clone {{ cookiecutter.__repository_git }}.git + cd {{ cookiecutter.__project_slug }} + ``` + +2. Install this code base. + + ```shell + make install + ``` + + +### Fire Up the Servers 🔥 + +1. Create a new Plone site on your first run. + + ```shell + make backend-create-site + ``` + +2. Start the backend at http://localhost:8080/. + + ```shell + make backend-start + ``` + +3. In a new shell session, start the frontend at http://localhost:3000/. + + ```shell + make frontend-start + ``` + +Voila! Your Plone site should be live and kicking! 🎉 + +### Local Stack Deployment 📦 + +Deploy a local Docker Compose environment that includes the following. + +- Docker images for Backend and Frontend 🖼️ +- A stack with a Traefik router and a PostgreSQL database 🗃️ +- Accessible at [http://{{ cookiecutter.__project_slug }}.localhost](http://{{ cookiecutter.__project_slug }}.localhost) 🌐 + +Run the following commands in a shell session. + +```shell +make stack-create-site +make stack-start +``` + +And... you're all set! Your Plone site is up and running locally! 🚀 + +## Project structure 🏗️ + +This monorepo consists of the following distinct sections: + +- **backend**: Houses the API and Plone installation, utilizing pip instead of buildout, and includes a policy package named {{ cookiecutter.python_package_name }}. +- **frontend**: Contains the React (Volto) package. +- **devops**: Encompasses Docker stack, Ansible playbooks, and cache settings. +- **docs**: Scaffold for writing documentation for your project. + +### Why this structure? 🤔 + +- All necessary codebases to run the site are contained within the repository (excluding existing add-ons for Plone and React). +- Specific GitHub Workflows are triggered based on changes in each codebase (refer to .github/workflows). +- Simplifies the creation of Docker images for each codebase. +- Demonstrates Plone installation/setup without buildout. + +## Code quality assurance 🧐 + +To check your code against quality standards, run the following shell command. + +```shell +make check +``` + +### Format the codebase + +To format and rewrite the code base, ensuring it adheres to quality standards, run the following shell command. + +```shell +make format +``` + +| Section | Tool | Description | Configuration | +| --- | --- | --- | --- | +| backend | Ruff | Python code formatting, imports sorting | [`backend/pyproject.toml`](./backend/pyproject.toml) | +| backend | `zpretty` | XML and ZCML formatting | -- | +| frontend | ESLint | Fixes most common frontend issues | [`frontend/.eslintrc.js`](.frontend/.eslintrc.js) | +| frontend | prettier | Format JS and Typescript code | [`frontend/.prettierrc`](.frontend/.prettierrc) | +| frontend | Stylelint | Format Styles (css, less, sass) | [`frontend/.stylelintrc`](.frontend/.stylelintrc) | + +Formatters can also be run within the `backend` or `frontend` folders. + +### Linting the codebase +or `lint`: + + ```shell +make lint +``` + +| Section | Tool | Description | Configuration | +| --- | --- | --- | --- | +| backend | Ruff | Checks code formatting, imports sorting | [`backend/pyproject.toml`](./backend/pyproject.toml) | +| backend | Pyroma | Checks Python package metadata | -- | +| backend | check-python-versions | Checks Python version information | -- | +| backend | `zpretty` | Checks XML and ZCML formatting | -- | +| frontend | ESLint | Checks JS / Typescript lint | [`frontend/.eslintrc.js`](.frontend/.eslintrc.js) | +| frontend | prettier | Check JS / Typescript formatting | [`frontend/.prettierrc`](.frontend/.prettierrc) | +| frontend | Stylelint | Check Styles (css, less, sass) formatting | [`frontend/.stylelintrc`](.frontend/.stylelintrc) | + +Linters can be run individually within the `backend` or `frontend` folders. + +## Internationalization 🌐 + +Generate translation files for Plone and Volto with ease: + +```shell +make i18n +``` + +## Credits and acknowledgements 🙏 + +{{ cookiecutter.__generator_signature }}. A special thanks to all contributors and supporters! diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/backend/version.txt b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/backend/version.txt new file mode 100644 index 00000000..36bef1fb --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/backend/version.txt @@ -0,0 +1 @@ +{{ cookiecutter.plone_version }} diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/dependabot.yml b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/dependabot.yml new file mode 100644 index 00000000..df4d15b3 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml new file mode 100644 index 00000000..29241078 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml @@ -0,0 +1,114 @@ +--- +name: {{ cookiecutter.__devops_compose_name }} + +services: + traefik: + image: traefik:{{ cookiecutter.__devops_traefik_version }} + + ports: + - 80:80 + + labels: + - traefik.enable=true + - traefik.constraint-label=public +{%- if cookiecutter.__devops_traefik_local_include_ui == 'yes' %} + - traefik.http.routers.traefik-public-http.rule=Host(`traefik.${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) + - traefik.http.routers.traefik-public-http.entrypoints=http + - traefik.http.routers.traefik-public-http.service=api@internal + - traefik.http.services.traefik-public.loadbalancer.server.port=8000 +{%- endif %} + + # GENERIC MIDDLEWARES + - traefik.http.middlewares.gzip.compress=true + - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 + + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + command: + - --providers.docker + - --providers.docker.constraints=Label(`traefik.constraint-label`, `public`) + - --providers.docker.exposedbydefault=false + - --entrypoints.http.address=:80 + - --accesslog + - --log + - --api + + frontend: + build: + context: ./frontend + args: + - VOLTO_VERSION=${VOLTO_VERSION} + environment: + RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone + depends_on: + - backend + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Service + - traefik.http.services.svc-frontend.loadbalancer.server.port=3000 + # Routers + ## / + - traefik.http.routers.rt-frontend.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) + - traefik.http.routers.rt-frontend.entrypoints=http + - traefik.http.routers.rt-frontend.service=svc-frontend + - traefik.http.routers.rt-frontend.middlewares=gzip + + + backend: + build: + context: ./backend + args: + - PLONE_VERSION=${PLONE_VERSION} + environment: + RELSTORAGE_DSN: "dbname='${DB_NAME:-plone}' user='${DB_NAME:-plone}' host='${DB_HOST:-db}' password='${DB_PASSWORD:-{{ cookiecutter.__devops_db_password }}}' port='${DB_PORT:-5432}'" + depends_on: + - db + volumes: + - vol-site-data:/data + labels: + - traefik.enable=true + - traefik.constraint-label=public + # Services + - traefik.http.services.svc-backend.loadbalancer.server.port=8080 + + # Middlewares + ## VHM rewrite /++api++/ + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/http/${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}/Plone/++api++/VirtualHostRoot$$1" + + ## VHM rewrite /ClassicUI/ + - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" + - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/http/${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + + ## Basic Authentication + ### Note: all dollar signs in the hash need to be doubled for escaping. + ### To create user:password pair, it's possible to use this command: + ### echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g + ### Defaults to admin:admin + - traefik.http.middlewares.mw-backend-auth.basicauth.headerField=X-Auth-ClassicUI + - traefik.http.middlewares.mw-backend-auth.basicauth.users=admin:$$apr1$$uZPT5Fgu$$AmlIdamxT5ipBvPlsdfD70 + # Routers + - traefik.http.routers.rt-backend-api.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && (PathPrefix(`/++api++`)) + - traefik.http.routers.rt-backend-api.entrypoints=http + - traefik.http.routers.rt-backend-api.service=svc-backend + - traefik.http.routers.rt-backend-api.middlewares=gzip,mw-backend-vhm-api + + ## /ClassicUI + - traefik.http.routers.rt-backend-classic.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-backend-classic.entrypoints=http + - traefik.http.routers.rt-backend-classic.service=svc-backend + - traefik.http.routers.rt-backend-classic.middlewares=gzip,mw-backend-auth,mw-backend-vhm-classic + + db: + image: postgres:{{ cookiecutter.__devops_postgres_version }} + environment: + POSTGRES_USER: plone + POSTGRES_PASSWORD: {{ cookiecutter.__devops_db_password }} + POSTGRES_DB: plone + volumes: + - vol-site-data:/var/lib/postgresql/data + +volumes: + vol-site-data: {} diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/news/.changelog_template.jinja b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/news/.changelog_template.jinja new file mode 100644 index 00000000..b35bff39 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/news/.changelog_template.jinja @@ -0,0 +1,15 @@ +{% if sections[""] %} +{% for category, val in definitions.items() if category in sections[""] %} + +### {{ definitions[category]['name'] }} + +{% for text, values in sections[""][category].items() %} +- {{ text }} {{ values|join(', ') }} +{% endfor %} + +{% endfor %} +{% else %} +No significant changes. + + +{% endif %} \ No newline at end of file diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/towncrier.toml b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/towncrier.toml new file mode 100644 index 00000000..b28af3e2 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "./news/.changelog_template.jinja" +start_string = "\n" +issue_format = "[#{issue}]({{ cookiecutter.__repository_url }}/pull/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true \ No newline at end of file diff --git a/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/version.txt b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/version.txt new file mode 100644 index 00000000..a83f5d76 --- /dev/null +++ b/templates/add-ons/monorepo/{{ cookiecutter.__folder_name }}/version.txt @@ -0,0 +1 @@ +{{ cookiecutter.__version_package }} diff --git a/templates/ci/github/Makefile b/templates/ci/github/Makefile new file mode 100644 index 00000000..e6835b8c --- /dev/null +++ b/templates/ci/github/Makefile @@ -0,0 +1,57 @@ +SHELL := /bin/bash +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +.PHONY: all +all: build + +BASE_FOLDER = $(shell git rev-parse --show-toplevel) +VENV_FOLDER = ${BASE_FOLDER}/.venv +BIN_FOLDER = ${VENV_FOLDER}/bin + +TEMPLATE = ci_$(shell basename $(CURRENT_DIR)) +PROJECT_FOLDER_NAME = .github + + + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: ## Clean + rm -rf $(PROJECT_FOLDER_NAME) + +$(VENV_FOLDER): ## cookieplone installation + $(MAKE) -C $(BASE_FOLDER) sync + +.PHONY: format +format: $(VENV_FOLDER)## Format code + @echo "$(GREEN)==> Formatting $(TEMPLATE) codebase $(RESET)" + @uv run ruff format --config $(BASE_FOLDER)/pyproject.toml hooks + @uv run ruff check --select I --fix --config $(BASE_FOLDER)/pyproject.toml hooks + +.PHONY: generate +generate: $(VENV_FOLDER) ## Create a sample package + @echo "$(GREEN)==> Creating new test package$(RESET)" + rm -rf $(PROJECT_FOLDER_NAME) + COOKIEPLONE_REPOSITORY=$(BASE_FOLDER) uv run cookieplone $(TEMPLATE) --no-input + +.PHONY: test +test: $(VENV_FOLDER)## Create a sample package and tests it + @echo "$(GREEN)==> Test template$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/sub/$(TEMPLATE) + +.PHONY: test-pdb +test-pdb: $(VENV_FOLDER)## Stop on the first failed test + @echo "$(GREEN)==> Test template, stop on first error$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/sub/$(TEMPLATE) -x --pdb diff --git a/templates/ci/github/README.md b/templates/ci/github/README.md new file mode 100644 index 00000000..b24decde --- /dev/null +++ b/templates/ci/github/README.md @@ -0,0 +1,68 @@ +# Cookieplone GitHub settings + +[![Cookieplone Templates: CI](https://github.com/plone/cookieplone-templates/actions/workflows/main.yml/badge.svg)](https://github.com/plone/cookieplone-templates/blob/main/.github/workflows/main.yml) +[![Built with Cookieplone](https://img.shields.io/badge/built%20with-Cookiecutter-ff69b4.svg?logo=cookiecutter)](https://github.com/plone/cookieplone-templates/) +[![License](https://img.shields.io/github/license/plone/cookieplone-templates)](../../../LICENSE) +[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +This is a subtemplate used by other templates, in automated tests, and for OCI image generation—accessible with **Cookieplone** at the path `sub/github`. + + +## Prerequisites + +- [uv](https://docs.astral.sh/uv/) is the recommended tool for managing Python versions and project dependencies. +- Node.js and pnpm: essential for managing and running JavaScript packages. + + +### uv + +To install uv, use the following command, or visit the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/) for alternative methods. + +```shell +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + + +### Node.js + +Follow the [Plone documentation](https://6.docs.plone.org/install/install-from-packages.html#pre-requisites-for-installation) for detailed instructions. + + + +## Generate your project settings 🎉 + +```shell +uvx cookieplone sub/github +``` + + +### Project generation options 🛠️ + +The table below describes the options you can customize using the [Cookiecutter CLI](https://github.com/cookiecutter/cookiecutter) during the generation process. + +| Option | Description | Example | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | +| `title` | Your project's human-readable name, capitals and spaces allowed. | `Project Settings` | +| `author` | This is you! Its value goes into places like `LICENSE`, `package.json` and other files. | `Our Company` | +| `email` | The email address to contact the project maintainer. | `email@example.com` | +| `volto_version` | Volto version to be used. | `18.10.0` | + + +## Code quality assurance 🧐 + +Your project comes equipped with linters to ensure code quality. +Run the following command to automatically format your code. + +```shell +make format +``` + + +## License 📜 + +This project is licensed under the [MIT License](/LICENSE). + + +## Let's get building! 🚀 + +Happy coding! diff --git a/templates/ci/github/cookiecutter.json b/templates/ci/github/cookiecutter.json new file mode 100644 index 00000000..78123c97 --- /dev/null +++ b/templates/ci/github/cookiecutter.json @@ -0,0 +1,30 @@ +{ + "title": "Project Title", + "folder_name": ".github", + "organization": "collective", + "project_slug": "project-title", + "repository_url": "https://github.com/{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "hostname": "project.example.com", + "has_backend": ["1", "0"], + "has_frontend": ["1", "0"], + "has_docs": ["1", "0"], + "has_varnish": ["1", "0"], + "has_gha_deploy": ["1", "0"], + "cookieplone_template": "ci/github", + "__folder_name": "{{ cookiecutter.folder_name }}", + "__node_version": "22", + "__python_version": "3.12", + "__gha_version_checkout": "v5", + "__gha_version_plone_meta": "2.x", + "__gha_version_docker_stack": "v1.2.0", + "__gha_version_paths_filter": "v3.0.2", + "_copy_without_render": [], + "_extensions": [ + ], + "__cookieplone_repository_path": "", + "__cookieplone_template": "{{ cookiecutter.cookieplone_template }}", + "__generator_sha": "", + "__generator_template_url": "https://github.com/plone/cookieplone-templates/tree/main/{{ cookiecutter.__cookieplone_template }}", + "__generator_date_long": "{% now 'utc', '%Y-%m-%d %H:%M:%S' %}", + "__generator_signature": "This was generated by [the cookieplone-templates {{ cookiecutter.__cookieplone_template }} template]({{ cookiecutter.__generator_template_url }}) on {{ cookiecutter.__generator_date_long }}" +} diff --git a/templates/ci/github/hooks/post_gen_project.py b/templates/ci/github/hooks/post_gen_project.py new file mode 100644 index 00000000..5fac50a5 --- /dev/null +++ b/templates/ci/github/hooks/post_gen_project.py @@ -0,0 +1,37 @@ +"""Post generation hook.""" + +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path + +from cookieplone.utils import console, files + +context: OrderedDict = {{cookiecutter}} + + +def handle_remove_files(context: OrderedDict, output_dir: Path): + files_to_remove = [] + output_dir = output_dir + files.remove_files(output_dir, files_to_remove) + + +def main(): + """Final fixes.""" + output_dir = Path().cwd() + actions = [ + [ + handle_remove_files, + "Remove unnecessary files", + True, + ], + ] + for func, title, enabled in actions: + if not int(enabled): + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + +if __name__ == "__main__": + main() diff --git a/templates/ci/github/hooks/pre_gen_project.py b/templates/ci/github/hooks/pre_gen_project.py new file mode 100644 index 00000000..6a2e382b --- /dev/null +++ b/templates/ci/github/hooks/pre_gen_project.py @@ -0,0 +1,17 @@ +"""Pre generation hook.""" + +from collections import OrderedDict +from pathlib import Path + +output_path = Path().resolve() + +context: OrderedDict = {{cookiecutter}} + + +def main(): + """Validate context.""" + pass + + +if __name__ == "__main__": + main() diff --git a/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/backend.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/backend.yml new file mode 100644 index 00000000..6a193763 --- /dev/null +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/backend.yml @@ -0,0 +1,92 @@ +name: Backend CI + +on: + workflow_call: + inputs: + base-tag: + required: true + type: string + image-name-prefix: + required: true + type: string + image-name-suffix: + required: true + type: string + python-version: + required: true + type: string + plone-version: + required: true + type: string + working-directory: + required: false + type: string + default: backend + +jobs: + + lint: + name: "Backend: Lint" + uses: plone/meta/.github/workflows/backend-lint.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + python-version: {{ "${{ inputs.python-version }}" }} + plone-version: {{ "${{ inputs.plone-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + + test: + name: "Backend: Test" + uses: plone/meta/.github/workflows/backend-pytest.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + python-version: {{ "${{ inputs.python-version }}" }} + plone-version: {{ "${{ inputs.plone-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + + coverage: + name: "Backend: Coverage" + uses: plone/meta/.github/workflows/backend-pytest-coverage.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + python-version: {{ "${{ inputs.python-version }}" }} + plone-version: {{ "${{ inputs.plone-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + + release: + name: "Backend: Build and publish Container Image" + uses: plone/meta/.github/workflows/container-image-build-push.yml@{{ cookiecutter.__gha_version_plone_meta }} + needs: + - lint + - test + - coverage + permissions: + contents: read + packages: write + with: + base-tag: {{ "${{ inputs.base-tag }}" }} + image-name-prefix: {{ "${{ inputs.image-name-prefix }}" }} + image-name-suffix: {{ "${{ inputs.image-name-suffix }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + build-args: | + PLONE_VERSION={{ "${{ inputs.plone-version }}" }} + push: {{ "${{ github.event_name != 'pull_request' }}" }} + secrets: + username: {{ "${{ github.actor }}" }} + password: {{ "${{ secrets.GITHUB_TOKEN }}" }} + + report: + name: "Final report" + if: {{ "${{ always() }}" }} + runs-on: ubuntu-latest + needs: + - lint + - test + - coverage + - release + steps: + - name: Write report + run: | + echo '# Backend Workflow Report' >> $GITHUB_STEP_SUMMARY + echo '| Job ID | Conclusion |' >> $GITHUB_STEP_SUMMARY + echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY + echo '| lint | {{ "${{ needs.lint.result }}" }} |' >> $GITHUB_STEP_SUMMARY + echo '| test | {{ "${{ needs.test.result }}" }} |' >> $GITHUB_STEP_SUMMARY + echo '| coverage | {{ "${{ needs.coverage.result }}" }} |' >> $GITHUB_STEP_SUMMARY + echo '| release | {{ "${{ needs.release.result }}" }} |' >> $GITHUB_STEP_SUMMARY diff --git a/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/config.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/config.yml new file mode 100644 index 00000000..67ca65ee --- /dev/null +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/config.yml @@ -0,0 +1,152 @@ +name: "Compute Config variables" + +on: + workflow_call: + inputs: + node-version: + required: false + type: string + default: "{{ cookiecutter.__node_version }}" + python-version: + required: false + type: string + default: "{{ cookiecutter.__python_version }}" + stack-name: + required: false + type: string + default: "{{ cookiecutter.hostname | replace('.', '-') }}" + environment: + required: false + type: string + default: "{{ cookiecutter.hostname }}" + outputs: + acceptance: + description: "Flag reporting if we should run the acceptance jobs" + value: {{ "${{ jobs.config.outputs.acceptance }}" }} + backend: + description: "Flag reporting if we should run the backend jobs" + value: {{ "${{ jobs.config.outputs.backend }}" }} + devops: + description: "Flag reporting if we should run the devops jobs" + value: {{ "${{ jobs.config.outputs.devops }}" }} + docs: + description: "Flag reporting if we should run the docs jobs" + value: {{ "${{ jobs.config.outputs.docs }}" }} + frontend: + description: "Flag reporting if we should run the frontend jobs" + value: {{ "${{ jobs.config.outputs.frontend }}" }} + varnish: + description: "Flag reporting if we should run the varnish jobs" + value: {{ "${{ jobs.config.outputs.varnish }}" }} + base-tag: + description: "Base tag to be used when creating container images" + value: {{ "${{ jobs.config.outputs.base-tag }}" }} + event-name: + description: "Event name triggering the workflow" + value: {{ "${{ github.event_name }}" }} + image-name-prefix: + description: "Image name prefix for container images" + value: {{ "${{ jobs.config.outputs.image-name-prefix }}" }} + stack-name: + description: "Stack name for the deployment" + value: {{ "${{ inputs.stack-name }}" }} + environment: + description: "Environment to be used for the deployment" + value: {{ "${{ inputs.environment }}" }} + node-version: + description: "Node version to be used" + value: {{ "${{ inputs.node-version }}" }} + python-version: + description: "Python version to be used" + value: {{ "${{ inputs.python-version }}" }} + plone-version: + description: "Plone version to be used" + value: {{ "${{ jobs.config.outputs.plone-version }}" }} + volto-version: + description: "Volto version to be used" + value: {{ "${{ jobs.config.outputs.volto-version }}" }} + +jobs: + config: + runs-on: ubuntu-latest + outputs: + acceptance: {{ "${{ steps.filter.outputs.acceptance }}" }} + backend: {{ "${{ steps.filter.outputs.backend }}" }} + devops: {{ "${{ steps.filter.outputs.devops }}" }} + docs: {{ "${{ steps.filter.outputs.docs }}" }} + frontend: {{ "${{ steps.filter.outputs.frontend }}" }} + varnish: {{ "${{ steps.filter.outputs.varnish }}" }} + base-tag: {{ "${{ steps.vars.outputs.base-tag }}" }} + plone-version: {{ "${{ steps.vars.outputs.plone-version }}" }} + volto-version: {{ "${{ steps.vars.outputs.volto-version }}" }} + image-name-prefix: {{ "${{ steps.vars.outputs.image-name-prefix }}" }} + + steps: + + - name: Checkout + uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} + + - name: Setup uv + uses: plone/meta/.github/actions/setup_uv@2.x + with: + python-version: {{ "${{ inputs.python-version }}" }} + working-directory: '.' + + - name: Compute several vars needed for the CI + id: vars + shell: bash + run: | + REPOSITORY_SETTINGS="$(uvx repoplone settings dump)" + echo "image-name-prefix=$(jq -r '.container_images_prefix' <<< "$REPOSITORY_SETTINGS")" >> "$GITHUB_OUTPUT" + echo "plone-version=$(jq -r '.backend.base_package_version' <<< "$REPOSITORY_SETTINGS")" >> "$GITHUB_OUTPUT" + echo "volto-version=$(jq -r '.frontend.volto_version' <<< "$REPOSITORY_SETTINGS")" >> "$GITHUB_OUTPUT" + echo "base-tag=sha-$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + - name: Filter paths + uses: dorny/paths-filter@{{ cookiecutter.__gha_version_paths_filter }} + id: filter + with: + filters: | + acceptance: + - '.github/workflows/backend*' + - '.github/workflows/frontend*' + - '.github/workflows/config*' + - 'backend/**' + - 'frontend/**' + backend: + - 'backend/**' + - '.github/workflows/backend*' + - '.github/workflows/config*' + devops: + - 'devops/**' + - '.github/workflows/config*' + docs: + - '.readthedocs.yaml' + - 'docs/**' + - '.github/workflows/docs.yaml' + - '.github/workflows/config*' + frontend: + - 'frontend/**' + - '.github/workflows/frontend*' + - '.github/workflows/config*' + varnish: + - 'devops/varnish/**' + - '.github/workflows/varnish*' + - '.github/workflows/config*' + + - name: Test vars + run: | + echo '# Config Settings' >> $GITHUB_STEP_SUMMARY + echo '| Variable | Value |' >> $GITHUB_STEP_SUMMARY + echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY + echo "| base-tag | {{ "${{ steps.vars.outputs.base-tag }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| plone-version | {{ "${{ steps.vars.outputs.plone-version }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| volto-version | {{ "${{ steps.vars.outputs.volto-version }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| image-name-prefix | {{ "${{ steps.vars.outputs.image-name-prefix }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| event-name | {{ "${{ github.event_name }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| acceptance | {{ "${{ steps.filter.outputs.acceptance }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| backend | {{ "${{ steps.filter.outputs.backend }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| devops | {{ "${{ steps.filter.outputs.devops }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| docs | {{ "${{ steps.filter.outputs.docs }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| frontend | {{ "${{ steps.filter.outputs.frontend }}" }} |" >> $GITHUB_STEP_SUMMARY + echo "| varnish | {{ "${{ steps.filter.outputs.varnish }}" }} |" >> $GITHUB_STEP_SUMMARY diff --git a/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/docs.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/docs.yml new file mode 100644 index 00000000..e76bedff --- /dev/null +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/docs.yml @@ -0,0 +1,24 @@ +name: "Build documentation, check links, spelling, grammar, and style" + +on: + workflow_call: + inputs: + check-vale: + required: false + type: boolean + default: false + python-version: + required: true + type: string + working-directory: + required: false + type: string + default: docs +jobs: + build: + name: "Docs: Build" + uses: plone/meta/.github/workflows/docs-build.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + python-version: {{ "${{ inputs.python-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + check-vale: {{ "${{ inputs.check-vale }}" }} diff --git a/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/frontend.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/frontend.yml new file mode 100644 index 00000000..020bcab5 --- /dev/null +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/frontend.yml @@ -0,0 +1,88 @@ +name: Frontend CI + +on: + workflow_call: + inputs: + base-tag: + required: true + type: string + image-name-prefix: + required: true + type: string + image-name-suffix: + required: true + type: string + node-version: + required: true + type: string + volto-version: + required: true + type: string + working-directory: + required: false + type: string + default: frontend + +jobs: + code-analysis: + name: "Frontend: Codeanalysis" + uses: plone/meta/.github/workflows/frontend-code.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + node-version: {{ "${{ inputs.node-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + + i18n: + name: "Frontend: i18n" + uses: plone/meta/.github/workflows/frontend-i18n.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + node-version: {{ "${{ inputs.node-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + + unit: + name: "Frontend: Unit tests" + uses: plone/meta/.github/workflows/frontend-unit.yml@{{ cookiecutter.__gha_version_plone_meta }} + with: + node-version: {{ "${{ inputs.node-version }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + + release: + name: "Frontend: Build and publish container image" + uses: plone/meta/.github/workflows/container-image-build-push.yml@{{ cookiecutter.__gha_version_plone_meta }} + needs: + - code-analysis + - i18n + - unit + permissions: + contents: read + packages: write + with: + base-tag: {{ "${{ inputs.base-tag }}" }} + image-name-prefix: {{ "${{ inputs.image-name-prefix }}" }} + image-name-suffix: {{ "${{ inputs.image-name-suffix }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + build-args: | + VOLTO_VERSION={{ "${{ inputs.volto-version }}" }} + push: {{ "${{ github.event_name != 'pull_request' }}" }} + secrets: + username: {{ "${{ github.actor }}" }} + password: {{ "${{ secrets.GITHUB_TOKEN }}" }} + + report: + name: "Final report" + if: {{ "${{ always() }}" }} + runs-on: ubuntu-latest + needs: + - code-analysis + - i18n + - unit + - release + steps: + - name: Write report + run: | + echo '# Frontend Workflow Report' >> $GITHUB_STEP_SUMMARY + echo '| Job ID | Conclusion |' >> $GITHUB_STEP_SUMMARY + echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY + echo '| code-analysis | {{ "${{ needs.code-analysis.result }}" }} |' >> $GITHUB_STEP_SUMMARY + echo '| i18n | {{ "${{ needs.i18n.result }}" }} |' >> $GITHUB_STEP_SUMMARY + echo '| unit | {{ "${{ needs.unit.result }}" }} |' >> $GITHUB_STEP_SUMMARY + echo '| release | {{ "${{ needs.release.result }}" }} |' >> $GITHUB_STEP_SUMMARY diff --git a/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/main.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/main.yml new file mode 100644 index 00000000..924d95a5 --- /dev/null +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/main.yml @@ -0,0 +1,119 @@ +name: {{ cookiecutter.title }} CI + +on: + push: + paths: + - "backend/**" + - "frontend/**" + - ".github/workflows/*.yml" + {%- if cookiecutter.has_docs == '1' %} + - "docs/**" + - .readthedocs.yaml + {%- endif %} + - "devops/**" + workflow_dispatch: + +jobs: + config: + uses: ./.github/workflows/config.yml + +{%- if cookiecutter.has_backend == '1' %} + backend: + uses: ./.github/workflows/backend.yml + needs: + - config + with: + base-tag: {{ "${{ needs.config.outputs.base-tag }}" }} + image-name-prefix: {{ "${{ needs.config.outputs.image-name-prefix }}" }} + image-name-suffix: backend + python-version: {{ "${{ needs.config.outputs.python-version }}" }} + plone-version: {{ "${{ needs.config.outputs.plone-version }}" }} + if: {{ "${{ needs.config.outputs.backend == 'true' }}" }} + permissions: + contents: read + packages: write +{%- endif %} + +{%- if cookiecutter.has_docs == '1' %} + + docs: + uses: ./.github/workflows/docs.yml + needs: + - config + with: + python-version: {{ "${{ needs.config.outputs.python-version }}" }} + working-directory: docs + if: {{ "${{ needs.config.outputs.docs == 'true' }}" }} + +{%- endif %} + +{%- if cookiecutter.has_frontend == '1' %} + frontend: + uses: ./.github/workflows/frontend.yml + needs: + - config + with: + base-tag: {{ "${{ needs.config.outputs.base-tag }}" }} + image-name-prefix: {{ "${{ needs.config.outputs.image-name-prefix }}" }} + image-name-suffix: frontend + node-version: {{ "${{ needs.config.outputs.node-version }}" }} + volto-version: {{ "${{ needs.config.outputs.volto-version }}" }} + if: {{ "${{ needs.config.outputs.frontend == 'true' }}" }} + permissions: + contents: read + packages: write +{%- endif %} + +{%- if cookiecutter.has_varnish == '1' %} + + varnish: + uses: ./.github/workflows/varnish.yml + needs: + - config + with: + base-tag: {{ "${{ needs.config.outputs.base-tag }}" }} + image-name-prefix: {{ "${{ needs.config.outputs.image-name-prefix }}" }} + image-name-suffix: varnish + if: {{ "${{ needs.config.outputs.varnish == 'true' }}" }} + permissions: + contents: read + packages: write +{%- endif %} + + report: + name: "Final report" + if: {{ "${{ always() }}" }} + runs-on: ubuntu-latest + needs: + - config + {%- if cookiecutter.has_backend == '1' %} + - backend + {%- endif %} + {%- if cookiecutter.has_frontend == '1' %} + - frontend + {%- endif %} + {%- if cookiecutter.has_docs == '1' %} + - docs + {%- endif %} + {%- if cookiecutter.has_varnish == '1' %} + - varnish + {%- endif %} + steps: + - name: Write report + run: | + {{ "echo '# Workflow Report' >> $GITHUB_STEP_SUMMARY" }} + {{ "echo '| Job ID | Conclusion |' >> $GITHUB_STEP_SUMMARY" }} + {{ "echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY" }} + {{ "echo '| config | ${{ needs.config.result }} |' >> $GITHUB_STEP_SUMMARY" }} + {%- if cookiecutter.has_backend == '1' %} + {{ "echo '| backend | ${{ needs.backend.result }} |' >> $GITHUB_STEP_SUMMARY" }} + {%- endif %} + {%- if cookiecutter.has_docs == '1' %} + {{ "echo '| docs | ${{ needs.docs.result }} |' >> $GITHUB_STEP_SUMMARY" }} + {%- endif %} + {%- if cookiecutter.has_frontend == '1' %} + {{ "echo '| frontend | ${{ needs.frontend.result }} |' >> $GITHUB_STEP_SUMMARY" }} + {%- endif %} + {%- if cookiecutter.has_varnish == '1' %} + {{ "echo '| varnish | ${{ needs.varnish.result }} |' >> $GITHUB_STEP_SUMMARY" }} + {%- endif %} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/manual_deploy.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/manual_deploy.yml similarity index 55% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/manual_deploy.yml rename to templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/manual_deploy.yml index f6a87675..c51978ef 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/manual_deploy.yml +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/manual_deploy.yml @@ -3,31 +3,17 @@ name: Manual Deployment of {{ cookiecutter.hostname }} on: workflow_dispatch: - jobs: - meta: - runs-on: ubuntu-latest - outputs: - ENVIRONMENT: {{ "${{ steps.vars.outputs.ENVIRONMENT }}" }} - STACK_NAME: {{ "${{ steps.vars.outputs.STACK_NAME }}" }} - steps: - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Set Env Vars - id: vars - run: | - ENVIRONMENT={{ "${{ vars.LIVE_ENV }}" }} - echo "ENVIRONMENT=${ENVIRONMENT}" >> $GITHUB_OUTPUT - echo "STACK_NAME=${ENVIRONMENT//./-}" >> $GITHUB_OUTPUT + config: + uses: ./.github/workflows/config.yml deploy: if: {{ "${{ github.ref == 'refs/heads/main' }}" }} needs: - - meta + - config runs-on: ubuntu-latest - environment: {{ "${{ needs.meta.outputs.ENVIRONMENT }}" }} + environment: {{ "${{ needs.config.outputs.environment }}" }} steps: - name: Checkout uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} @@ -42,8 +28,8 @@ jobs: remote_port: {{ "${{ secrets.DEPLOY_PORT }}" }} remote_user: {{ "${{ secrets.DEPLOY_USER }}" }} remote_private_key: {{ "${{ secrets.DEPLOY_SSH }}" }} - stack_file: devops/stacks/{{ "${{ needs.meta.outputs.ENVIRONMENT }}" }}.yml - stack_name: {{ "${{ needs.meta.outputs.STACK_NAME }}" }} + stack_file: devops/stacks/{{ "${{ needs.config.outputs.environment }}" }}.yml + stack_name: {{ "${{ needs.config.outputs.stack-name }}" }} stack_param: {{ "${{ github.ref_name }}" }} env_file: {{ "${{ secrets.ENV_FILE }}" }} deploy_timeout: 480 diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/rtd-pr-preview.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/rtd-pr-preview.yml similarity index 100% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/rtd-pr-preview.yml rename to templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/rtd-pr-preview.yml diff --git a/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/varnish.yml b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/varnish.yml new file mode 100644 index 00000000..320f2cf9 --- /dev/null +++ b/templates/ci/github/{{ cookiecutter.__folder_name }}/workflows/varnish.yml @@ -0,0 +1,36 @@ +name: Varnish Image Creation + +on: + workflow_call: + inputs: + base-tag: + required: true + type: string + image-name-prefix: + required: true + type: string + image-name-suffix: + required: true + type: string + working-directory: + required: false + type: string + default: devops/varnish + +jobs: + + release: + name: "Varnish: Build and publish Container Image" + uses: plone/meta/.github/workflows/container-image-build-push.yml@2.x + permissions: + contents: read + packages: write + with: + base-tag: {{ "${{ inputs.base-tag }}" }} + image-name-prefix: {{ "${{ inputs.image-name-prefix }}" }} + image-name-suffix: {{ "${{ inputs.image-name-suffix }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + push: {{ "${{ github.event_name != 'pull_request' }}" }} + secrets: + username: {{ "${{ github.actor }}" }} + password: {{ "${{ secrets.GITHUB_TOKEN }}" }} diff --git a/templates/devops/ansible/.gitignore b/templates/devops/ansible/.gitignore new file mode 100644 index 00000000..cabb1f51 --- /dev/null +++ b/templates/devops/ansible/.gitignore @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/templates/devops/ansible/Makefile b/templates/devops/ansible/Makefile new file mode 100644 index 00000000..0512a518 --- /dev/null +++ b/templates/devops/ansible/Makefile @@ -0,0 +1,49 @@ +SHELL := /bin/bash +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +.PHONY: all +all: build + +BASE_FOLDER = $(shell git rev-parse --show-toplevel) +VENV_FOLDER = ${BASE_FOLDER}/.venv +BIN_FOLDER = ${VENV_FOLDER}/bin + +TEMPLATE = devops_ansible +DEVOPS_FOLDER_NAME = ansible + +.PHONY: clean +clean: ## Clean + rm -rf $(ADDON_NAME) + +$(VENV_FOLDER): ## cookieplone installation + $(MAKE) -C $(BASE_FOLDER) sync + +.PHONY: format +format: $(VENV_FOLDER)## Format code + @echo "$(GREEN)==> Formatting $(TEMPLATE) codebase $(RESET)" + @uv run ruff format --config $(BASE_FOLDER)/pyproject.toml hooks + @uv run ruff check --select I --fix --config $(BASE_FOLDER)/pyproject.toml hooks + +.PHONY: generate +generate: $(VENV_FOLDER) ## Create a sample package + @echo "$(GREEN)==> Creating new test package$(RESET)" + rm -rf $(DEVOPS_FOLDER_NAME) + COOKIEPLONE_REPOSITORY=$(BASE_FOLDER) uv run cookieplone $(TEMPLATE) --no-input + +.PHONY: test +test: $(VENV_FOLDER)## Create a sample package and tests it + @echo "$(GREEN)==> Creating new test package$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/devops/ansible + +.PHONY: test-pdb +test-pdb: $(VENV_FOLDER)## Stop on the first failed test + @echo "$(GREEN)==> Test template, stop on first error$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/devops/ansible -x --pdb diff --git a/templates/devops/ansible/README.md b/templates/devops/ansible/README.md new file mode 100644 index 00000000..36b672a6 --- /dev/null +++ b/templates/devops/ansible/README.md @@ -0,0 +1 @@ +# Cookieplone Ansible Playbooks diff --git a/templates/devops/ansible/cookiecutter.json b/templates/devops/ansible/cookiecutter.json new file mode 100644 index 00000000..905628eb --- /dev/null +++ b/templates/devops/ansible/cookiecutter.json @@ -0,0 +1,82 @@ +{ + "title": "Ansible", + "description": "Ansible setup to manage a Docker Swarm cluster for Plone hosting", + "folder_name": "{{ cookiecutter.title|replace(' ', '')|replace('-', '_')|replace('.', '')|lower }}", + "hostname": "{{ cookiecutter.folder_name }}.example.com", + "hostname_or_ip": "{{ cookiecutter.hostname }}", + "stack_name": "{{ cookiecutter.hostname|replace('.', '-') }}", + "stack_prefix": "{{ cookiecutter.hostname | extract_host | replace('.', '-') }}", + "stack_location": "etc/stacks/{{ cookiecutter.hostname }}.yml", + "author": "Plone Community", + "email": "collective@plone.org", + "repository_type": ["github", "gitlab_com", "gitlab"], + "organization": "collective", + "repository_url": "https://github.com/{{ cookiecutter.organization }}/{{ cookiecutter.folder_name }}", + "container_registry": ["github", "docker_hub", "gitlab"], + "initialize_git": ["1", "0"], + "__folder_name": "{{ cookiecutter.folder_name }}", + "__project_slug": "{{ cookiecutter.folder_name }}", + "__devops_host": "{{ cookiecutter.hostname | extract_host }}", + "__devops_stack_name": "{{ cookiecutter.stack_name }}", + "__devops_stack_prefix": "{{ cookiecutter.stack_prefix }}", + "__devops_swarm_public_network": "nw-public", + "__devops_swarm_stack_network": "nw-internal", + "__devops_traefik_version": "v2.11", + "__devops_traefik_stack_include_ui": "no", + "__devops_cronjob_version": "1.14", + "__devops_project_user": "500", + "__repository_url": "{{ cookiecutter.repository_url }}", + "__container_registry_prefix": "{{ cookiecutter.container_registry | image_prefix }}", + "__container_image_prefix": "{{ cookiecutter.__container_registry_prefix }}{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}", + "__year": "{% now 'local', '%Y' %}", + "__generator_date_long": "{% now 'utc', '%Y-%m-%d %H:%M:%S' %}", + "__generator_signature": "This was generated by the [cookieplone-templates documentation_starter template](https://github.com/plone/cookieplone-templates/tree/main/documentation_starter) on {{ cookiecutter.__generator_date_long }}", + "__documentation_starter_format": "1", + "__prompts__": { + "title": "Title", + "description": "A short description for this repository", + "folder_name": "Name of the folder to be created", + "hostname": "Public URL of the server", + "hostname_or_ip": "Address of the server (IP or hostname)", + "repository_type": { + "__prompt__": "Repository Type", + "github": "GitHub", + "gitlab_com": "GitLab.com", + "gitlab": "GitLab Self-Hosted" + }, + "organization": "GitHub / GitLab Username or Organization", + "repository_url": "Repository URL", + "container_registry": { + "__prompt__": "Container Registry", + "github": "GitHub Container Registry", + "docker_hub": "Docker Hub", + "gitlab": "GitLab" + }, + "stack_name": "Docker Swarm Stack Name", + "stack_prefix": "A 5 to 10 character prefix for the stack", + "author": "Author", + "email": "Author E-mail", + "initialize_git": { + "__prompt__": "Initialize Git Repository?", + "1": "Yes", + "0": "No" + } + }, + "_copy_without_render": [ + "etc/.ssh", + "etc/base", + "etc/docker", + "etc/keys", + "etc/ssh", + "playbooks", + "requirements", + "tasks" + ], + "_extensions": [ + "cookieplone.filters.pascal_case", + "cookieplone.filters.extract_host", + "cookieplone.filters.image_prefix" + ], + "__cookieplone_repository_path": "", + "__cookieplone_template": "" +} diff --git a/templates/devops/ansible/hooks/post_gen_project.py b/templates/devops/ansible/hooks/post_gen_project.py new file mode 100644 index 00000000..db377b55 --- /dev/null +++ b/templates/devops/ansible/hooks/post_gen_project.py @@ -0,0 +1,70 @@ +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path +from random import choice +from string import ascii_letters, digits + +from cookieplone.utils import console, git + +context: OrderedDict = {{cookiecutter}} + + +def generate_vaultpass(context: OrderedDict, output_dir: Path): + """Generate a vault password file for Ansible.""" + vault_pass_path = output_dir / ".vault_pass" + value = "".join(choice(ascii_letters + digits) for _ in range(32)) # noQA: S311 + if not vault_pass_path.exists(): + vault_pass_path.write_text(value) + console.print(f"Vault password file created at {vault_pass_path}") + else: + console.print(f"Vault password file already exists at {vault_pass_path}") + + +def handle_git_initialization(context: OrderedDict, output_dir: Path): + """Initialize a Git repository for the documentation codebase.""" + git.initialize_repository(output_dir) + + +def main(): + """Final fixes.""" + output_dir = Path().cwd() + initialize_git = bool(int(context.get("initialize_git"))) + # Cleanup / Git + actions = [ + [ + generate_vaultpass, + "Generate Ansible vault password", + True, + ], + [ + handle_git_initialization, + "Initialize Git repository", + initialize_git, + ], + ] + for func, title, enabled in actions: + if not int(enabled): + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + msg = """ + [bold blue]{{ cookiecutter.title }}[/bold blue] + + Now, enter the {{ cookiecutter.folder_name }} folder, start using your + playbooks. + + Sorry for the convenience, + The Plone Community. + """ + console.panel( + title="New Ansible setup created", + subtitle="", + msg=msg, + url="https://plone.org/", + ) + + +if __name__ == "__main__": + main() diff --git a/templates/devops/ansible/hooks/pre_gen_project.py b/templates/devops/ansible/hooks/pre_gen_project.py new file mode 100644 index 00000000..6a2e382b --- /dev/null +++ b/templates/devops/ansible/hooks/pre_gen_project.py @@ -0,0 +1,17 @@ +"""Pre generation hook.""" + +from collections import OrderedDict +from pathlib import Path + +output_path = Path().resolve() + +context: OrderedDict = {{cookiecutter}} + + +def main(): + """Validate context.""" + pass + + +if __name__ == "__main__": + main() diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.ansible-lint b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.ansible-lint new file mode 100644 index 00000000..17bbe856 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.ansible-lint @@ -0,0 +1,26 @@ +--- +exclude_paths: + - .cache/ # implicit unless exclude_paths is defined in config + - .github/ + - .ansible-lint + - etc/ + - keys/ + - roles/ + - "~/.ansible/roles/" + - "inventory/**/vault.yml" + +enable_list: + - fqcn-builtins + - no-log-password # opt-in + - no-same-owner # opt-in + +kinds: + - playbook: "./playbooks/**/*.{yml,yaml}" + - tasks: "./tasks/**/*.{yml,yaml}" + - yaml: "./inventory/.{yml,yaml}" + +skip_list: + - role-name + +# Offline mode disables installation of requirements.yml +offline: true diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.editorconfig b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.editorconfig new file mode 100644 index 00000000..eb3ec996 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.editorconfig @@ -0,0 +1,36 @@ +# EditorConfig Configurtaion file, for more details see: +# http://EditorConfig.org +# EditorConfig is a convention description, that could be interpreted +# by multiple editors to enforce common coding conventions for specific +# file types + +# top-most EditorConfig file: +# Will ignore other EditorConfig files in Home directory or upper tree level. +root = true + + +[*] # For All Files +# Unix-style newlines with a newline ending every file +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +# Set default charset +charset = utf-8 +# Indent style default +indent_style = space +# Max Line Length - a hard line wrap, should be disabled +max_line_length = off + +[*.{py,cfg,ini}] +# 4 space indentation +indent_size = 4 + +[*.{html,dtml,pt,zpt,xml,zcml,js,json,less,css,yml,yaml,sh}] +# 2 space indentation +indent_size = 2 + +[{Makefile,.gitmodules}] +# Tab indentation (no size specified, but view as 4 spaces) +indent_style = tab +indent_size = unset +tab_width = unset diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/.env_dist b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.env_dist similarity index 100% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/.env_dist rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/.env_dist diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.gitignore b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.gitignore new file mode 100644 index 00000000..cf900597 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +.env +.venv +.galaxy_install_info +.Python +.vagrant +.vault_pass.txt +*.retry +etc/keys/* +!etc/keys/.gitkeep +!roles/.gitkeep +roles/* +.install.stamp diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/Makefile b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/Makefile new file mode 100644 index 00000000..f5ce4b4c --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/Makefile @@ -0,0 +1,57 @@ +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-xeu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +ifeq ($(wildcard .env),) +$(info No .env file found, using default environment variables) +else +$(info Using environment variables from .env file) +include .env +export +endif + +INSTALL_STAMP := .install.stamp + +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +VENV_DIR=$(CURRENT_DIR)/.venv +BIN_DIR=$(VENV_DIR)/bin +ROLES_DIR=$(CURRENT_DIR)/roles + +.PHONY: all +all: help + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +install: $(INSTALL_STAMP) +$(INSTALL_STAMP): pyproject.toml + @echo "🚀 Setting up devops" + @echo "- Installing dependencies" + @uv sync + @echo "- Install Ansible Roles and Collections" + @uv run ansible-galaxy install -r requirements.yml --force --no-deps + @touch $(INSTALL_STAMP) + +.PHONY: update +update: $(INSTALL_STAMP) ## Update dependencies and roles + @echo "🚀 Update dependencies and roles" + @uv run ansible-galaxy install -r requirements.yml --force --no-deps + +.PHONY: clean +clean: ## Remove virtualenv and downloaded roles + @echo "🚀 Cleanup the current environment" + @rm -rf $(INSTALL_STAMP) .venv roles/* + +.PHONY: lint +lint: $(INSTALL_STAMP) + @uv run ansible-lint diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README.md b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/README.md similarity index 83% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README.md rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/README.md index 7c0ab289..93cfc34c 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README.md +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/README.md @@ -1,4 +1,4 @@ -# DevOps Operations for {{ cookiecutter.title }} 🚀 +# DevOps Ansible Operations for {{ cookiecutter.title }} 🚀 Welcome to the DevOps documentation for {{ cookiecutter.title }}! In this guide, we'll walk you through the setup and deployment process, ensuring a smooth and efficient development workflow. We leverage the power of [Ansible](https://www.ansible.com/), [Docker](https://www.docker.com/), and [Docker Swarm](https://docs.docker.com/engine/swarm/) to automate, containerize, and orchestrate application deployment. 🛠️🐳🌐 @@ -24,15 +24,15 @@ We deploy a robust website running [Plone](https://plone.org/) using a Docker st Now, let’s dive into the setup! 🏊‍♂️💫 ## Setup -Ensure you navigate to the `devops` folder before executing any commands listed in this document. From the root of your repository, execute: +Ensure you navigate to the `devops/ansible/` folder before executing any commands listed in this document. From the root of your repository, execute: ```shell -cd devops +cd devops/ansible/ ``` ### Environment Configuration -Start by creating an `.env` file in the `devops` folder. You can copy the existing `.env_dist` file as a starting point: +Start by creating an `.env` file in the `devops/ansible/` folder. You can copy the existing `.env_dist` file as a starting point: ```shell cp .env_dist .env @@ -63,29 +63,41 @@ You need either a Ubuntu or Debian based system for {{ cookiecutter.hostname }}, Execute the following to create a Python 3 virtual environment and install Ansible along with its dependencies: ```shell -make setup +make install ``` ### Inventory Configuration -Modify `devops/inventory/hosts.yml` with the appropriate connection details: +Modify `devops/ansible/inventory/hosts.yml` with the appropriate connection details: ```yaml --- -prod: +cluster: hosts: {{ cookiecutter.hostname }}: ansible_user: root + ansible_host: {{ cookiecutter.hostname }} host: {{ cookiecutter.__devops_host }} hostname: {{ cookiecutter.hostname }} + swarm_node: + labels: + type: manager + env: production + ``` ## Server Setup -With the correct information in `devops/inventory/hosts.yml`, initiate the remote server setup: +With the correct information in `devops/ansible/inventory/hosts.yml`, test the connection to the server with: + +```shell +uv run ansible-playbook playbooks/_connect.yml +``` + +And then, if the connection is successful, initiate the remote server setup by running: ```shell -make server-setup +uv run ansible-playbook playbooks/setup.yml ``` This command executes the Ansible playbook `devops/playbooks/setup.yml` on the remote server, performing various setup tasks including user creation, SSH setup, Docker installation, and more. diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/ansible.cfg b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/ansible.cfg similarity index 57% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/ansible.cfg rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/ansible.cfg index fbb968dc..aec21203 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/ansible.cfg +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/ansible.cfg @@ -1,9 +1,13 @@ [defaults] -inventory = inventory +inventory = ./inventory +forks = 50 +roles_path = ./roles retry_files_enabled = False allow_world_readable_tmpfiles = true -roles_path = roles -interpreter_python=auto +interpreter_python = python3 +vault_password_file = ./.vault_pass +pipelining = true +timeout = 40 [ssh_connection] pipelining=True diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/.ssh/.gitkeep b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/.ssh/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/.ssh/ansible-ssh-config b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/.ssh/ansible-ssh-config new file mode 100644 index 00000000..e69de29b diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/base/apt_proxy.j2 b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/base/apt_proxy.j2 new file mode 100644 index 00000000..6e913c5d --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/base/apt_proxy.j2 @@ -0,0 +1,2 @@ +Acquire::http::proxy "{{ proxy.http_proxy }}"; +Acquire::https::proxy "{{ proxy.https_proxy }}"; diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/base/environment.j2 b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/base/environment.j2 new file mode 100644 index 00000000..13d7df73 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/base/environment.j2 @@ -0,0 +1,3 @@ +http_proxy={{ proxy.http_proxy }} +https_proxy={{ proxy.https_proxy }} +no_proxy={{ proxy.no_proxy }} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/docker/systemd/http-proxy.conf.j2 b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/docker/systemd/http-proxy.conf.j2 similarity index 100% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/docker/systemd/http-proxy.conf.j2 rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/docker/systemd/http-proxy.conf.j2 diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/keys/.gitkeep b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/keys/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/ssh/default_ssh_config.j2 b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/ssh/default_ssh_config.j2 new file mode 100644 index 00000000..c52bc952 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/ssh/default_ssh_config.j2 @@ -0,0 +1,3 @@ +# Github +Host github.com +Port 22 diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/stacks/cronjob.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/stacks/cronjob.yml new file mode 100644 index 00000000..d20ea270 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/stacks/cronjob.yml @@ -0,0 +1,16 @@ +--- +version: '3.9' + +services: + swarm-cronjob: + image: "ghcr.io/crazy-max/swarm-cronjob:${CRONJOB_VERSION}" + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + environment: + - "TZ=${TZ}" + - "LOG_LEVEL=${LOG_LEVEL}" + - "LOG_JSON=${LOG_JSON}" + deploy: + placement: + constraints: + - node.role == manager diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/stacks/traefik.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/stacks/traefik.yml new file mode 100644 index 00000000..e01d8d7f --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/etc/stacks/traefik.yml @@ -0,0 +1,94 @@ +--- +version: '3.9' + +services: + + socket-proxy: + image: tecnativa/docker-socket-proxy + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + NODES: 1 + SERVICES: 1 + TASKS: 1 + NETWORKS: 1 + networks: + - nw-internal + deploy: + placement: + constraints: + - node.role == manager + + traefik: + image: "traefik:${TRAEFIK_VERSION}" + + ports: + - 80:80 + - 443:443 + + deploy: + replicas: 1 + update_config: + parallelism: 1 + delay: 5s + order: start-first + labels: + - "traefik.enable=true" + - "traefik.constraint-label=public" + - "traefik.http.services.traefik-public.loadbalancer.server.port=8000" + ## Basic Authentication + - "traefik.http.middlewares.admin-auth.basicauth.users=${TRAEFIK_BASIC_AUTH}" +{%- if cookiecutter.__devops_traefik_stack_include_ui == 'yes' %} + - "traefik.http.routers.traefik-public-https.rule=Host(`${TRAEFIK_HOSTNAME}`)" + - "traefik.http.routers.traefik-public-https.entrypoints=https" + - "traefik.http.routers.traefik-public-https.tls=true" + - "traefik.http.routers.traefik-public-https.service=api@internal" + - "traefik.http.routers.traefik-public-https.middlewares=admin-auth" +{%- endif %} + # GENERIC MIDDLEWARES + - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true" + - "traefik.http.middlewares.gzip.compress=true" + - "traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2" + + # GENERIC ROUTERS + # - "traefik.http.routers.generic-https-redirect.entrypoints=http" + # - "traefik.http.routers.generic-https-redirect.rule=HostRegexp(`{host:.*}`)" + # - "traefik.http.routers.generic-https-redirect.priority=1" + # - "traefik.http.routers.generic-https-redirect.middlewares=https-redirect" + + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - vol-traefik-certs:/certificates + + command: + - --providers.docker + - --providers.docker.watch + - --providers.docker.endpoint=tcp://socket-proxy:2375 + - --providers.docker.swarmmode + - --providers.docker.exposedbydefault=false + - --providers.docker.network={{ cookiecutter.__devops_swarm_public_network }} + - --providers.docker.constraints=Label(`traefik.constraint-label`, `public`) + - --entrypoints.http.address=:80 + - --entrypoints.https.address=:443 + - --certificatesresolvers.le.acme.email=${TRAEFIK_EMAIL} + - --certificatesresolvers.le.acme.storage=/certificates/acme.json + - --certificatesresolvers.le.acme.tlschallenge=true + - --accesslog + - --log + - --log.level=INFO + - --api + + networks: + - nw-public + - nw-internal + +volumes: + vol-traefik-certs: {} + +networks: + {{ cookiecutter.__devops_swarm_public_network }}: + external: true + nw-internal: + internal: true + driver: overlay diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/base.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/base.yml similarity index 76% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/base.yml rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/base.yml index 1dc2e841..3b60dba8 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/base.yml +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/base.yml @@ -1,3 +1,4 @@ --- +timezone: UTC devops: email: {{ cookiecutter.email }} diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/disks.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/disks.yml new file mode 100644 index 00000000..9409685f --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/disks.yml @@ -0,0 +1,2 @@ +# Additional mounts +additional_mounts: {} diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/docker.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/docker.yml new file mode 100644 index 00000000..d3f70469 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/docker.yml @@ -0,0 +1,20 @@ +--- +# geerlingguy.docker settings +docker_edition: 'ce' +docker_packages: + - "{{ "docker-{{ docker_edition }}" }}" + - "{{ "docker-{{ docker_edition }}-cli" }}" + - "{{ "docker-{{ docker_edition }}-rootless-extras" }}" +docker_packages_state: present + +docker_users: + - "{{ "{{ users.default.name }}" }}" + +# See all options in https://docs.docker.com/reference/cli/dockerd/#on-linux +docker_daemon_options: + mtu: 1450 + data-root: "/var/lib/docker" + log-driver: "json-file" + log-opts: + max-size: "5m" + max-file: "3" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/packages.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/packages.yml new file mode 100644 index 00000000..5c58330c --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/packages.yml @@ -0,0 +1,18 @@ +--- +base_packages: + install: + - acl + - apt-transport-https + - aptitude + - ca-certificates + - cron + - curl + - iputils-ping + - netcat-traditional + - python3-apt + - python3-minimal + - python3-pip + - python3-venv + - sudo + remove: + - nftables diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/proxy.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/proxy.yml new file mode 100644 index 00000000..c8f0c3d2 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/proxy.yml @@ -0,0 +1,5 @@ +--- +proxy: + http_proxy: + https_proxy: + no_proxy: diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/sshd.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/sshd.yml new file mode 100644 index 00000000..acd681b0 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/sshd.yml @@ -0,0 +1,5 @@ +--- +sshd: + port: 22 + allow_root: "yes" + allow_users: "{{ "{{ users.default.name }} {{ users.setup.name }}" }}" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/stacks.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/stacks.yml new file mode 100644 index 00000000..3f38c5ce --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/stacks.yml @@ -0,0 +1,59 @@ +--- +stacks: + + traefik: + enabled: true + stack_file: "etc/stacks/traefik.yml" + stack_name: "traefik" + main_folder: "/srv/traefik" + env_vars: + TRAEFIK_VERSION: "{{ cookiecutter.__devops_traefik_version}}" + TRAEFIK_HOSTNAME: "traefik.{{ cookiecutter.hostname }}" + TRAEFIK_EMAIL: "{{ "{{ devops.email }}" }}" + ### To create user:password pair, it's possible to use this command: + ### echo $(htpasswd -nb user password) + ### Defaults to admin:admin + TRAEFIK_BASIC_AUTH: "user:$apr1$M13lILVS$TqQMZMnUtV.1.qlm99Nam1" + folders: + - path: "/srv/traefik" + owner: "{{ "{{ users.default.name }}" }}" + group: root + mode: "0755" + + cronjob: + enabled: true + stack_file: "etc/stacks/cronjob.yml" + stack_name: "cronjob" + main_folder: "/srv/cronjob" + env_vars: + TZ: "UTC" + LOG_LEVEL: "info" + LOG_JSON: "false" + CRONJOB_VERSION: "{{ cookiecutter.__devops_cronjob_version}}" + folders: + - path: "/srv/cronjob" + owner: "{{ "{{ users.default.name }}" }}" + group: root + mode: "0755" + + project: + enabled: true + stack_file: "{{ cookiecutter.stack_location }}" + stack_name: "{{ cookiecutter.stack_name }}" + main_folder: "/srv/{{ cookiecutter.stack_prefix }}" + env_vars: + IMAGE_NAME_PREFIX: "{{ cookiecutter.__container_image_prefix }}" + IMAGE_TAG: "latest" + STACK_NAME: "{{ cookiecutter.stack_name}}" + STACK_HOSTNAME: "{{ cookiecutter.hostname }}" + STACK_PREFIX: "{{ cookiecutter.__devops_stack_prefix }}" + CERTRESOLVER: "le" + folders: + - path: "/srv/{{ cookiecutter.stack_prefix }}" + owner: "{{ "{{ users.default.name }}" }}" + group: root + mode: "0755" + - path: "/srv/{{ cookiecutter.stack_prefix }}/data" + owner: {{ cookiecutter.__devops_project_user }} + group: root + mode: "0750" diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/swap.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/swap.yml similarity index 100% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/swap.yml rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/swap.yml diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/swarm.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/swarm.yml similarity index 96% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/swarm.yml rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/swarm.yml index 12f3637b..9cbff2d7 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/swarm.yml +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/swarm.yml @@ -1,4 +1,4 @@ --- swarm: advertise_addr: "{{ "{{ ansible_default_ipv4.address }}" }}" - public_network: "{{ cookiecutter.__devops_swarm_public_network }}" + public_network: "{{ cookiecutter.__devops_swarm_public_network}}" diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/ufw.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/ufw.yml similarity index 50% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/ufw.yml rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/ufw.yml index a1140e65..f4f3d38b 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/ufw.yml +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/ufw.yml @@ -1,5 +1,5 @@ --- -ssh_port: 22 +ssh_port: "{{ "{{ sshd.port }}" }}" additional_ports: - 80 - 443 diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/users.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/users.yml new file mode 100644 index 00000000..9e08a54a --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/users.yml @@ -0,0 +1,9 @@ +--- +users: + setup: + name: root + homedir: /root + default: + name: plone + group: sudo + additional_keys: [] diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/vault.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/vault.yml new file mode 100644 index 00000000..6978fbcb --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/group_vars/all/vault.yml @@ -0,0 +1,3 @@ +--- +vault: + proxy_credentials: "" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/hosts.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/hosts.yml new file mode 100644 index 00000000..884d2b25 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/inventory/hosts.yml @@ -0,0 +1,25 @@ +--- +cluster: + hosts: + {{ cookiecutter.hostname }}: + ansible_user: root + ansible_host: {{ cookiecutter.hostname_or_ip }} + host: {{ cookiecutter.__devops_host }} + hostname: {{ cookiecutter.hostname }} + swarm_node: + labels: + type: manager + env: production + + children: + cluster_managers: + children: + cluster_manager: + hosts: + {{ cookiecutter.hostname }}: + + cluster_managers_additional: + hosts: + + cluster_workers: + hosts: diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/_connect.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/_connect.yml new file mode 100644 index 00000000..de18dbd5 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/_connect.yml @@ -0,0 +1,31 @@ +--- +# Playbook: playbooks/_connect.yml +# Description: Check connectivity to servers, report OS version +# +# Usage: +# +# # All servers +# uv run ansible-playbook playbooks/_connect.yml +# +# # One server +# uv run ansible-playbook playbooks/_connect.yml --limit cluster_manager +# +- name: "Check connectivity" + hosts: "all" + remote_user: "{{ users.default.name }}" + gather_facts: true + become: true + + tasks: + - name: "Display OS version information" + ansible.builtin.debug: + msg: "{{ ansible_hostname }}: {{ ansible_lsb.description }}" + + - name: "Display IP Addresses" + ansible.builtin.debug: + msg: "{{ item }}" + loop: "{{ ansible_all_ipv4_addresses }}" + + handlers: + - name: "Include default handlers" + ansible.builtin.import_tasks: ../tasks/handlers/common.yml diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/deploy.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/deploy.yml new file mode 100644 index 00000000..4d3f2aa4 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/deploy.yml @@ -0,0 +1,48 @@ +--- +# Playbook: playbooks/deploy.yml +# Description: Deploy a stack in the cluster +# +# Usage: +# +# # All Stacks +# uv run ansible-playbook playbooks/deploy.yml --limit cluster_manager +# +# # All Stacks in staging +# uv run ansible-playbook playbooks/deploy.yml --limit cluster_manager_staging +# +# # All Stacks in production +# uv run ansible-playbook playbooks/deploy.yml --limit cluster_manager_production +# +# # One app +# uv run ansible-playbook playbooks/deploy.yml --tags traefik +# uv run ansible-playbook playbooks/deploy.yml --tags cronjob +# uv run ansible-playbook playbooks/deploy.yml --tags project +# +- name: "Deploy Stacks" + hosts: "cluster_manager" + remote_user: "{{ users.default.name }}" + gather_facts: true + become: true + + tasks: + + - name: "Traefik: Deploy" + tags: + - traefik + vars: + stack: "{{ stacks.traefik }}" + ansible.builtin.import_tasks: ../tasks/stacks/task_deploy.yml + + - name: "Cronjob: Deploy" + tags: + - cronjob + vars: + stack: "{{ stacks.cronjob }}" + ansible.builtin.import_tasks: ../tasks/stacks/task_deploy.yml + + - name: "Project: Deploy" + tags: + - project + vars: + stack: "{{ stacks.project }}" + ansible.builtin.import_tasks: ../tasks/stacks/task_deploy.yml diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/setup.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/setup.yml new file mode 100644 index 00000000..38dc707d --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/setup.yml @@ -0,0 +1,91 @@ +--- +# Playbook: playbooks/setup.yml +# Description: Bootstrap a new server, or a group of servers +# +# Usage: +# +# - Run in all cluster servers +# uv run ansible-playbook playbooks/setup.yml --limit cluster +# +- name: "Bootstrap a server" + hosts: "all" + gather_facts: true + become: true + + pre_tasks: + - name: "Proxy: Setup" + ansible.builtin.import_tasks: ../tasks/base/task_proxy.yml + + - name: "Base Packages: Setup" + ansible.builtin.import_tasks: ../tasks/base/task_base_packages.yml + + roles: + - role: geerlingguy.swap + + tasks: + + - name: "Hostname" + tags: + - base + - hostname + ansible.builtin.import_tasks: ../tasks/base/task_hostname.yml + + - name: "Timezone" + tags: + - base + - timezone + become: true + community.general.timezone: + name: "{{ timezone }}" + + - name: "Disks" + tags: + - base + - disks + ansible.builtin.import_tasks: ../tasks/base/task_mount_points.yml + + - name: "Setup default user" + tags: + - base + - user + ansible.builtin.import_tasks: ../tasks/base/task_user.yml + + - name: "SSH: Setup" + tags: + - base + - ssh + ansible.builtin.import_tasks: ../tasks/base/task_ssh.yml + + - name: "Docker: Setup" + tags: + - base + - docker + ansible.builtin.import_tasks: ../tasks/docker/task_setup.yml + + - name: "Docker Swarm: Setup" + tags: + - base + - swarm + when: inventory_hostname in groups['cluster'] + ansible.builtin.import_tasks: ../tasks/docker/task_swarm.yml + + - name: "Docker Swarm: Initial Stacks" + tags: + - base + - swarm + when: inventory_hostname in groups['cluster_manager'] + block: + - name: "Docker Swarm: Deploy Traefik" + vars: + stack: "{{ stacks.traefik }}" + ansible.builtin.import_tasks: ../tasks/stacks/task_deploy.yml + + - name: "Docker Swarm: Deploy Cronjob" + vars: + stack: "{{ stacks.cronjob }}" + ansible.builtin.import_tasks: ../tasks/stacks/task_deploy.yml + + + handlers: + - name: "Include default handlers" + ansible.builtin.import_tasks: ../tasks/handlers/common.yml diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/stacks.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/stacks.yml new file mode 100644 index 00000000..97894f51 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/playbooks/stacks.yml @@ -0,0 +1,27 @@ +--- +# Playbook: playbooks/stacks.yml +# Description: Information about stacks in the cluster +# +# Usage: +# +# # All Stacks +# uv run ansible-playbook playbooks/stacks.yml +# +# +- name: "Stacks Information" + hosts: "cluster_manager" + remote_user: "{{ users.default.name }}" + gather_facts: true + become: true + + tasks: + + - name: "Gather stacks information" + community.docker.docker_stack_info: + register: stacks + + - name: "Show results" + ansible.builtin.include_tasks: ../tasks/docker/task_stack.yml + loop: "{{ stacks.results }}" + loop_control: + loop_var: stack diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/pyproject.toml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/pyproject.toml new file mode 100644 index 00000000..b4b16a32 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "devops" +version = "0.1.0" +description = "{{ cookiecutter.description }}" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "ansible==11.0.0", + "dnspython==2.7.0", + "python-gitlab==5.0.0", + "python-dateutil==2.9.0", + "passlib==1.7.4", + "ansible-lint==24.10.0", +] + +[tool.uv] +environments = ["platform_system != 'Windows'"] +package = false diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/requirements.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/requirements.yml new file mode 100644 index 00000000..27adde89 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/requirements.yml @@ -0,0 +1,12 @@ +--- +roles: + - name: geerlingguy.swap + version: "1.2.0" + - name: geerlingguy.docker + version: "7.4.2" + +collections: + - name: community.general + - name: community.crypto + - name: community.docker + - name: ansible.posix diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_base_packages.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_base_packages.yml new file mode 100644 index 00000000..0bb31cf8 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_base_packages.yml @@ -0,0 +1,31 @@ +# code: language=ansible +--- +# Base packages +# +- name: "Base: Install Base Packages" + tags: + - base + ansible.builtin.apt: + name: "{{ base_packages.install }}" + update_cache: true + state: present + +- name: "Base: Remove Packages" + tags: + - base + ansible.builtin.apt: + name: "{{ base_packages.remove }}" + purge: true + state: absent + +- name: "Base: Install Python Packages" + tags: + - base + ansible.builtin.pip: + break_system_packages: true + name: + - docker + - jsondiff + - pyyaml + - uv + state: present diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_hostname.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_hostname.yml similarity index 83% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_hostname.yml rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_hostname.yml index da16164a..626deec0 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_hostname.yml +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_hostname.yml @@ -1,9 +1,12 @@ +# code: language=ansible --- - name: "Configure hostname" + tags: + - hostname block: # Add the host to the /etc/hosts - - name: "Add {{ host }} and {{ hostname }} to /etc/hosts" + - name: "Add to /etc/hosts: {{ host + ' and ' + hostname }}" ansible.builtin.lineinfile: dest: /etc/hosts line: "127.0.0.1 {{ host }} {{ hostname }}" @@ -15,6 +18,3 @@ cmd: "hostnamectl set-hostname {{ host }}" register: output changed_when: output.rc != 0 - - tags: - - hostname diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_mount_points.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_mount_points.yml new file mode 100644 index 00000000..85cab29a --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_mount_points.yml @@ -0,0 +1,23 @@ +# code: language=ansible +--- +- name: "FS: Configure mountpoints" + when: additional_mounts + tags: + - disk + block: + - name: "Create filesystem on disk" + community.general.filesystem: + fstype: "{{ item.value.fstype }}" + state: "present" + dev: "{{ item.value.src }}" + when: item.value.path + loop: "{{ lookup('ansible.builtin.dict', additional_mounts, wantlist=True) }}" + + - name: "Configure additional mountpoints" + ansible.posix.mount: + path: "{{ item.value.path }}" + src: "{{ item.value.src }}" + fstype: "{{ item.value.fstype }}" + state: "{{ item.value.state }}" + when: item.value.path + loop: "{{ lookup('ansible.builtin.dict', additional_mounts, wantlist=True) }}" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_proxy.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_proxy.yml new file mode 100644 index 00000000..e66291a9 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_proxy.yml @@ -0,0 +1,19 @@ +# code: language=ansible +--- +# Setup proxies +# +- name: "Proxy: Setup /etc/environment" + ansible.builtin.template: + src: "{{ playbook_dir }}/../etc/base/environment.j2" + dest: "/etc/environment" + owner: root + group: root + mode: "0755" + +- name: "Proxy: Setup /etc/apt/apt.conf.d/95proxies" + ansible.builtin.template: + src: "{{ playbook_dir }}/../etc/base/apt_proxy.j2" + dest: "/etc/apt/apt.conf.d/95proxies" + owner: root + group: root + mode: "0755" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_ssh.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_ssh.yml new file mode 100644 index 00000000..7f635326 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_ssh.yml @@ -0,0 +1,63 @@ +# code: language=ansible +--- +# Setup SSH +# +- name: "SSH: Generate a deployment key" + community.crypto.openssh_keypair: + path: "{{ playbook_dir }}/../etc/keys/{{ users.default.name }}_prod_deploy_ed25519" + type: "ed25519" + delegate_to: localhost + become: false + +- name: "SSH: Read authorized_keys from {{ users.setup.name }}" + ansible.builtin.command: + cmd: "cat {{ users.setup.homedir }}/.ssh/authorized_keys" + become_user: "{{ users.setup.name }}" + become: true + register: main_keys + changed_when: false + +- name: "SSH: Set setup keys for the new user" + ansible.posix.authorized_key: + user: "{{ users.default.name }}" + key: "{{ item }}" + loop: "{{ main_keys.stdout_lines }}" + become_user: "{{ users.default.name }}" + become: true + +- name: "SSH: Additional keys" + ansible.posix.authorized_key: + user: "{{ users.default.name }}" + key: "{{ lookup('file', item) }}" + with_fileglob: + - "{{ playbook_dir }}/../etc/keys/*.pub" + become_user: "{{ users.default.name }}" + become: true + +- name: Disallow password authentication + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "^PasswordAuthentication" + line: "PasswordAuthentication no" + state: present + +- name: Set root SSH access + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "^PermitRootLogin" + line: "PermitRootLogin {{ sshd.allow_root }}" + state: present + +- name: Set AllowUsers in SSH + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + insertafter: "^# Authentication" + line: "AllowUsers {{ sshd.allow_users }}" + notify: Restart ssh + +- name: "SSH: Listen to port {{ sshd.port }}" + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + insertafter: "^#Port 22" + line: "Port {{ sshd.port }}" + notify: Restart ssh diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_ufw.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_ufw.yml similarity index 59% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_ufw.yml rename to templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_ufw.yml index 94d895f0..7f41cfea 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_ufw.yml +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_ufw.yml @@ -1,7 +1,8 @@ +# code: language=ansible --- # Setup ufw # -- name: "ufw: Install package" +- name: "UFW: Install package" ansible.builtin.apt: name: - ufw @@ -10,16 +11,18 @@ tags: - ufw -- name: "ufw: Configuration" +- name: "UFW: Configuration" + tags: + - ufw block: - - name: "ufw: Set logging to {{ ufw.logging }}" - ansible.builtin.ufw: + - name: "UFW: Set logging to {{ ufw.logging }}" + community.general.ufw: logging: "{{ ufw.logging }}" notify: - - restart ufw + - Restart ufw - - name: "ufw: Configure default values" - ansible.builtin.ufw: + - name: "UFW: Configure default values" + community.general.ufw: direction: "{{ item.direction }}" policy: "{{ item.policy }}" with_items: @@ -28,24 +31,22 @@ - direction: 'outgoing' policy: 'allow' notify: - - restart ufw + - Restart ufw - - name: "ufw: Configure rules" - ansible.builtin.ufw: + - name: "UFW: Configure rules" + community.general.ufw: rule: "{{ item.rule }}" port: "{{ item.port }}" proto: "{{ item.proto }}" from_ip: "{{ item.from_ip | default('any') }}" with_items: "{{ ufw.rules }}" notify: - - restart ufw + - Restart ufw + +- name: "UFW: Setup Service" tags: - ufw - -- name: "ufw: Setup Service" block: - - name: "ufw: Enable firewall" - ansible.builtin.ufw: + - name: "UFW: Enable firewall" + community.general.ufw: state: enabled - tags: - - ufw diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_user.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_user.yml new file mode 100644 index 00000000..541c36e9 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/base/task_user.yml @@ -0,0 +1,27 @@ +# code: language=ansible +--- +# Setup User +# +- name: "Add user {{ users.default.name }}" + ansible.builtin.user: + name: "{{ users.default.name }}" + group: sudo + shell: /bin/bash + generate_ssh_key: true + ssh_key_file: .ssh/id_ed25519 + ssh_key_type: "ed25519" + +- name: "Allow default user to sudo without password" + ansible.builtin.copy: + content: "{{ users.default.name }} ALL=(ALL) NOPASSWD: ALL" + dest: "/etc/sudoers.d/{{ users.default.name }}" + mode: "0640" + +- name: "Config ssh for default user" + become: true + become_user: "{{ users.default.name }}" + ansible.builtin.copy: + src: "{{ playbook_dir }}/../etc/ssh/default_ssh_config.j2" + dest: "/home/{{ users.default.name }}/.ssh/config" + owner: "{{ users.default.name }}" + mode: "0600" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_setup.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_setup.yml new file mode 100644 index 00000000..7bfd73c0 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_setup.yml @@ -0,0 +1,35 @@ +# code: language=ansible +--- +# Setup Docker +# +- name: "Docker: Setup Proxy" + when: proxy is defined and proxy.https_proxy is defined + tags: + - docker + - proxy + block: + - name: "Docker: Create /etc/systemd/system/docker.service.d/ folder" + ansible.builtin.file: + dest: "/etc/systemd/system/docker.service.d" + state: directory + owner: root + group: root + mode: "0755" + + - name: "Docker: Create /etc/systemd/system/docker.service.d/http-proxy.conf" + ansible.builtin.template: + src: "{{ playbook_dir }}/../etc/docker/systemd/http-proxy.conf.j2" + dest: "/etc/systemd/system/docker.service.d/http-proxy.conf" + owner: root + group: root + mode: "0755" + notify: + - Reload systemd daemon + - Restart Docker + +- name: "Docker: Install Docker" + tags: + - docker + - install + ansible.builtin.include_role: + name: "geerlingguy.docker" diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_stack.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_stack.yml new file mode 100644 index 00000000..e47dd7c6 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_stack.yml @@ -0,0 +1,19 @@ +# code: language=ansible +--- +- name: "Stack information: {{ stack.Name }}" + ansible.builtin.debug: + msg: "{{ stack.Name }} - {{ stack.Services }}" + +- name: "Get services for {{ stack.Name }}" + community.docker.docker_swarm_info: + services: true + services_filters: + name: "{{ stack.Name }}" + register: services + +- name: "Display services for {{ stack.Name }}" + ansible.builtin.debug: + msg: "{{ service.Name }} - {{ service.Image }} - {{ service.Replicas }}" + loop: "{{ services.services }}" + loop_control: + loop_var: service diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_swarm.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_swarm.yml new file mode 100644 index 00000000..41a53ffd --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/docker/task_swarm.yml @@ -0,0 +1,135 @@ +# code: language=ansible +--- +# Docker Swarm + +- name: "Docker Swarm: Basic facts" + tags: + - swarm + - setup + ansible.builtin.set_fact: + is_manager: "{{ inventory_hostname in groups['cluster_manager'] }}" + is_additional_manager: "{{ inventory_hostname in groups['cluster_managers_additional'] }}" + is_worker: "{{ inventory_hostname in groups['cluster_workers'] }}" + cluster_manager: "{{ groups['cluster_manager'][0] }}" + +- name: "Docker Swarm: Cluster Manager Info" + tags: + - swarm + - setup + ansible.builtin.setup: + delegate_to: "{{ cluster_manager }}" + delegate_facts: true + +- name: "Docker Swarm: Initial Setup" + tags: + - swarm + - setup + when: is_manager + block: + - name: "Docker Swarm: Init Swarm" + community.docker.docker_swarm: + state: present + advertise_addr: "{{ swarm.advertise_addr }}" + +- name: "Docker Swarm: Get Swarm facts" + tags: + - swarm + - setup + delegate_to: "{{ cluster_manager }}" + delegate_facts: true + community.docker.docker_swarm_info: + services: false + ignore_errors: true + register: raw_swarm_info + +- name: "Docker Swarm: Swarm Info" + tags: + - swarm + - setup + ansible.builtin.set_fact: + token_manager: "{{ raw_swarm_info.swarm_facts.JoinTokens.Manager }}" + token_worker: "{{ raw_swarm_info.swarm_facts.JoinTokens.Worker }}" + manager_ip: "{{ hostvars[cluster_manager]['ansible_default_ipv4']['address'] }}" + +- name: "Docker Swarm: Register Node" + tags: + - swarm + - setup + when: not is_manager + block: + - name: "Docker Swarm: Add manager to cluster" + community.docker.docker_swarm: + state: join + advertise_addr: "{{ swarm.advertise_addr }}" + join_token: "{{ token_manager }}" + remote_addrs: + - "{{ manager_ip }}" + when: is_additional_manager + + - name: "Docker Swarm: Add Worker to cluster" + community.docker.docker_swarm: + state: join + advertise_addr: "{{ swarm.advertise_addr }}" + join_token: "{{ token_worker }}" + remote_addrs: + - "{{ manager_ip }}" + when: is_worker + +- name: "Docker Swarm: Add labels to node" + community.docker.docker_node: + hostname: "{{ host }}" + labels: "{{ swarm_node.labels }}" + labels_state: merge + delegate_to: "{{ cluster_manager }}" + +- name: "Docker Swarm: Set public network" + tags: + - swarm + - network + become: true + become_user: "{{ users.default.name }}" + when: is_manager + block: + - name: "Docker Swarm: Check for public network" + community.docker.docker_network_info: + name: "{{ swarm.public_network }}" + register: docker_swarm_net_public + + - name: "Docker Swarm: Create public network" + community.docker.docker_network: + name: "{{ swarm.public_network }}" + internal: false + attachable: true + driver: overlay + driver_options: + "com.docker.network.driver.mtu": "{{ docker_daemon_options.mtu }}" + when: not docker_swarm_net_public.exists + +- name: "Docker Swarm: Cronjob to purge data" + tags: + - swarm + - cron + become: true + become_user: "{{ users.default.name }}" + block: + - name: "Docker Swarm: Purge old containers" + ansible.builtin.cron: + name: "Purge old containers" + job: "/usr/bin/docker container prune -f" + hour: "*" + minute: 5 + state: present + - name: "Docker Swarm: Purge old images" + ansible.builtin.cron: + name: "Purge old images" + job: "/usr/bin/docker image prune -f" + hour: "*" + minute: 10 + state: present + - name: "Docker Swarm: Purge old volumes" + ansible.builtin.cron: + name: "Purge old volumes" + job: "/usr/bin/docker volume prune -f" + hour: "*" + minute: 15 + state: present diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/handlers/common.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/handlers/common.yml new file mode 100644 index 00000000..707af838 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/handlers/common.yml @@ -0,0 +1,44 @@ +# code: language=ansible +--- +- name: "Reload systemd daemon" + ansible.builtin.systemd: + daemon_reload: true + become_user: root + become: true + +- name: "Restart ufw" + ansible.builtin.service: + name: ufw + state: restarted + become: true + become_user: root + +- name: "Restart ssh" + ansible.builtin.service: + name: ssh + state: restarted + become: true + become_user: root + +- name: "Restart Docker" + ansible.builtin.service: + name: docker + state: restarted + become_user: root + become: true + +- name: "Restart multipathd" + ansible.builtin.service: + name: multipathd + state: reloaded + become_user: root + become: true + +- name: "Restart rpcbind" + ansible.builtin.systemd: + daemon_reload: true + name: rpcbind.socket + enabled: true + state: restarted + become_user: root + become: true diff --git a/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/stacks/task_deploy.yml b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/stacks/task_deploy.yml new file mode 100644 index 00000000..2bb88296 --- /dev/null +++ b/templates/devops/ansible/{{ cookiecutter.__folder_name }}/tasks/stacks/task_deploy.yml @@ -0,0 +1,33 @@ +# code: language=ansible +--- +# Deploy Stack +# +- name: "Stack: Prepare Deployment" + when: stack.enabled | bool + block: + + - name: "Stack: Create folders" + ansible.builtin.file: + dest: "{{ item.path }}" + state: directory + owner: "{{ item.owner }}" + group: "{{ item.group }}" + mode: "{{ item.mode }}" + with_items: "{{ stack.folders }}" + + - name: "Stack: Copy stack config" + ansible.builtin.template: + src: "{{ playbook_dir }}/../{{ stack.stack_file }}" + dest: "{{ stack.main_folder }}/{{ stack.stack_name }}.yml" + owner: "{{ users.default.name }}" + group: root + mode: "0755" + +- name: "Stack: Deploy Stack" + when: stack.enabled | bool + community.docker.docker_stack: + state: present + name: "{{ stack.stack_name }}" + compose: + - "{{ stack.main_folder }}/{{ stack.stack_name }}.yml" + environment: "{{ stack.env_vars }}" diff --git a/templates/projects/classic/cookiecutter.json b/templates/projects/classic/cookiecutter.json index 24bf24d6..7a0f1b86 100644 --- a/templates/projects/classic/cookiecutter.json +++ b/templates/projects/classic/cookiecutter.json @@ -31,7 +31,7 @@ "__profile_version": "{% now 'utc', '%Y%m%d001' %}", "__profile_language": "{{ cookiecutter.language_code|gs_language_code }}", "__locales_language": "{{ cookiecutter.language_code|locales_language_code }}", - "__gha_version_checkout": "v4", + "__gha_version_checkout": "v5", "__gha_version_setup_uv": "v5", "__gha_version_cache": "v4", "__gha_version_docker_stack": "v1.2.0", @@ -40,7 +40,7 @@ "__gha_version_docker_buildx": "v3", "__gha_version_docker_login": "v3", "__gha_version_docker_build_push": "v6", - "__gha_version_paths_filter": "v3", + "__gha_version_paths_filter": "v3.0.2", "__devops_host": "{{ cookiecutter.hostname | extract_host }}", "__devops_compose_name": "{{ cookiecutter.project_slug | replace('.','-') | replace('_','-') }}", "__devops_stack_name": "{{ cookiecutter.hostname | replace('.','-') | replace('_','-') }}", @@ -51,7 +51,7 @@ "__devops_traefik_stack_include_ui": "no", "__devops_traefik_docker_network": "{{ cookiecutter.__devops_stack_name }}_{{ cookiecutter.__devops_swarm_stack_network }}", "__devops_varnish_version": "7.6", - "__devops_db_version": "14.15", + "__devops_postgres_version": "18.1", "__devops_db_password": "{{ random_ascii_string(12) }}", "__devops_zeo_version": "6.0.0", "__backend_addon_git_initialize": "0", diff --git a/templates/projects/classic/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml b/templates/projects/classic/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml index 85472f40..4b8eab76 100644 --- a/templates/projects/classic/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml +++ b/templates/projects/classic/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml @@ -192,7 +192,7 @@ services: {%- if cookiecutter.devops_storage == 'relstorage' %} db: - image: postgres:{{ cookiecutter.__devops_db_version }} + image: postgres:{{ cookiecutter.__devops_postgres_version }} environment: POSTGRES_USER: plone POSTGRES_PASSWORD: {{ cookiecutter.__devops_db_password }} diff --git a/templates/projects/classic/{{ cookiecutter.__folder_name }}/docker-compose.yml b/templates/projects/classic/{{ cookiecutter.__folder_name }}/docker-compose.yml index 1a7e9554..cb5cf9b1 100644 --- a/templates/projects/classic/{{ cookiecutter.__folder_name }}/docker-compose.yml +++ b/templates/projects/classic/{{ cookiecutter.__folder_name }}/docker-compose.yml @@ -110,7 +110,7 @@ services: {%- if cookiecutter.devops_storage == 'relstorage' %} db: - image: postgres:{{ cookiecutter.__devops_db_version }} + image: postgres:{{ cookiecutter.__devops_postgres_version }} environment: POSTGRES_USER: plone POSTGRES_PASSWORD: {{ cookiecutter.__devops_db_password }} diff --git a/templates/projects/classic/{{ cookiecutter.__folder_name }}/repository.toml b/templates/projects/classic/{{ cookiecutter.__folder_name }}/repository.toml index 8745001e..736807c8 100644 --- a/templates/projects/classic/{{ cookiecutter.__folder_name }}/repository.toml +++ b/templates/projects/classic/{{ cookiecutter.__folder_name }}/repository.toml @@ -1,17 +1,13 @@ [repository] name = "{{ cookiecutter.__project_slug }}" -managed_by_uv = {{ cookiecutter.__backend_managed_by_uv }} changelog = "CHANGELOG.md" version = "version.txt" -compose = "docker-compose.yml" +compose = ["docker-compose.yml"] [repository.towncrier] section = "Project" settings = "towncrier.toml" -[backend] -path = "backend" - [backend.package] name = "{{ cookiecutter.python_package_name }}" path = "backend" diff --git a/templates/projects/monorepo/README.md b/templates/projects/monorepo/README.md index 85b1330d..85ab516a 100644 --- a/templates/projects/monorepo/README.md +++ b/templates/projects/monorepo/README.md @@ -110,7 +110,7 @@ The table below describes the options you can customize using the [Cookiecutter | `python_package_name` | Name of the Python package used to configure your project. It needs to be Python-importable, so no dashes, spaces or special characters are allowed. | `plone_site` | | `frontend_addon_name` | Name of the Volto addon package used to configure your frontend project. No spaces or special characters are allowed. | `volto-plone-site` | | `language_code` | Language to be used on the site. | `pt-br` | -| `github_organization` | Used for GitHub, GitLab, and Docker repositories. GitHub or GitLab username or organization slug from URL. | `collective` | +| `organization` | Used for GitHub, GitLab, and Docker repositories. GitHub or GitLab username or organization slug from URL. | `collective` | | `container_registry` | Container registry to be used. | `github` | | `devops_storage` | Storage backend to be used in the deployment stack. | `relstorage` | | `devops_ansible` | Should we create an Ansible playbook to bootstrap and deploy this project? | `Yes` | diff --git a/templates/projects/monorepo/cookiecutter.json b/templates/projects/monorepo/cookiecutter.json index aefe55f6..1d4e99b8 100644 --- a/templates/projects/monorepo/cookiecutter.json +++ b/templates/projects/monorepo/cookiecutter.json @@ -11,7 +11,8 @@ "python_package_name": "{{ cookiecutter.project_slug|replace(' ', '')|replace('-', '.') }}", "frontend_addon_name": "volto-{{ cookiecutter.python_package_name|replace('_', '-')|replace('.', '-') }}", "language_code": ["en", "de", "es", "pt-br", "nl", "fi", "it", "se"], - "github_organization": "collective", + "organization": "collective", + "repository_url": "https://github.com/{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", "container_registry": ["github", "docker_hub", "gitlab"], "devops_storage": ["relstorage", "zeo", "filestorage"], "devops_cache": ["1", "0"], @@ -19,47 +20,41 @@ "devops_gha_deploy": ["1", "0"], "initialize_documentation": ["1", "0"], "__project_slug": "{{ cookiecutter.project_slug }}", - "__repository_url": "https://github.com/{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}", - "__repository_git": "git@github.com:{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}", + "__repository_url": "{{ cookiecutter.repository_url }}", + "__repository_git": "git@github.com:{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}", "__version_package": "1.0.0a0", "__version_frontend_package": "1.0.0-alpha.0", "__feature_headless": "1", "__feature_distribution": "0", - "__backend_managed_by_uv": "false", + "__backend_managed_by_uv": "true", "__npm_package_name": "{{ cookiecutter.frontend_addon_name }}", "__folder_name": "{{ cookiecutter.project_slug }}", "__python_package_name_upper": "{{ cookiecutter.python_package_name | pascal_case }}", "__python_version": "3.12", "__node_version": "{{ cookiecutter.volto_version | node_version_for_volto }}", "__version_plone_volto": "{{ cookiecutter.volto_version }}", - "__version_pnpm": "9.1.1", + "__version_pnpm": "{{ '10.20.0' if cookiecutter.volto_version >= '19' else '9.1.1' }}", "__container_registry_prefix": "{{ cookiecutter.container_registry | image_prefix }}", - "__container_image_prefix": "{{ cookiecutter.__container_registry_prefix }}{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}", + "__container_image_prefix": "{{ cookiecutter.__container_registry_prefix }}{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", "__profile_version": "{% now 'utc', '%Y%m%d001' %}", "__profile_language": "{{ cookiecutter.language_code|gs_language_code }}", "__locales_language": "{{ cookiecutter.language_code|locales_language_code }}", - "__gha_version_checkout": "v4", + "__gha_version_checkout": "v5", "__gha_version_setup_node": "v4", - "__gha_version_setup_uv": "v5", + "__gha_version_plone_meta": "2.x", "__gha_version_cache": "v4", "__gha_version_docker_stack": "v1.2.0", - "__gha_version_docker_metadata": "v5", - "__gha_version_docker_qemu": "v3", - "__gha_version_docker_buildx": "v3", - "__gha_version_docker_login": "v3", - "__gha_version_docker_build_push": "v6", - "__gha_version_paths_filter": "v3", + "__gha_version_paths_filter": "v3.0.2", "__devops_host": "{{ cookiecutter.hostname | extract_host }}", "__devops_compose_name": "{{ cookiecutter.project_slug | replace('.','-') | replace('_','-') }}", "__devops_stack_name": "{{ cookiecutter.hostname | replace('.','-') | replace('_','-') }}", + "__devops_stack_prefix": "{{ cookiecutter.__devops_host }}", "__devops_swarm_public_network": "nw-public", "__devops_swarm_stack_network": "nw-internal", "__devops_traefik_version": "v2.11", "__devops_traefik_local_include_ui": "yes", - "__devops_traefik_stack_include_ui": "no", - "__devops_traefik_docker_network": "{{ cookiecutter.__devops_stack_name }}_{{ cookiecutter.__devops_swarm_stack_network }}", "__devops_varnish_version": "7.6", - "__devops_db_version": "14.15", + "__devops_postgres_version": "18.1", "__devops_db_password": "{{ random_ascii_string(12) }}", "__devops_zeo_version": "6.0.0", "__backend_addon_git_initialize": "0", @@ -88,7 +83,8 @@ "nl": "Nederlands", "se": "Svenska" }, - "github_organization": "GitHub or GitLab username or organization slug from URL", + "organization": "GitHub or GitLab username or organization slug from URL", + "repository_url": "URL to the repository", "container_registry": { "__prompt__": "Container Registry", "github": "GitHub Container Registry", @@ -144,12 +140,15 @@ "__cookieplone_subtemplates": [ ["add-ons/backend", "Setup Backend", "1"], ["add-ons/frontend", "Setup Frontend", "1"], + ["ci/github", "Setup CI", "1"], [ "docs/starter", - "Generate documentation scaffold", + "Setup Documentation", "{{cookiecutter.initialize_documentation}}" ], + ["devops/ansible", "Setup Ansible", "{{cookiecutter.devops_ansible}}"], ["sub/cache", "Setup Cache", "{{cookiecutter.devops_cache}}"], + ["sub/vscode", "Setup VSCode", "1"], ["sub/project_settings", "Setup Project Settings", "1"] ], "__cookieplone_repository_path": "", diff --git a/templates/projects/monorepo/hooks/post_gen_project.py b/templates/projects/monorepo/hooks/post_gen_project.py index ff2109c9..44e1bf13 100644 --- a/templates/projects/monorepo/hooks/post_gen_project.py +++ b/templates/projects/monorepo/hooks/post_gen_project.py @@ -4,6 +4,7 @@ import subprocess from collections import OrderedDict from copy import deepcopy +from datetime import date from pathlib import Path from cookieplone import generator @@ -11,6 +12,8 @@ context: OrderedDict = {{cookiecutter}} +calver_date = f"{date.today().strftime('%Y%m%d')}.0" # YYYYMMDD +configuration_version = f"{date.today().strftime('%Y%m%d')}001" # YYYYMMDD BACKEND_ADDON_REMOVE = [ ".github", @@ -26,18 +29,6 @@ POST_GEN_TO_REMOVE = { - "devops-ansible": [ - "devops/.env_dist", - "devops/.gitignore", - "devops/ansible.cfg", - "devops/etc", - "devops/inventory", - "devops/Makefile", - "devops/playbooks", - "devops/requirements", - "devops/tasks", - "devops/README.md", - ], "devops-gha": [ ".github/workflows/manual_deploy.yml", "devops/.env_gha", @@ -64,9 +55,10 @@ def _fix_frontend_addon_name(context: OrderedDict) -> OrderedDict: return context -def handle_devops_ansible(context: OrderedDict, output_dir: Path): - """Clean up ansible.""" - files.remove_files(output_dir, POST_GEN_TO_REMOVE["devops-ansible"]) +def handle_version(context: OrderedDict, output_dir: Path): + """Update version.txt.""" + version_path = output_dir / "version.txt" + version_path.write_text(calver_date) def handle_devops_gha_deploy(context: OrderedDict, output_dir: Path): @@ -100,6 +92,8 @@ def generate_addons_backend(context, output_dir): """Run Plone Addon generator.""" output_dir = output_dir folder_name = "backend" + context["initial_version"] = f"{calver_date}" + context["configuration_version"] = f"{configuration_version}" # Headless context["feature_headless"] = "1" context["initialize_documentation"] = "0" @@ -119,6 +113,7 @@ def generate_addons_frontend(context, output_dir): # Handle packages inside an organization context = _fix_frontend_addon_name(context) frontend_addon_name = context["frontend_addon_name"] + context["initial_version"] = f"{calver_date}.0" context["initialize_documentation"] = "0" path = generator.generate_subtemplate( f"{TEMPLATES_FOLDER}/add-ons/frontend", @@ -141,6 +136,26 @@ def generate_addons_frontend(context, output_dir): release_it_path.write_text(json.dumps(data, indent=2)) +def generate_devops_ansible(context, output_dir): + """Run Devops - Ansible generator.""" + output_dir = output_dir + folder_name = "devops/ansible" + + context["stack_location"] = f"../stacks/{context['hostname']}.yml" + context["stack_name"] = f"{context['__devops_stack_name']}" + context["stack_prefix"] = f"{context['__devops_stack_prefix']}" + context["hostname_or_ip"] = f"{context['hostname']}" + + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/devops/ansible", + output_dir, + folder_name, + context, + BACKEND_ADDON_REMOVE, + ) + files.remove_files(output_dir / folder_name, BACKEND_ADDON_REMOVE) + + def generate_docs_starter(context, output_dir): """Generate documentation scaffold""" output_dir = output_dir @@ -171,11 +186,53 @@ def generate_sub_project_settings(context: OrderedDict, output_dir: Path): folder_name = output_dir.name output_dir = output_dir.parent context = _fix_frontend_addon_name(context) + context["container_image_prefix"] = f"{context['__container_image_prefix']}" + context["cookieplone_template"] = f"{context['__cookieplone_template']}" + context["generator_sha"] = f"{context['__generator_sha']}" generator.generate_subtemplate( f"{TEMPLATES_FOLDER}/sub/project_settings", output_dir, folder_name, context ) +def generate_sub_vscode(context: OrderedDict, output_dir: Path): + """Configure VSCode settings.""" + # Use the same base folder + output_dir = output_dir + folder_name = ".vscode" + context["folder_name"] = folder_name + context["cookieplone_template"] = f"{context['__cookieplone_template']}" + context["generator_sha"] = f"{context['__generator_sha']}" + context["has_frontend"] = "1" + context["has_backend"] = "1" + context["has_github"] = "1" + context["is_monorepo"] = "1" + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/sub/vscode", output_dir, folder_name, context + ) + + +def generate_ci_github(context, output_dir): + """Run CI - GitHub.""" + output_dir = output_dir + folder_name = ".github" + + context["folder_name"] = ".github" + context["has_backend"] = "1" + context["has_frontend"] = "1" + context["has_docs"] = "{{ cookiecutter.initialize_documentation }}" + context["has_varnish"] = "{{ cookiecutter.devops_cache }}" + context["has_gha_deploy"] = "{{ cookiecutter.devops_gha_deploy }}" + context["cookieplone_template"] = "{{ cookiecutter.__cookieplone_template }}" + + generator.generate_subtemplate( + f"{TEMPLATES_FOLDER}/ci/github", + output_dir, + folder_name, + context, + [], + ) + + def run_actions(actions: list, output_dir: Path): for func, title, enabled in actions: if not int(enabled): @@ -231,9 +288,9 @@ def main(): # Cleanup / Git actions = [ [ - handle_devops_ansible, - "Remove Ansible files", - not int(context.get("devops_ansible")), # {{ cookiecutter.devops_ansible }} + handle_version, + "Update version.txt", + True, ], [ handle_devops_gha_deploy, diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/backend.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/backend.yml deleted file mode 100644 index 7e14c84e..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/backend.yml +++ /dev/null @@ -1,201 +0,0 @@ -name: Backend CI - -on: - workflow_call: - inputs: - base-tag: - required: true - type: string - image-name-prefix: - required: true - type: string - image-name-suffix: - required: true - type: string - python-version: - required: true - type: string - plone-version: - required: true - type: string - -defaults: - run: - working-directory: backend - -jobs: - - lint: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Install the latest version of uv - uses: astral-sh/setup-uv@{{ cookiecutter.__gha_version_setup_uv }} - with: - python-version: {{ "${{ inputs.python-version }}" }} - enable-cache: true - - - name: Check formatting - if: {{ "${{ success() || failure() }}" }} - id: ruff-format - run: uvx ruff@latest format --diff - - - name: Check lint - if: {{ "${{ success() || failure() }}" }} - id: ruff-lint - run: uvx ruff@latest check --diff - - - name: Check XML / ZCML - if: {{ "${{ success() || failure() }}" }} - id: zpretty - run: uvx zpretty@latest --check src - - - name: Check Package Metadata - if: {{ "${{ success() || failure() }}" }} - id: pyroma - run: uvx pyroma@latest -d . - - - name: Check Python Versions - if: {{ "${{ success() || failure() }}" }} - id: py-versions - run: uvx check-python-versions@latest . - - - name: Report - if: {{ "${{ success() || failure() }}" }} - run: | - echo '# Code Analysis' >> $GITHUB_STEP_SUMMARY - echo '| Test | Status |' >> $GITHUB_STEP_SUMMARY - echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY - echo '| Format | {{ "${{ steps.ruff-format.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - echo '| Lint | {{ "${{ steps.ruff-lint.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - echo '| XML / ZCML | {{ "${{ steps.zpretty.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - echo '| Package Metadata | {{ "${{ steps.pyroma.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - echo '| Python Versions | {{ "${{ steps.py-versions.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - - test: - runs-on: ubuntu-latest - env: - PYTHON_VERSION: {{ "${{ inputs.python-version }}" }} - PLONE_VERSION: {{ "${{ inputs.plone-version }}" }} - steps: - - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Install the latest version of uv - uses: astral-sh/setup-uv@{{ cookiecutter.__gha_version_setup_uv }} - with: - python-version: {{ "${{ inputs.python-version }}" }} - enable-cache: false - - - name: Restore uv cache - uses: actions/cache@{{ cookiecutter.__gha_version_cache }} - with: - path: {{ "${{ env.UV_CACHE_DIR }}" }} - key: uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.plone-version }}-${{ hashFiles('pyproject.toml') }}" }} - restore-keys: | - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.plone-version }}-${{ hashFiles('pyproject.toml') }}" }} - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.plone-version }}" }} - - - name: Install Plone and package - run: make install - - - name: Run tests - run: make test - - coverage: - runs-on: ubuntu-latest - needs: - - test - steps: - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Install the latest version of uv - uses: astral-sh/setup-uv@{{ cookiecutter.__gha_version_setup_uv }} - with: - python-version: {{ "${{ inputs.python-version }}" }} - enable-cache: false - - - name: Restore uv cache - uses: actions/cache@{{ cookiecutter.__gha_version_cache }} - with: - path: /tmp/.uv-cache - key: uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.plone-version }}-${{ hashFiles('pyproject.toml') }}" }} - restore-keys: | - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.plone-version }}-${{ hashFiles('pyproject.toml') }}" }} - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ inputs.plone-version }}" }} - - - name: Install Plone and package - run: make install - - - name: Run tests - run: make test-coverage - - - name: Report Coverage - run: | - echo "# Coverage Report" >> $GITHUB_STEP_SUMMARY - echo "$(uv run coverage report --format markdown)" >> $GITHUB_STEP_SUMMARY - - release: - runs-on: ubuntu-latest - needs: - - lint - - coverage - permissions: - contents: read - packages: write - - steps: - - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Docker meta - id: meta - uses: docker/metadata-action@{{ cookiecutter.__gha_version_docker_metadata }} - with: - images: | - {{ "${{ inputs.image-name-prefix }}-${{ inputs.image-name-suffix }}" }} - labels: | - org.label-schema.docker.cmd=docker run -d -p 8080:8080 {{ "${{ inputs.image-name-prefix }}-${{ inputs.image-name-suffix }}:${{ inputs.base-tag }}" }} - flavor: - latest=false - tags: | - type=ref,event=branch - type=sha - type=raw,value=latest,enable={{ "{{is_default_branch}}" }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@{{ cookiecutter.__gha_version_docker_qemu }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@{{ cookiecutter.__gha_version_docker_buildx }} - - - name: Login to Container Registry - uses: docker/login-action@{{ cookiecutter.__gha_version_docker_login }} - with: - {%- if cookiecutter.container_registry == 'docker_hub' %} - username: {{ "${{ secrets.DOCKERHUB_USERNAME }}" }} - password: {{ "${{ secrets.DOCKERHUB_TOKEN }}" }} - {%- endif %} - {%- if cookiecutter.container_registry == 'github' %} - registry: ghcr.io - username: {{ "${{ github.actor }}" }} - password: {{ "${{ secrets.GITHUB_TOKEN }}" }} - {%- endif %} - - - name: Build and push - uses: docker/build-push-action@{{ cookiecutter.__gha_version_docker_build_push }} - with: - platforms: linux/amd64 - context: backend/ - file: backend/Dockerfile - push: {{ "${{ github.event_name != 'pull_request' }}" }} - tags: {{ "${{ steps.meta.outputs.tags }}" }} - labels: {{ "${{ steps.meta.outputs.labels }}" }} - build-args: | - PLONE_VERSION={{ "${{ inputs.plone-version }}" }} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/docs.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/docs.yml deleted file mode 100644 index c26d14a5..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/docs.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: "Build documentation, check links, spelling, grammar, and style" - -on: - workflow_call: - inputs: - python-version: - required: true - type: string - -defaults: - run: - working-directory: docs - -jobs: - docs: - name: Documentation - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Install the latest version of uv - uses: astral-sh/setup-uv@{{ cookiecutter.__gha_version_setup_uv }} - with: - python-version: {{ "${{ inputs.python-version }}" }} - enable-cache: false - - - name: Restore uv cache - uses: actions/cache@{{ cookiecutter.__gha_version_cache }} - with: - path: {{ "${{ env.UV_CACHE_DIR }}" }} - key: uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }}" }} - restore-keys: | - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('uv.lock') }}" }} - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('pyproject.toml') }}" }} - uv-{{ "${{ runner.os }}-${{ inputs.python-version }}" }} - - - name: Install requirements - run: | - make install - - - name: Check for broken links - run: | - make linkcheckbroken - - - name: Build HTML documentation - run: | - make html - - - name: Run vale - run: | - make vale VALEOPTS="--minAlertLevel='warning'" diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/frontend.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/frontend.yml deleted file mode 100644 index 5083dff0..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/frontend.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Frontend CI - -on: - workflow_call: - inputs: - base-tag: - required: true - type: string - image-name-prefix: - required: true - type: string - image-name-suffix: - required: true - type: string - node-version: - required: true - type: string - volto-version: - required: true - type: string - -defaults: - run: - working-directory: ./frontend - -jobs: - code-analysis: - runs-on: ubuntu-latest - steps: - - name: Checkout codebase - uses: actions/checkout@v4 - - - name: Use Node.js {{ "${{ inputs.node-version }}" }} - uses: actions/setup-node@v4 - with: - node-version: {{ "${{ inputs.node-version }}" }} - - - name: Enable corepack - run: | - npm i -g corepack@latest - corepack enable - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - uses: actions/cache@v4 - name: Setup pnpm cache - with: - path: {{ "${{ env.STORE_PATH }}" }} - key: {{ "${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}" }} - restore-keys: | - {{ "${{ runner.os }}" }}-pnpm-store- - - - name: Install dependencies - run: make install - - - name: Linting - id: lint - if: {{ "${{ success() || failure() }}" }} - run: make lint - - - name: i18n sync - id: i18n - if: {{ "${{ success() || failure() }}" }} - run: make ci-i18n - - - name: Unit Tests - id: unit - if: {{ "${{ success() || failure() }}" }} - run: make test - - - name: Report - if: {{ "${{ success() || failure() }}" }} - run: | - echo '# Code Analysis' >> $GITHUB_STEP_SUMMARY - echo '| Test | Status |' >> $GITHUB_STEP_SUMMARY - echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY - echo '| Lint | {{ "${{ steps.lint.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - echo '| i18n | {{ "${{ steps.i18n.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - echo '| Unit Tests | {{ "${{ steps.unit.conclusion == 'failure' && '❌' || ' ✅' }}" }} |' >> $GITHUB_STEP_SUMMARY - - release: - runs-on: ubuntu-latest - needs: - - code-analysis - permissions: - contents: read - packages: write - - steps: - - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Docker meta - id: meta - uses: docker/metadata-action@{{ cookiecutter.__gha_version_docker_metadata }} - with: - images: | - {{ "${{ inputs.image-name-prefix }}-${{ inputs.image-name-suffix }}" }} - labels: | - org.label-schema.docker.cmd=docker run -d -p 8080:8080 {{ "${{ inputs.image-name-prefix }}-${{ inputs.image-name-suffix }}:${{ inputs.base-tag }}" }} - flavor: - latest=false - tags: | - type=ref,event=branch - type=sha - type=raw,value=latest,enable={{ "{{is_default_branch}}" }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@{{ cookiecutter.__gha_version_docker_qemu }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@{{ cookiecutter.__gha_version_docker_buildx }} - - - name: Login to Container Registry - uses: docker/login-action@{{ cookiecutter.__gha_version_docker_login }} - with: - {%- if cookiecutter.container_registry == 'docker_hub' %} - username: {{ "${{ secrets.DOCKERHUB_USERNAME }}" }} - password: {{ "${{ secrets.DOCKERHUB_TOKEN }}" }} - {%- endif %} - {%- if cookiecutter.container_registry == 'github' %} - registry: ghcr.io - username: {{ "${{ github.actor }}" }} - password: {{ "${{ secrets.GITHUB_TOKEN }}" }} - {%- endif %} - - - name: Build and push - uses: docker/build-push-action@{{ cookiecutter.__gha_version_docker_build_push }} - with: - platforms: linux/amd64 - context: frontend/ - file: frontend/Dockerfile - push: {{ "${{ github.event_name != 'pull_request' }}" }} - tags: {{ "${{ steps.meta.outputs.tags }}" }} - labels: {{ "${{ steps.meta.outputs.labels }}" }} - build-args: | - VOLTO_VERSION={{ "${{ inputs.volto-version }}" }} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/main.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/main.yml deleted file mode 100644 index 4ee5f406..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.github/workflows/main.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: {{ cookiecutter.title }} CI - -on: - push: - paths: - - "backend/**" - - "frontend/**" - - ".github/workflows/backend.yml" - {%- if cookiecutter.initialize_documentation == '1' %} - - "docs/**" - - ".github/workflows/docs.yml" - - .readthedocs.yaml - {%- endif %} - - ".github/workflows/frontend.yml" - - ".github/workflows/main.yml" - - "devops/**" - workflow_dispatch: - -env: - IMAGE_NAME_PREFIX: {{ cookiecutter.__container_image_prefix }} - NODE_VERSION: "{{ cookiecutter.__node_version }}.x" - PYTHON_VERSION: "{{ cookiecutter.__python_version }}" - -jobs: - config: - runs-on: ubuntu-latest - outputs: - backend: {{ "${{ steps.filter.outputs.backend }}" }} - {%- if cookiecutter.initialize_documentation == '1' %} - docs: {{ "${{ steps.filter.outputs.docs }}" }} - {%- endif %} - frontend: {{ "${{ steps.filter.outputs.frontend }}" }} - BASE_TAG: {{ "${{ steps.vars.outputs.BASE_TAG }}" }} - IMAGE_NAME_PREFIX: {{ "${{ env.IMAGE_NAME_PREFIX }}" }} - NODE_VERSION: {{ "${{ env.NODE_VERSION }}" }} - PYTHON_VERSION: {{ "${{ env.PYTHON_VERSION }}" }} - PLONE_VERSION: {{ "${{ steps.vars.outputs.PLONE_VERSION }}" }} - VOLTO_VERSION: {{ "${{ steps.vars.outputs.VOLTO_VERSION }}" }} - steps: - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Compute several vars needed for the CI - id: vars - run: | - echo "BASE_TAG=sha-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - echo "PLONE_VERSION=$(cat backend/version.txt)" >> $GITHUB_OUTPUT - python3 -c 'import json; data = json.load(open("./frontend/mrs.developer.json")); print("VOLTO_VERSION=" + (data["core"].get("tag") or "latest"))' >> $GITHUB_OUTPUT - - - uses: dorny/paths-filter@{{ cookiecutter.__gha_version_paths_filter }} - id: filter - with: - filters: | - backend: - - 'backend/**' - - '.github/workflows/backend.yml' - {%- if cookiecutter.initialize_documentation == '1' %} - docs: - - 'docs/**' - - .readthedocs.yaml - - '.github/workflows/docs.yml' - {%- endif %} - frontend: - - 'frontend/**' - - '.github/workflows/frontend.yml' - - - name: Test vars - run: | - echo 'BASE_TAG={{ "${{ steps.vars.outputs.BASE_TAG }}" }}' - echo 'PLONE_VERSION={{ "${{ steps.vars.outputs.PLONE_VERSION }}" }}' - echo 'VOLTO_VERSION={{ "${{ steps.vars.outputs.VOLTO_VERSION }}" }}' - echo 'backend: {{ "${{ steps.filter.outputs.backend }}" }}' - {%- if cookiecutter.initialize_documentation == '1' %} - echo 'docs: {{ "${{ steps.filter.outputs.docs }}" }}' - {%- endif %} - echo 'frontend: {{ "${{ steps.filter.outputs.frontend }}" }}' - - backend: - uses: ./.github/workflows/backend.yml - needs: - - config - with: - base-tag: {{ "${{ needs.config.outputs.BASE_TAG }}" }} - image-name-prefix: {{ "${{ needs.config.outputs.IMAGE_NAME_PREFIX }}" }} - image-name-suffix: backend - python-version: {{ "${{ needs.config.outputs.PYTHON_VERSION }}" }} - plone-version: {{ "${{ needs.config.outputs.PLONE_VERSION }}" }} - if: {{ "${{ needs.config.outputs.backend == 'true' }}" }} - permissions: - contents: read - packages: write - -{%- if cookiecutter.initialize_documentation == '1' %} - - docs: - uses: ./.github/workflows/docs.yml - needs: - - config - with: - python-version: {{ "${{ needs.config.outputs.PYTHON_VERSION }}" }} - if: {{ "${{ needs.config.outputs.docs == 'true' }}" }} -{%- endif %} - - frontend: - uses: ./.github/workflows/frontend.yml - needs: - - config - with: - base-tag: {{ "${{ needs.config.outputs.BASE_TAG }}" }} - image-name-prefix: {{ "${{ needs.config.outputs.IMAGE_NAME_PREFIX }}" }} - image-name-suffix: frontend - node-version: {{ "${{ needs.config.outputs.NODE_VERSION }}" }} - volto-version: {{ "${{ needs.config.outputs.VOLTO_VERSION }}" }} - if: {{ "${{ needs.config.outputs.frontend == 'true' }}" }} - permissions: - contents: read - packages: write - - report: - name: "Final report" - if: {{ "${{ always() }}" }} - runs-on: ubuntu-latest - needs: - - config - - backend - - frontend - {%- if cookiecutter.initialize_documentation == '1' %} - - docs - {%- endif %} - steps: - - name: Write report - run: | - {{ "echo '# Workflow Report' >> $GITHUB_STEP_SUMMARY" }} - {{ "echo '| Job ID | Conclusion |' >> $GITHUB_STEP_SUMMARY" }} - {{ "echo '| --- | --- |' >> $GITHUB_STEP_SUMMARY" }} - {{ "echo '| config | ${{ needs.config.result }} |' >> $GITHUB_STEP_SUMMARY" }} - {{ "echo '| backend | ${{ needs.backend.result }} |' >> $GITHUB_STEP_SUMMARY" }} - {%- if cookiecutter.initialize_documentation == '1' %} - {{ "echo '| docs | ${{ needs.docs.result }} |' >> $GITHUB_STEP_SUMMARY" }} - {%- endif %} - {{ "echo '| frontend | ${{ needs.frontend.result }} |' >> $GITHUB_STEP_SUMMARY" }} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/Makefile b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/Makefile index 0c1157a5..8f45a3f0 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/Makefile +++ b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/Makefile @@ -11,11 +11,13 @@ MAKEFLAGS+=--no-builtin-rules CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) GIT_FOLDER=$(CURRENT_DIR)/.git -PROJECT_NAME={{ cookiecutter.__project_slug }} +REPOSITORY_SETTINGS := $(shell uvx repoplone settings dump) + +PROJECT_NAME := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.name') STACK_NAME={{ cookiecutter.__devops_stack_name }} -VOLTO_VERSION=$(shell cat frontend/mrs.developer.json | python -c "import sys, json; print(json.load(sys.stdin)['core']['tag'])") -PLONE_VERSION=$(shell cat backend/version.txt) +VOLTO_VERSION := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.frontend.volto_version') +PLONE_VERSION := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.backend.base_package_version') # We like colors # From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects @@ -33,6 +35,13 @@ all: install help: ## This help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: debug-settings +debug-settings: ## Debug settings + @echo "Debug settings" + @echo "PROJECT_NAME: $(PROJECT_NAME)" + @echo "VOLTO_VERSION: $(VOLTO_VERSION)" + @echo "PLONE_VERSION: $(PLONE_VERSION)" + ########################################### # Frontend ########################################### @@ -193,12 +202,12 @@ acceptance-test: .PHONY: acceptance-frontend-image-build acceptance-frontend-image-build: @echo "Build acceptance frontend image" - @docker build frontend -t {{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}-frontend:acceptance -f frontend/Dockerfile --build-arg VOLTO_VERSION=$(VOLTO_VERSION) + @docker build frontend -t {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-frontend:acceptance -f frontend/Dockerfile --build-arg VOLTO_VERSION=$(VOLTO_VERSION) .PHONY: acceptance-backend-image-build acceptance-backend-image-build: @echo "Build acceptance backend image" - @docker build backend -t {{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}-backend:acceptance -f backend/Dockerfile.acceptance --build-arg PLONE_VERSION=$(PLONE_VERSION) + @docker build backend -t {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-backend:acceptance -f backend/Dockerfile.acceptance --build-arg PLONE_VERSION=$(PLONE_VERSION) .PHONY: acceptance-images-build acceptance-images-build: ## Build Acceptance frontend/backend images @@ -208,12 +217,12 @@ acceptance-images-build: ## Build Acceptance frontend/backend images .PHONY: acceptance-frontend-container-start acceptance-frontend-container-start: @echo "Start acceptance frontend" - @docker run --rm -p 3000:3000 --name {{ cookiecutter.__project_slug }}-frontend-acceptance --link {{ cookiecutter.__project_slug }}-backend-acceptance:backend -e RAZZLE_API_PATH=http://localhost:55001/plone -e RAZZLE_INTERNAL_API_PATH=http://backend:55001/plone -d {{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}-frontend:acceptance + @docker run --rm -p 3000:3000 --name {{ cookiecutter.__project_slug }}-frontend-acceptance --link {{ cookiecutter.__project_slug }}-backend-acceptance:backend -e RAZZLE_API_PATH=http://localhost:55001/plone -e RAZZLE_INTERNAL_API_PATH=http://backend:55001/plone -d {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-frontend:acceptance .PHONY: acceptance-backend-container-start acceptance-backend-container-start: @echo "Start acceptance backend" - @docker run --rm -p 55001:55001 --name {{ cookiecutter.__project_slug }}-backend-acceptance -d {{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}-backend:acceptance + @docker run --rm -p 55001:55001 --name {{ cookiecutter.__project_slug }}-backend-acceptance -d {{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}-backend:acceptance .PHONY: acceptance-containers-start acceptance-containers-start: ## Start Acceptance containers diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/README.md b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/README.md index ac180949..2163d509 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/README.md +++ b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/README.md @@ -2,8 +2,7 @@ [![Built with Cookieplone](https://img.shields.io/badge/built%20with-Cookieplone-0083be.svg?logo=cookiecutter)](https://github.com/plone/cookieplone-templates/) [![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -[![Backend Tests]({{ cookiecutter.__repository_url }}/actions/workflows/backend.yml/badge.svg)]({{ cookiecutter.__repository_url }}/actions/workflows/backend.yml) -[![Frontend Tests]({{ cookiecutter.__repository_url }}/actions/workflows/frontend.yml/badge.svg)]({{ cookiecutter.__repository_url }}/actions/workflows/frontend.yml) +[![CI]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml/badge.svg)]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml) {{ cookiecutter.description }} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/Makefile b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/Makefile deleted file mode 100644 index 9fb3e5e7..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/Makefile +++ /dev/null @@ -1,137 +0,0 @@ -### Defensive settings for make: -# https://tech.davis-hansson.com/p/make/ -SHELL:=bash -.ONESHELL: -.SHELLFLAGS:=-xeu -o pipefail -O inherit_errexit -c -.SILENT: -.DELETE_ON_ERROR: -MAKEFLAGS+=--warn-undefined-variables -MAKEFLAGS+=--no-builtin-rules - -include .env -export - -ifndef DEPLOY_ENV -$(error DEPLOY_ENV is not set) -endif - -ifndef DEPLOY_USER -$(error DEPLOY_USER is not set) -endif - -ifndef DEPLOY_SCALES - DEPLOY_SCALES=2 -endif - -ifeq ($(DEPLOY_ENV), dev) - CONTEXT=dev - CONTEXT_HOST=ssh://$(DEPLOY_USER)@$(DEPLOY_HOST):$(DEPLOY_PORT) -else ifeq ($(DEPLOY_ENV), prod) - CONTEXT=prod - CONTEXT_HOST=ssh://$(DEPLOY_USER)@$(DEPLOY_HOST):$(DEPLOY_PORT) -else -$(error DEPLOY_ENV is not supported) -endif - -DOCKER=docker -DOCKER_CMD_CONTEXT=${DOCKER} --context ${CONTEXT} - -# We like colors -# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects -RED=`tput setaf 1` -GREEN=`tput setaf 2` -RESET=`tput sgr0` -YELLOW=`tput setaf 3` - -# Add the following 'help' target to your Makefile -# And add help text after each target name starting with '\#\#' -.PHONY: help -help: ## This help message - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' - -.PHONY: clean -clean: ## remove all build, test, coverage and Python artifacts - @echo "$(GREEN)==> Clean up the installation$(RESET)" - rm -Rf bin lib lib64 include pyvenv.cfg .docker - -bin/pip: - @echo "$(GREEN)==> Python: Setup Virtual Env$(RESET)" - python3 -m venv . - bin/pip install -U pip pipx - -bin/ansible: - -.PHONY: setup -setup: bin/pip ## Ansible: Install Ansible and dependencies - @echo "$(GREEN)==> Ansible: Install Ansible and dependencies $(RESET)" - bin/pip install -r requirements/requirements.txt --upgrade - bin/ansible-galaxy install -r requirements/roles.yml --force --no-deps - bin/ansible-galaxy collection install -r requirements/collections.yml - -.PHONY: docker-setup -docker-setup: ## Docker: Setup remote docker context - @echo "$(GREEN)==> Docker: Setup remote docker context named ${CONTEXT} $(RESET)" - ${DOCKER} context create ${CONTEXT} --description "{{ cookiecutter.title }} deployment" --docker "host=${CONTEXT_HOST}" - -.PHONY: docker-login -docker-login: ## Docker: Login - @echo "$(GREEN)==> Docker: Login$(RESET)" - ${DOCKER} login - -.PHONY: docker-info -docker-info: ## Docker: Information about remote docker context - @echo "$(GREEN)==> Docker: Information about remote docker context $(RESET)" - ${DOCKER_CMD_CONTEXT} info - -.PHONY: server-setup -server-setup: bin/ansible ## Server: Configure a remote server - @echo "$(GREEN)==> Server: Configure a remote server with Ansible$(RESET)" - ./bin/ansible-playbook playbooks/setup.yml --limit ${CONTEXT} - -.PHONY: stack-deploy -stack-deploy: ## Deploy stacks: $(STACK_NAME) - @echo "$(GREEN)==> Stack $(STACK_NAME): Deploy to context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} stack deploy --with-registry-auth -c stacks/{{ cookiecutter.hostname }}.yml $(STACK_NAME) - -.PHONY: stack-create-site -stack-create-site: ## Create a new site stacks: $(STACK_NAME) - @echo "$(GREEN)==> Stack $(STACK_NAME): Create site in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} exec `${DOCKER_CMD_CONTEXT} ps -qf 'name=$(STACK_NAME)_backend'|head -n1` ./docker-entrypoint.sh create-site - -.PHONY: stack-status -stack-status: ## Check status of stack - @echo "$(GREEN)==> Stack $(STACK_NAME): Status in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} stack ps -f "desired-state=running" $(STACK_NAME) - -.PHONY: stack-logs-webserver -stack-logs-webserver: ## Display webserver logs - @echo "$(GREEN)==> Stack $(STACK_NAME): Logs for webserver in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} service logs $(STACK_NAME)_traefik - -.PHONY: stack-logs-frontend -stack-logs-frontend: ## Display frontend logs - @echo "$(GREEN)==> Stack $(STACK_NAME): Logs for frontend in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} service logs $(STACK_NAME)_frontend - -.PHONY: stack-logs-backend -stack-logs-backend: ## Display backend logs - @echo "$(GREEN)==> Stack $(STACK_NAME): Logs for backend in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} service logs $(STACK_NAME)_backend - -{%- if cookiecutter.devops_storage == 'relstorage' %} - -.PHONY: stack-logs-db -stack-logs-db: ## Display db logs - @echo "$(GREEN)==> Stack $(STACK_NAME): Logs for db in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} service logs $(STACK_NAME)_db -{%- endif %} - -.PHONY: stack-scale-services -stack-scale-services: ## Scale services - @echo "$(GREEN)==> Stack $(STACK_NAME): Scaling Frontend and Backend to $(DEPLOY_SCALES) in context $(DEPLOY_ENV) $(RESET)" - ${DOCKER_CMD_CONTEXT} service scale $(STACK_NAME)_frontend=$(DEPLOY_SCALES) $(STACK_NAME)_backend=$(DEPLOY_SCALES) - -.PHONY: all -all: ## Create new box, run ansible, deploy stack - $(MAKE) run-bootstrap - $(MAKE) deploy diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GHA.md b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GHA.md index 14e88b45..5d59a38b 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GHA.md +++ b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GHA.md @@ -9,7 +9,7 @@ Welcome to the DevOps operations guide for deploying your project using GitHub A ### Step 1: Create a New Environment 1. Visit [GitHub](https://github.com/) and log in with your credentials. -2. Go to your repository at [{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}]({{ cookiecutter.__repository_url }}). +2. Go to your repository at [{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}]({{ cookiecutter.__repository_url }}). 3. Click on `Settings` at the top-right corner. 4. In the left sidebar, click on `Environments`. 5. Press `New environment`. @@ -46,12 +46,11 @@ This variable is referenced in the `.github/workflows/manual_deploy.yml` file. ## Manual Deployment 🚀 -Ensure that both Backend and Frontend tests have been successfully executed: +Ensure that the CI workflow have been successfully executed: -- [Backend Tests Workflow]({{ cookiecutter.__repository_url }}/actions/workflows/backend.yml) -- [Frontend Tests Workflow]({{ cookiecutter.__repository_url }}/actions/workflows/frontend.yml) +- [CI Workflow]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml) -Upon successful completion of the tests, Docker images for the Backend (`{{ cookiecutter.__container_image_prefix }}-backend`) and Frontend (`{{ cookiecutter.__container_image_prefix }}-frontend`) will be available. +Upon successful completion of the tests, Docker images for the Backend (`{{ cookiecutter.__container_image_prefix }}-backend`) and Frontend (`{{ cookiecutter.__container_image_prefix }}-frontend`) should be available. ### Initiating Manual Deployment diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GITLAB.md b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GITLAB.md index 570ffe45..f78a6de0 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GITLAB.md +++ b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/README-GITLAB.md @@ -11,7 +11,7 @@ See [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/). ### Step 1: Add the deployment environment variables 1. Visit [GitLab](https://gitlab.com/), or your organization's GitLab instance, and log in with your credentials. -2. Go to your repository at [{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}](https://gitlab.com/{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}). +2. Go to your repository at [{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}](https://gitlab.com/{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}). Note that you might need to adjust the URL's host name to your organization's. 3. Click on `Settings` at the left menu and select `CI/CD`. 4. Expand the `Variables`. diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/docker/daemon/daemon.json.j2 b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/docker/daemon/daemon.json.j2 deleted file mode 100644 index 3c6a42c6..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/etc/docker/daemon/daemon.json.j2 +++ /dev/null @@ -1,16 +0,0 @@ -{ - "data-root": "{{ docker.storage.root }}", - {%- if docker.network.custom %} - "bip": "{{ docker.network.bip }}", - "default-address-pools": [ - { "base": "{{ docker.network.default_address_pool.base }}", "size": {{ docker.network.default_address_pool.size }} } - ], - "mtu": {{ docker.network.mtu }}, - {%- endif %} - "log-driver": "{{ docker.logs.driver | default('json-file') }}", - "log-opts": { - "labels": "{{ docker.log.opt.labels | default('com.docker.swarm.service.name') }}", - "max-size": "{{ docker.log.opt.max_size | default('10m') }}", - "max-file": "{{ docker.log.opt.max_file | default('5') }}" - } -} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/docker.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/docker.yml deleted file mode 100644 index 64894c8d..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/docker.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -docker: - storage: - root: "/var/lib/docker" - network: - custom: false - mtu: 1450 - bip: "10.0.1.1/24" - default_address_pool: - base: "172.17.0.0/12" - size: 20 - log: - driver: "json-file" - opt: - max_size: "5m" - max_file: "3" diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/packages.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/packages.yml deleted file mode 100644 index db85ee4c..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/packages.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -base_packages: - - apt-transport-https - - ca-certificates - - acl - - aptitude - - sudo - - curl - - python3-apt - - python3-minimal - - python3-pip diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/projects.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/projects.yml deleted file mode 100644 index 1c56f576..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/projects.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -project_folders: - - path: "/srv/{{ cookiecutter.__project_slug }}/data" - owner: 999 - group: 999 - mode: "0750" diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/sshd.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/sshd.yml deleted file mode 100644 index be9e3510..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/sshd.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -sshd: - port: 22 diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/users.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/users.yml deleted file mode 100644 index 43026ebf..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/group_vars/all/users.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -users: - default: - name: "{{ lookup('ansible.builtin.env', 'DEPLOY_USER', default='plone') }}" - group: sudo - additional_keys: [] diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/hosts.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/hosts.yml deleted file mode 100644 index 0c07f2ce..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/inventory/hosts.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -prod: - hosts: - {{ cookiecutter.hostname }}: - ansible_user: root - host: {{ cookiecutter.__devops_host }} - hostname: {{ cookiecutter.hostname }} diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/playbooks/setup.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/playbooks/setup.yml deleted file mode 100644 index 9a0b4831..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/playbooks/setup.yml +++ /dev/null @@ -1,138 +0,0 @@ ---- -# Playbook: playbooks/setup.yml -# Description: Server setup -# -# Usage: -# -# # Group of servers -# ./bin/ansible-playbook playbooks/setup.yml -# -- hosts: "all" - gather_facts: true - become: true - pre_tasks: - - - name: "Base Packages: Setup" - ansible.builtin.import_tasks: ../tasks/base/task_base_packages.yml - - - name: "Base Python: Setup" - ansible.builtin.import_tasks: ../tasks/base/task_base_python.yml - - - roles: - - role: geerlingguy.swap - - tasks: - - - name: "Hostname" - ansible.builtin.import_tasks: ../tasks/base/task_hostname.yml - tags: - - base - - hostname - - - name: "User {{ users.default.name }}: Setup" - tags: - - base - - users - block: - - name: "Add user {{ users.default.name }}" - ansible.builtin.user: - name: "{{ users.default.name }}" - shell: /bin/bash - generate_ssh_key: true - ssh_key_file: .ssh/id_ed25519 - ssh_key_type: "ed25519" - - - name: "Allow user {{ users.default.name }} to sudo without password" - ansible.builtin.copy: - content: "{{ users.default.name }} ALL=(ALL) NOPASSWD: ALL" - dest: /etc/sudoers.d/{{ users.default.name }} - mode: 0640 - - - name: "SSH: Setup" - tags: - - base - - ssh - block: - - name: "SSH: Generate a deployment key" - community.crypto.openssh_keypair: - path: ../etc/keys/{{ users.default.name }}_prod_deploy_ed25519 - type: "ed25519" - delegate_to: localhost - become: false - - - name: "SSH: Read authorized_keys from root" - ansible.builtin.command: - cmd: cat /root/.ssh/authorized_keys - become_user: "root" - become: true - register: root_keys - changed_when: false - - - name: "SSH: Set root keys for the new user" - ansible.builtin.authorized_key: - user: "{{ users.default.name }}" - key: "{{ item }}" - loop: "{{ root_keys.stdout_lines }}" - become_user: "{{ users.default.name }}" - become: true - - - name: "SSH: Additional keys" - ansible.builtin.authorized_key: - user: "{{ users.default.name }}" - key: "{{ lookup('file', item) }}" - with_fileglob: - - ../etc/keys/*.pub - become_user: "{{ users.default.name }}" - become: true - - - name: "SSH: Disallow password authentication" - ansible.builtin.lineinfile: - path: /etc/ssh/sshd_config - regexp: "^PasswordAuthentication" - line: "PasswordAuthentication no" - state: present - - - name: "SSH: Set AllowUsers" - ansible.builtin.lineinfile: - path: /etc/ssh/sshd_config - insertafter: "^# Authentication" - line: "AllowUsers {{ users.default.name }} root" - notify: restart ssh - - - name: "SSH: Listen to port {{ sshd.port }}" - ansible.builtin.lineinfile: - path: /etc/ssh/sshd_config - insertafter: "^#Port 22" - line: "Port {{ sshd.port }}" - notify: restart ssh - - - name: "Docker: Setup" - ansible.builtin.import_tasks: ../tasks/base/task_docker.yml - tags: - - base - - docker - - - name: "Docker Swarm: Setup" - ansible.builtin.import_tasks: ../tasks/swarm/task_swarm.yml - tags: - - base - - swarm - - - name: "Project Structure" - tags: - - base - - project - block: - - name: "Project: Create folder structure" - ansible.builtin.file: - dest: "{{ item.path }}" - state: directory - owner: "{{ item.owner }}" - group: "{{ item.group }}" - mode: "{{ item.mode }}" - with_items: "{{ project_folders }}" - - handlers: - - name: "Include default handlers" - ansible.builtin.import_tasks: ../tasks/handlers/common.yml diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/collections.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/collections.yml deleted file mode 100644 index 0d11afd2..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/collections.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -collections: - - name: community.crypto - - name: community.docker - - name: community.general diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/requirements.txt b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/requirements.txt deleted file mode 100644 index 59a28070..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -ansible -ansible-lint -lxml -passlib -python-gitlab -requests -yamllint -dnspython diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/roles.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/roles.yml deleted file mode 100644 index c1c699c0..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/requirements/roles.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -- name: geerlingguy.swap diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml index 604003ca..c58e9b83 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml +++ b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/stacks/{{ cookiecutter.hostname }}.yml @@ -1,96 +1,18 @@ --- -version: '3.8' +version: '3.9' services: - traefik: - image: traefik:{{ cookiecutter.__devops_traefik_version }} - - ports: - - 80:80 - - 443:443 - - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 5s - order: start-first - labels: - - traefik.enable=true - - traefik.constraint-label=public - - traefik.http.services.traefik-public.loadbalancer.server.port=8000 -{%- if cookiecutter.__devops_traefik_stack_include_ui == 'yes' %} - ## Basic Authentication - ### Note: all dollar signs in the hash need to be doubled for escaping. - ### To create user:password pair, it's possible to use this command: - ### echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g - ### Defaults to admin:admin - - traefik.http.middlewares.admin-auth.basicauth.users=admin:$$apr1$$uZPT5Fgu$$AmlIdamxT5ipBvPlsdfD70 - - traefik.http.routers.traefik-public-https.rule=Host(`traefik-{{ cookiecutter.hostname }}`) - - traefik.http.routers.traefik-public-https.entrypoints=https - - traefik.http.routers.traefik-public-https.tls=true - - traefik.http.routers.traefik-public-https.service=api@internal - - traefik.http.routers.traefik-public-https.middlewares=admin-auth -{%- else %} - ## Basic Authentication - ### Note: all dollar signs in the hash need to be doubled for escaping. - ### To create user:password pair, it's possible to use this command: - ### echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g - ### Defaults to admin:admin - #- traefik.http.middlewares.admin-auth.basicauth.users=admin:$$apr1$$uZPT5Fgu$$AmlIdamxT5ipBvPlsdfD70 - #- traefik.http.routers.traefik-public-https.rule=Host(`traefik-{{ cookiecutter.hostname }}`) - #- traefik.http.routers.traefik-public-https.entrypoints=https - #- traefik.http.routers.traefik-public-https.tls=true - #- traefik.http.routers.traefik-public-https.service=api@internal - #- traefik.http.routers.traefik-public-https.middlewares=admin-auth -{%- endif %} - - # GENERIC MIDDLEWARES - - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https - - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true - - traefik.http.middlewares.gzip.compress=true - - traefik.http.middlewares.gzip.compress.excludedcontenttypes=image/png, image/jpeg, font/woff2 - - # GENERIC ROUTERS - - traefik.http.routers.generic-https-redirect.entrypoints=http - - traefik.http.routers.generic-https-redirect.rule=HostRegexp(`{host:.*}`) - - traefik.http.routers.generic-https-redirect.priority=1 - - traefik.http.routers.generic-https-redirect.middlewares=https-redirect - - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - vol-traefik-certs:/certificates - - command: - - --providers.docker - - --providers.docker.constraints=Label(`traefik.constraint-label`, `public`) - - --providers.docker.exposedbydefault=false - - --providers.docker.swarmmode - - --providers.docker.network={{ cookiecutter.__devops_traefik_docker_network }} - - --entrypoints.http.address=:80 - - --entrypoints.https.address=:443 - - --certificatesresolvers.le.acme.email={{ cookiecutter.email }} - - --certificatesresolvers.le.acme.storage=/certificates/acme.json - - --certificatesresolvers.le.acme.tlschallenge=true - - --accesslog - - --log - - --log.level=INFO - - --api - - networks: - - {{ cookiecutter.__devops_swarm_public_network }} - - {{ cookiecutter.__devops_swarm_stack_network }} {%- if cookiecutter.devops_cache == '1' %} purger: image: ghcr.io/kitconcept/cluster-purger:latest environment: - PURGER_SERVICE_NAME: varnish + PURGER_SERVICE_NAME: {{ cookiecutter.__devops_stack_name}}_varnish PURGER_SERVICE_PORT: 80 - PURGER_PUBLIC_SITES: "['{{ cookiecutter.hostname }}']" + PURGER_PUBLIC_SITES: "['${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}']" networks: - - {{ cookiecutter.__devops_swarm_stack_network }} + - {{ cookiecutter.__devops_swarm_public_network }} deploy: replicas: 2 update_config: @@ -99,7 +21,7 @@ services: order: start-first varnish: - image: {{ cookiecutter.__container_image_prefix }}-varnish:${STACK_PARAM:-latest} + image: {{ cookiecutter.__container_image_prefix }}-varnish:${IMAGE_TAG:-latest} command: - '-p' - 'nuke_limit=2000' @@ -107,12 +29,8 @@ services: - 'workspace_client=192k' - '-p' - 'workspace_backend=192k' - depends_on: - - traefik - - frontend - - backend networks: - - {{ cookiecutter.__devops_swarm_stack_network }} + - {{ cookiecutter.__devops_swarm_public_network }} deploy: replicas: 1 update_config: @@ -123,27 +41,24 @@ services: - traefik.enable=true - traefik.constraint-label=public # Services - - traefik.http.services.svc-varnish.loadbalancer.server.port=80 + - traefik.http.services.svc-{{ cookiecutter.__devops_stack_prefix }}-varnish.loadbalancer.server.port=80 # Routers ## Router: Varnish Public - - traefik.http.routers.rt-varnish-public.rule=Host(`{{ cookiecutter.hostname }}`) - - traefik.http.routers.rt-varnish-public.entrypoints=https - - traefik.http.routers.rt-varnish-public.tls=true - - traefik.http.routers.rt-varnish-public.tls.certresolver=le - - traefik.http.routers.rt-varnish-public.service=svc-varnish - - traefik.http.routers.rt-varnish-public.middlewares=gzip + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-varnish-public.rule=Host(`${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}`) + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-varnish-public.entrypoints=https + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-varnish-public.tls=true + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-varnish-public.tls.certresolver=le + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-varnish-public.service=svc-{{ cookiecutter.__devops_stack_prefix }}-varnish + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-varnish-public.middlewares=gzip {%- endif %} frontend: - image: {{ cookiecutter.__container_image_prefix }}-frontend:${STACK_PARAM:-latest} + image: {{ cookiecutter.__container_image_prefix }}-frontend:${IMAGE_TAG:-latest} environment: - RAZZLE_INTERNAL_API_PATH: http://backend:8080/Plone - RAZZLE_API_PATH: https://{{ cookiecutter.hostname }} - depends_on: - - backend + RAZZLE_INTERNAL_API_PATH: http://{{ cookiecutter.__devops_stack_name}}_backend:8080/Plone + RAZZLE_API_PATH: https://${STACK_HOSTNAME:-{{ cookiecutter.hostname }}} networks: - {{ cookiecutter.__devops_swarm_public_network }} - - {{ cookiecutter.__devops_swarm_stack_network }} deploy: replicas: 2 update_config: @@ -154,35 +69,31 @@ services: - traefik.enable=true - traefik.constraint-label=public # Service - - traefik.http.services.svc-frontend.loadbalancer.server.port=3000 + - traefik.http.services.svc-{{ cookiecutter.__devops_stack_prefix }}-frontend.loadbalancer.server.port=3000 # Routers {%- if cookiecutter.devops_cache == '0' %} ## / - - traefik.http.routers.rt-frontend.rule=Host(`{{ cookiecutter.hostname }}`) - - traefik.http.routers.rt-frontend.entrypoints=https - - traefik.http.routers.rt-frontend.tls=true - - traefik.http.routers.rt-frontend.tls.certresolver=le - - traefik.http.routers.rt-frontend.service=svc-frontend - - traefik.http.routers.rt-frontend.middlewares=gzip + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.rule=Host(`${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}`) + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.entrypoints=https + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.tls=true + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.tls.certresolver=le + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.service=svc-{{ cookiecutter.__devops_stack_prefix }}-frontend + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.middlewares=gzip {%- else %} ## / (Internal) - - traefik.http.routers.rt-frontend.rule=Host(`{{ cookiecutter.hostname }}`) && Headers(`X-Varnish-Routed`, `1`) - - traefik.http.routers.rt-frontend.entrypoints=http - - traefik.http.routers.rt-frontend.service=svc-frontend + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.rule=Host(`${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}`) && Headers(`X-Varnish-Routed`, `1`) + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.entrypoints=http + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-frontend.service=svc-{{ cookiecutter.__devops_stack_prefix }}-frontend {%- endif %} backend: - image: {{ cookiecutter.__container_image_prefix }}-backend:${STACK_PARAM:-latest} + image: {{ cookiecutter.__container_image_prefix }}-backend:${IMAGE_TAG:-latest} {%- if cookiecutter.devops_storage == 'relstorage' %} environment: - RELSTORAGE_DSN: "dbname='${DB_NAME:-plone}' user='${DB_USER:-plone}' host='${DB_HOST:-db}' password='${DB_PASSWORD:-{{ cookiecutter.__devops_db_password }}}' port='${DB_PORT:-5432}'" - depends_on: - - db + RELSTORAGE_DSN: "dbname='${DB_NAME:-plone}' user='${DB_USER:-plone}' host='${DB_HOST:-{{ cookiecutter.__devops_stack_name}}_db}' password='${DB_PASSWORD:-{{ cookiecutter.__devops_db_password }}}' port='${DB_PORT:-5432}'" {%- elif cookiecutter.devops_storage == 'zeo' %} environment: - ZEO_ADDRESS: "zeo:8100" - depends_on: - - zeo + ZEO_ADDRESS: "{ cookiecutter.__devops_stack_name}}_db:8100" {%- else %} volumes: - vol-site-data:/data @@ -204,85 +115,73 @@ services: - traefik.enable=true - traefik.constraint-label=public # Services - - traefik.http.services.svc-backend.loadbalancer.server.port=8080 + - traefik.http.services.svc-{{ cookiecutter.__devops_stack_prefix }}-backend.loadbalancer.server.port=8080 # Middlewares ## VHM rewrite /++api++/ - - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" - - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/https/{{ cookiecutter.hostname }}/Plone/++api++/VirtualHostRoot$$1" + - "traefik.http.middlewares.mw-{{ cookiecutter.__devops_stack_prefix }}-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" + - "traefik.http.middlewares.mw-{{ cookiecutter.__devops_stack_prefix }}-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}/Plone/++api++/VirtualHostRoot$$1" ## VHM rewrite /ClassicUI/ - - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" - - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/https/{{ cookiecutter.hostname }}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + - "traefik.http.middlewares.mw-{{ cookiecutter.__devops_stack_prefix }}-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" + - "traefik.http.middlewares.mw-{{ cookiecutter.__devops_stack_prefix }}-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/https/${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" ## Basic Authentication ### Note: all dollar signs in the hash need to be doubled for escaping. ### To create user:password pair, it's possible to use this command: ### echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g ### Defaults to admin:admin - - traefik.http.middlewares.mw-backend-auth.basicauth.headerField=X-Auth-ClassicUI - - traefik.http.middlewares.mw-backend-auth.basicauth.users=admin:$$apr1$$uZPT5Fgu$$AmlIdamxT5ipBvPlsdfD70 + - traefik.http.middlewares.mw-{{ cookiecutter.__devops_stack_prefix }}-backend-auth.basicauth.headerField=X-Auth-ClassicUI + - traefik.http.middlewares.mw-{{ cookiecutter.__devops_stack_prefix }}-backend-auth.basicauth.users=admin:$$apr1$$uZPT5Fgu$$AmlIdamxT5ipBvPlsdfD70 # Routes {%- if cookiecutter.devops_cache == '0' %} ## /++api++ - - traefik.http.routers.rt-backend-api.rule=Host(`{{ cookiecutter.hostname }}`) && PathPrefix(`/++api++`) - - traefik.http.routers.rt-backend-api.entrypoints=https - - traefik.http.routers.rt-backend-api.tls=true - - traefik.http.routers.rt-backend-api.service=svc-backend - - traefik.http.routers.rt-backend-api.middlewares=gzip,mw-backend-vhm-api + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.rule=Host(`${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.entrypoints=https + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.tls=true + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.service=svc-{{ cookiecutter.__devops_stack_prefix }}-backend + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.middlewares=gzip,mw-{{ cookiecutter.__devops_stack_prefix }}-backend-vhm-api {%- else %} ## /++api++ (Internal) - - traefik.http.routers.rt-backend-api.rule=Host(`{{ cookiecutter.hostname }}`) && PathPrefix(`/++api++`) && Headers(`X-Varnish-Routed`, `1`) - - traefik.http.routers.rt-backend-api.entrypoints=http - - traefik.http.routers.rt-backend-api.service=svc-backend - - traefik.http.routers.rt-backend-api.middlewares=mw-backend-vhm-api + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.rule=Host(`${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}`) && PathPrefix(`/++api++`) && Headers(`X-Varnish-Routed`, `1`) + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.entrypoints=http + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.service=svc-{{ cookiecutter.__devops_stack_prefix }}-backend + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-api.middlewares=mw-{{ cookiecutter.__devops_stack_prefix }}-backend-vhm-api {%- endif %} ## /ClassicUI - - traefik.http.routers.rt-backend-classic.rule=Host(`{{ cookiecutter.hostname }}`) && PathPrefix(`/ClassicUI`) - - traefik.http.routers.rt-backend-classic.entrypoints=https - - traefik.http.routers.rt-backend-classic.tls=true - - traefik.http.routers.rt-backend-classic.service=svc-backend - - traefik.http.routers.rt-backend-classic.middlewares=gzip,mw-backend-auth,mw-backend-vhm-classic + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-classic.rule=Host(`${STACK_HOSTNAME:-{{ cookiecutter.hostname }}}`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-classic.entrypoints=https + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-classic.tls=true + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-classic.service=svc-{{ cookiecutter.__devops_stack_prefix }}-backend + - traefik.http.routers.rt-{{ cookiecutter.__devops_stack_prefix }}-backend-classic.middlewares=gzip,mw-{{ cookiecutter.__devops_stack_prefix }}-backend-auth,mw-backend-vhm-classic -{%- if cookiecutter.devops_storage == 'relstorage' %} +{%- if cookiecutter.devops_storage in ('relstorage', 'zeo') %} db: - image: postgres:{{ cookiecutter.__devops_db_version }} +{%- if cookiecutter.devops_storage == 'relstorage' %} + image: postgres:{{ cookiecutter.__devops_postgres_version }} environment: POSTGRES_USER: plone POSTGRES_PASSWORD: {{ cookiecutter.__devops_db_password }} POSTGRES_DB: plone - deploy: - replicas: 1 - update_config: - parallelism: 1 - delay: 1s - order: stop-first volumes: - vol-site-data:/var/lib/postgresql/data - networks: - - {{ cookiecutter.__devops_swarm_stack_network }} {%- elif cookiecutter.devops_storage == 'zeo' %} - - zeo: image: plone/plone-zeo:{{ cookiecutter.__devops_zeo_version }} + volumes: + - vol-site-data:/data +{%- endif %} deploy: replicas: 1 update_config: parallelism: 1 delay: 1s order: stop-first - volumes: - - vol-site-data:/data networks: - {{ cookiecutter.__devops_swarm_stack_network }} {%- endif %} volumes: vol-traefik-certs: {} - vol-site-data: - driver_opts: - type: none - device: "/srv/{{ cookiecutter.__project_slug }}/data" - o: bind + vol-site-data: {} networks: {{ cookiecutter.__devops_swarm_public_network }}: diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_base_packages.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_base_packages.yml deleted file mode 100644 index 743aa582..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_base_packages.yml +++ /dev/null @@ -1,10 +0,0 @@ ---- -# Base packages -# -- name: "Base: Install Base Packages" - ansible.builtin.apt: - name: "{{ base_packages }}" - update_cache: true - state: present - tags: - - base diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_base_python.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_base_python.yml deleted file mode 100644 index 33bc4e4b..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_base_python.yml +++ /dev/null @@ -1,14 +0,0 @@ ---- -# Allow installing packages into system python -# -- name: "Base: Allow installing packages into system Python" - ansible.builtin.file: - name: "/usr/lib/python{{ item }}/EXTERNALLY-MANAGED" - state: absent - with_items: - - "3.10" - - "3.11" - - "3.12" - - "3.13" - tags: - - base diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_docker.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_docker.yml deleted file mode 100644 index ee770224..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/base/task_docker.yml +++ /dev/null @@ -1,140 +0,0 @@ ---- -# Setup Docker -# -- name: "Docker: Ensure dependencies are installed" - ansible.builtin.apt: - name: - - apt-transport-https - - ca-certificates - state: present - tags: - - docker - -- name: "Docker: Install support Python Packages" - ansible.builtin.pip: - name: - - docker - - jsondiff - state: present - tags: - - docker - -- name: "Docker: Create /etc/docker/" - ansible.builtin.file: - dest: "/etc/docker/" - state: directory - owner: root - group: root - mode: 0755 - -- name: "Docker: Create /etc/docker/daemon.json" - ansible.builtin.template: - src: ../../etc/docker/daemon/daemon.json.j2 - dest: "/etc/docker/daemon.json" - owner: root - group: root - mode: 0755 - notify: - - systemd daemon reload - - restart docker - -- name: "Docker: Setup Repository" - block: - - name: "Docker: Add GnuPG key" - ansible.builtin.apt_key: - url: https://download.docker.com/linux/{{ ansible_lsb.id | lower }}/gpg - state: present - - - name: "Docker: Add Repository (amd64)" - ansible.builtin.apt_repository: - repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_lsb.id | lower }} {{ ansible_lsb.codename }} stable" - state: present - filename: 'docker' - - - name: "Docker: Add Repository (arm64)" - ansible.builtin.apt_repository: - repo: "deb [arch=arm64] https://download.docker.com/linux/{{ ansible_lsb.id | lower }} {{ ansible_lsb.codename }} stable" - state: present - filename: 'docker' - tags: - - docker - -- name: "Docker: Setup Packages" - block: - - name: "Docker: Remove old packages" - ansible.builtin.apt: - name: - - docker - - docker-engine - state: absent - - - name: "Docker: Install packages" - ansible.builtin.apt: - name: - - docker-ce - - docker-ce-cli - - containerd.io - - docker-compose - state: present - update_cache: true - tags: - - docker - -- name: "Docker: Setup Service" - block: - - name: "Docker: Service is started and enabled at boot" - ansible.builtin.service: - name: docker - state: started - enabled: true - tags: - - docker - -- name: "Docker: Add {{ users.default.name }} user to docker group" - ansible.builtin.user: - name: "{{ users.default.name }}" - group: docker - state: present - tags: - - docker - -- name: "Docker: Setup docker_gwbridge" - community.docker.docker_network: - name: "docker_gwbridge" - driver: bridge - driver_options: - "com.docker.network.bridge.enable_icc": false - "com.docker.network.bridge.enable_ip_masquerade": true - "com.docker.network.bridge.name": "docker_gwbridge" - "com.docker.network.driver.mtu": "{{ docker.network.mtu }}" - ipam_config: - - subnet: 172.30.0.1/16 - when: docker.network.custom - tags: - - docker - - docker_gwbridge - -- name: "Docker: Setup Proxy" - block: - - name: "Docker: Create /etc/systemd/system/docker.service.d/ folder" - ansible.builtin.file: - dest: "/etc/systemd/system/docker.service.d" - state: directory - owner: root - group: root - mode: 0755 - - - name: "Docker: Create /etc/systemd/system/docker.service.d/http-proxy.conf" - ansible.builtin.template: - src: ../../etc/docker/systemd/http-proxy.conf.j2 - dest: "/etc/systemd/system/docker.service.d/http-proxy.conf" - owner: root - group: root - mode: 0755 - notify: - - systemd daemon reload - - restart docker - - when: proxy is defined and proxy.https_proxy is defined - tags: - - docker diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/handlers/common.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/handlers/common.yml deleted file mode 100644 index 6303fc23..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/handlers/common.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -- name: systemd daemon reload - ansible.builtin.systemd: - daemon_reload: true - become_user: root - become: true - -- name: restart ufw - ansible.builtin.service: - name: ufw - state: restarted - become: true - become_user: root - -- name: restart ssh - ansible.builtin.service: - name: ssh - state: restarted - become: true - become_user: root - -- name: restart docker - ansible.builtin.service: - name: docker - state: restarted - become_user: root - become: true diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/swarm/task_swarm.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/swarm/task_swarm.yml deleted file mode 100644 index 4d14f85a..00000000 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/devops/tasks/swarm/task_swarm.yml +++ /dev/null @@ -1,59 +0,0 @@ -# code: language=ansible ---- -# Docker Swarm - -- name: "Docker Swarm: Initial Setup" - block: - - name: "Docker Swarm: Init Swarm" - community.docker.docker_swarm: - state: present - advertise_addr: "{{ swarm.advertise_addr }}" - register: swarm_info - -- name: "Docker Swarm: Set public network" - become: true - become_user: "{{ users.default.name }}" - block: - - name: "Docker Swarm: Check for public network" - community.docker.docker_network_info: - name: "{{ swarm.public_network }}" - register: docker_swarm_net_public - - - name: "Docker Swarm: Create public network" - community.docker.docker_network: - name: "{{ swarm.public_network }}" - internal: false - attachable: true - driver: overlay - driver_options: - "com.docker.network.driver.mtu": "{{ docker.network.mtu }}" - when: not docker_swarm_net_public.exists - -- name: "Docker Swarm: Purge data" - become: true - become_user: "{{ users.default.name }}" - tags: - - cron - - swarm - block: - - name: "Docker Swarm: Purge old containers" - ansible.builtin.cron: - name: "Purge old containers" - job: "/usr/bin/docker container prune -f" - hour: "*" - minute: 5 - state: present - - name: "Docker Swarm: Purge old images" - ansible.builtin.cron: - name: "Purge old images" - job: "/usr/bin/docker image prune -f" - hour: "*" - minute: 10 - state: present - - name: "Docker Swarm: Purge old volumes" - ansible.builtin.cron: - name: "Purge old volumes" - job: "/usr/bin/docker volume prune -f" - hour: "*" - minute: 15 - state: present diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml index 297c4bb1..086fa4b3 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml +++ b/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/docker-compose.yml @@ -12,7 +12,7 @@ services: - traefik.enable=true - traefik.constraint-label=public {%- if cookiecutter.__devops_traefik_local_include_ui == 'yes' %} - - traefik.http.routers.traefik-public-http.rule=Host(`traefik.{{ cookiecutter.__project_slug }}.localhost`) + - traefik.http.routers.traefik-public-http.rule=Host(`traefik.${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) - traefik.http.routers.traefik-public-http.entrypoints=http - traefik.http.routers.traefik-public-http.service=api@internal - traefik.http.services.traefik-public.loadbalancer.server.port=8000 @@ -37,11 +37,12 @@ services: {%- if cookiecutter.devops_cache == '1' %} purger: image: ghcr.io/kitconcept/cluster-purger:latest + platform: linux/amd64 environment: PURGER_SERVICE_NAME: varnish PURGER_SERVICE_PORT: 80 PURGER_MODE: "compose" - PURGER_PUBLIC_SITES: "['{{ cookiecutter.__project_slug }}.localhost']" + PURGER_PUBLIC_SITES: "['${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}']" varnish: build: @@ -55,6 +56,7 @@ services: - backend {%- endif %} + frontend: build: context: ./frontend @@ -72,19 +74,19 @@ services: # Routers ## / {%- if cookiecutter.devops_cache == '0' %} - - traefik.http.routers.rt-frontend.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) + - traefik.http.routers.rt-frontend.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) - traefik.http.routers.rt-frontend.entrypoints=http - traefik.http.routers.rt-frontend.service=svc-frontend - traefik.http.routers.rt-frontend.middlewares=gzip {%- endif %} {%- if cookiecutter.devops_cache == '1' %} ### Router: Varnish Public - - traefik.http.routers.rt-frontend-public.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) + - traefik.http.routers.rt-frontend-public.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) - traefik.http.routers.rt-frontend-public.entrypoints=http - traefik.http.routers.rt-frontend-public.service=svc-varnish - traefik.http.routers.rt-frontend-public.middlewares=gzip ### Router: Internal - - traefik.http.routers.rt-frontend-internal.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) && Headers(`X-Varnish-Routed`, `1`) + - traefik.http.routers.rt-frontend-internal.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && Headers(`X-Varnish-Routed`, `1`) - traefik.http.routers.rt-frontend-internal.entrypoints=http - traefik.http.routers.rt-frontend-internal.service=svc-frontend {%- endif %} @@ -117,11 +119,11 @@ services: # Middlewares ## VHM rewrite /++api++/ - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.regex=^/\\+\\+api\\+\\+($$|/.*)" - - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/http/{{ cookiecutter.__project_slug }}.localhost/Plone/++api++/VirtualHostRoot$$1" + - "traefik.http.middlewares.mw-backend-vhm-api.replacepathregex.replacement=/VirtualHostBase/http/${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}/Plone/++api++/VirtualHostRoot$$1" ## VHM rewrite /ClassicUI/ - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.regex=^/ClassicUI($$|/.*)" - - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/http/{{ cookiecutter.__project_slug }}.localhost/Plone/VirtualHostRoot/_vh_ClassicUI$$1" + - "traefik.http.middlewares.mw-backend-vhm-classic.replacepathregex.replacement=/VirtualHostBase/http/${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}/Plone/VirtualHostRoot/_vh_ClassicUI$$1" ## Basic Authentication ### Note: all dollar signs in the hash need to be doubled for escaping. @@ -132,7 +134,7 @@ services: - traefik.http.middlewares.mw-backend-auth.basicauth.users=admin:$$apr1$$uZPT5Fgu$$AmlIdamxT5ipBvPlsdfD70 # Routers {%- if cookiecutter.devops_cache == '0' %} - - traefik.http.routers.rt-backend-api.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) && (PathPrefix(`/++api++`)) + - traefik.http.routers.rt-backend-api.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && (PathPrefix(`/++api++`)) - traefik.http.routers.rt-backend-api.entrypoints=http - traefik.http.routers.rt-backend-api.service=svc-backend - traefik.http.routers.rt-backend-api.middlewares=gzip,mw-backend-vhm-api @@ -140,19 +142,19 @@ services: {%- if cookiecutter.devops_cache == '1' %} ## /++api++/ ### Router: Varnish Public - - traefik.http.routers.rt-backend-api-public.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) && PathPrefix(`/++api++`) + - traefik.http.routers.rt-backend-api-public.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && PathPrefix(`/++api++`) - traefik.http.routers.rt-backend-api-public.entrypoints=http - traefik.http.routers.rt-backend-api-public.service=svc-varnish - traefik.http.routers.rt-backend-api-public.middlewares=gzip ### Router: Internal - - traefik.http.routers.rt-backend-api-internal.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) && PathPrefix(`/++api++`) && Headers(`X-Varnish-Routed`, `1`) + - traefik.http.routers.rt-backend-api-internal.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && PathPrefix(`/++api++`) && Headers(`X-Varnish-Routed`, `1`) - traefik.http.routers.rt-backend-api-internal.entrypoints=http - traefik.http.routers.rt-backend-api-internal.service=svc-backend - traefik.http.routers.rt-backend-api-internal.middlewares=gzip,mw-backend-vhm-api {%- endif %} ## /ClassicUI - - traefik.http.routers.rt-backend-classic.rule=Host(`{{ cookiecutter.__project_slug }}.localhost`) && PathPrefix(`/ClassicUI`) + - traefik.http.routers.rt-backend-classic.rule=Host(`${HOSTNAME:-{{ cookiecutter.__project_slug }}.localhost}`) && PathPrefix(`/ClassicUI`) - traefik.http.routers.rt-backend-classic.entrypoints=http - traefik.http.routers.rt-backend-classic.service=svc-backend - traefik.http.routers.rt-backend-classic.middlewares=gzip,mw-backend-auth,mw-backend-vhm-classic @@ -160,7 +162,7 @@ services: {%- if cookiecutter.devops_storage == 'relstorage' %} db: - image: postgres:{{ cookiecutter.__devops_db_version }} + image: postgres:{{ cookiecutter.__devops_postgres_version }} environment: POSTGRES_USER: plone POSTGRES_PASSWORD: {{ cookiecutter.__devops_db_password }} diff --git a/templates/sub/addon_settings/Makefile b/templates/sub/addon_settings/Makefile new file mode 100644 index 00000000..b505a789 --- /dev/null +++ b/templates/sub/addon_settings/Makefile @@ -0,0 +1,57 @@ +SHELL := /bin/bash +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +.PHONY: all +all: build + +BASE_FOLDER = $(shell git rev-parse --show-toplevel) +VENV_FOLDER = ${BASE_FOLDER}/.venv +BIN_FOLDER = ${VENV_FOLDER}/bin + +TEMPLATE = $(shell basename $(CURRENT_DIR)) +PROJECT_FOLDER_NAME = project-title + + + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: ## Clean + rm -rf $(PROJECT_FOLDER_NAME) + +$(VENV_FOLDER): ## cookieplone installation + $(MAKE) -C $(BASE_FOLDER) sync + +.PHONY: format +format: $(VENV_FOLDER)## Format code + @echo "$(GREEN)==> Formatting $(TEMPLATE) codebase $(RESET)" + @uv run ruff format --config $(BASE_FOLDER)/pyproject.toml hooks + @uv run ruff check --select I --fix --config $(BASE_FOLDER)/pyproject.toml hooks + +.PHONY: generate +generate: $(VENV_FOLDER) ## Create a sample package + @echo "$(GREEN)==> Creating new test package$(RESET)" + rm -rf $(PROJECT_FOLDER_NAME) + COOKIEPLONE_REPOSITORY=$(BASE_FOLDER) uv run cookieplone $(TEMPLATE) --no-input + +.PHONY: test +test: $(VENV_FOLDER)## Create a sample package and tests it + @echo "$(GREEN)==> Test template$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/sub/$(TEMPLATE) + +.PHONY: test-pdb +test-pdb: $(VENV_FOLDER)## Stop on the first failed test + @echo "$(GREEN)==> Test template, stop on first error$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/sub/$(TEMPLATE) -x --pdb diff --git a/templates/sub/addon_settings/README.md b/templates/sub/addon_settings/README.md new file mode 100644 index 00000000..bed982d5 --- /dev/null +++ b/templates/sub/addon_settings/README.md @@ -0,0 +1,68 @@ +# Cookieplone project settings + +[![Cookieplone Templates: CI](https://github.com/plone/cookieplone-templates/actions/workflows/main.yml/badge.svg)](https://github.com/plone/cookieplone-templates/blob/main/.github/workflows/main.yml) +[![Built with Cookieplone](https://img.shields.io/badge/built%20with-Cookiecutter-ff69b4.svg?logo=cookiecutter)](https://github.com/plone/cookieplone-templates/) +[![License](https://img.shields.io/github/license/plone/cookieplone-templates)](../../../LICENSE) +[![Black code style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +This is a subtemplate used by other templates, in automated tests, and for OCI image generation—accessible with **Cookieplone** at the path `sub/project_settings`. + + +## Prerequisites + +- [uv](https://docs.astral.sh/uv/) is the recommended tool for managing Python versions and project dependencies. +- Node.js and pnpm: essential for managing and running JavaScript packages. + + +### uv + +To install uv, use the following command, or visit the [uv installation page](https://docs.astral.sh/uv/getting-started/installation/) for alternative methods. + +```shell +curl -LsSf https://astral.sh/uv/install.sh | sh +``` + + +### Node.js + +Follow the [Plone documentation](https://6.docs.plone.org/install/install-from-packages.html#pre-requisites-for-installation) for detailed instructions. + + + +## Generate your project settings 🎉 + +```shell +uvx cookieplone sub/project_settings +``` + + +### Project generation options 🛠️ + +The table below describes the options you can customize using the [Cookiecutter CLI](https://github.com/cookiecutter/cookiecutter) during the generation process. + +| Option | Description | Example | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- | +| `title` | Your project's human-readable name, capitals and spaces allowed. | `Project Settings` | +| `author` | This is you! Its value goes into places like `LICENSE`, `package.json` and other files. | `Our Company` | +| `email` | The email address to contact the project maintainer. | `email@example.com` | +| `volto_version` | Volto version to be used. | `18.10.0` | + + +## Code quality assurance 🧐 + +Your project comes equipped with linters to ensure code quality. +Run the following command to automatically format your code. + +```shell +make format +``` + + +## License 📜 + +This project is licensed under the [MIT License](/LICENSE). + + +## Let's get building! 🚀 + +Happy coding! diff --git a/templates/sub/addon_settings/cookiecutter.json b/templates/sub/addon_settings/cookiecutter.json new file mode 100644 index 00000000..11535b92 --- /dev/null +++ b/templates/sub/addon_settings/cookiecutter.json @@ -0,0 +1,61 @@ +{ + "title": "Project Title", + "description": "A new project using Plone 6.", + "project_slug": "{{ cookiecutter.title | slugify }}", + "author": "Plone Foundation", + "email": "collective@plone.org", + "python_package_name": "{{ cookiecutter.project_slug|replace(' ', '')|replace('-', '.') }}", + "frontend_addon_name": "volto-{{ cookiecutter.python_package_name|replace('_', '-')|replace('.', '-') }}", + "npm_package_name": "{{ cookiecutter.frontend_addon_name }}", + "language_code": "en", + "use_prerelease_versions": "{{ 'No' | use_prerelease_versions }}", + "plone_version": "{{ 'No' | latest_plone }}", + "volto_version": "{{ cookiecutter.use_prerelease_versions | latest_volto }}", + "organization": "collective", + "repository_url": "https://github.com/{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "container_image_prefix": "{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "cookieplone_template": "", + "generator_sha": "", + "__feature_distribution": "0", + "__backend_base_package_name": "Products.CMFPlone", + "__backend_base_package_version": "{{ cookiecutter.plone_version }}", + "__project_slug": "{{ cookiecutter.project_slug }}", + "__repository_url": "{{ cookiecutter.repository_url }}", + "__repository_git": "git@github.com:{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}", + "__container_image_prefix": "{{ cookiecutter.container_image_prefix }}", + "__node_version": "{{ cookiecutter.volto_version | node_version_for_volto }}", + "__npm_package_name": "{{ cookiecutter.npm_package_name }}", + "__folder_name": "{{ cookiecutter.project_slug }}", + "__package_path": "{{ cookiecutter.python_package_name | package_path }}", + "__python_version": "3.12", + "__supported_versions_python": ["{{ cookiecutter.__python_version }}"], + "__supported_versions_plone": ["{{ cookiecutter.plone_version | as_major_minor }}"], + "__python_version_identifier": "{{ cookiecutter.__python_version | replace('.', '') }}", + "__version_frontend_package": "1.0.0-alpha.0", + "__version_plone_volto": "{{ cookiecutter.volto_version }}", + "__version_mrs_developer": "^2.2.0", + "__version_pnpm": "{{ '10.20.0' if cookiecutter.volto_version >= '19' else '9.1.1' }}", + "__version_release_it": "^17.1.1", + "__test_framework": "{{ 'vitest' if cookiecutter.volto_version >= '19' else 'jest'}}", + "_copy_without_render": [], + "_extensions": [ + "cookieplone.filters.use_prerelease_versions", + "cookieplone.filters.node_version_for_volto", + "cookieplone.filters.extract_host", + "cookieplone.filters.image_prefix", + "cookieplone.filters.pascal_case", + "cookieplone.filters.locales_language_code", + "cookieplone.filters.gs_language_code", + "cookieplone.filters.package_namespace_path", + "cookieplone.filters.package_path", + "cookieplone.filters.as_major_minor", + "cookieplone.filters.latest_volto", + "cookieplone.filters.latest_plone" + ], + "__cookieplone_repository_path": "", + "__cookieplone_template": "{{ cookiecutter.cookieplone_template }}", + "__generator_sha": "{{ cookiecutter.generator_sha }}", + "__generator_template_url": "https://github.com/plone/cookieplone-templates/tree/main/{{ cookiecutter.__cookieplone_template }}", + "__generator_date_long": "{% now 'utc', '%Y-%m-%d %H:%M:%S' %}", + "__generator_signature": "Generated from the [`cookieplone-templates` {{ cookiecutter.__cookieplone_template }} template]({{ cookiecutter.__generator_template_url }}) on {{ cookiecutter.__generator_date_long }}." +} diff --git a/templates/sub/addon_settings/hooks/post_gen_project.py b/templates/sub/addon_settings/hooks/post_gen_project.py new file mode 100644 index 00000000..c0ff65e4 --- /dev/null +++ b/templates/sub/addon_settings/hooks/post_gen_project.py @@ -0,0 +1,25 @@ +"""Post generation hook.""" + +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path + +from cookieplone.utils import console, files + +context: OrderedDict = {{cookiecutter}} + + +def main(): + """Final fixes.""" + output_dir = Path().cwd() + actions = [] + for func, title, enabled in actions: + if not int(enabled): + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + +if __name__ == "__main__": + main() diff --git a/templates/sub/addon_settings/hooks/pre_gen_project.py b/templates/sub/addon_settings/hooks/pre_gen_project.py new file mode 100644 index 00000000..6a2e382b --- /dev/null +++ b/templates/sub/addon_settings/hooks/pre_gen_project.py @@ -0,0 +1,17 @@ +"""Pre generation hook.""" + +from collections import OrderedDict +from pathlib import Path + +output_path = Path().resolve() + +context: OrderedDict = {{cookiecutter}} + + +def main(): + """Validate context.""" + pass + + +if __name__ == "__main__": + main() diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/.dockerignore b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/.dockerignore new file mode 100644 index 00000000..e9e94ffc --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/.dockerignore @@ -0,0 +1,14 @@ +.editorconfig +.gitattributes +bin +Dockerfile +Dockerfile.acceptance +include +instance +instance.yaml +lib +lib64 +Makefile +pyvenv.cfg +var +.venv diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/Makefile b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/Makefile new file mode 100644 index 00000000..78aa6cd3 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/Makefile @@ -0,0 +1,166 @@ +### Defensive settings for make: +# https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +.SHELLFLAGS:=-xeu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` +# Python checks +UV?=uv + +# installed? +ifeq (, $(shell which $(UV) )) + $(error "UV=$(UV) not found in $(PATH)") +endif + +REPOSITORY_SETTINGS := $(shell uvx repoplone settings dump) +IMAGE_NAME_PREFIX := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.container_images_prefix') + +IMAGE_TAG=latest +IMAGE_NAME=$(IMAGE_NAME_PREFIX)-backend:$(IMAGE_TAG) +PLONE_SITE_ID=Plone +BACKEND_FOLDER=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) +PLONE_VERSION := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.backend.base_package_version') +EXAMPLE_CONTENT_FOLDER := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.backend.code_path')/setuphandlers/examplecontent +PACKAGE_NAME := $(shell echo '$(REPOSITORY_SETTINGS)' | jq -r '.backend.name') + +VENV_FOLDER=$(BACKEND_FOLDER)/.venv +export VIRTUAL_ENV=$(VENV_FOLDER) +BIN_FOLDER=$(VENV_FOLDER)/bin + +# Environment variables to be exported +export UV_VENV_CLEAR := 1 +export PYTHONWARNINGS := ignore +export DOCKER_BUILDKIT := 1 + +all: build + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: debug-settings +debug-settings: ## Debug settings + @echo "PLONE_VERSION: $(PLONE_VERSION)" + @echo "PACKAGE_NAME: $(PACKAGE_NAME)" + @echo "EXAMPLE_CONTENT_FOLDER: $(EXAMPLE_CONTENT_FOLDER)" + +requirements-mxdev.txt: pyproject.toml mx.ini ## Generate constraints file + @echo "$(GREEN)==> Generate constraints file$(RESET)" + @echo '-c https://dist.plone.org/release/$(PLONE_VERSION)/constraints.txt' > requirements.txt + @uvx mxdev -c mx.ini + +$(VENV_FOLDER): requirements-mxdev.txt ## Install dependencies + @echo "$(GREEN)==> Install environment$(RESET)" +ifdef CI + @uv venv $(VENV_FOLDER) +else + @uv venv --python={{ cookiecutter.__supported_versions_python[0] }} $(VENV_FOLDER) +endif + @uv pip install -r requirements-mxdev.txt + +.PHONY: sync +sync: $(VENV_FOLDER) ## Sync project dependencies + @echo "$(GREEN)==> Sync project dependencies$(RESET)" + @uv pip install -r requirements-mxdev.txt + +instance/etc/zope.ini instance/etc/zope.conf: instance.yaml ## Create instance configuration + @echo "$(GREEN)==> Create instance configuration$(RESET)" + @uvx cookiecutter -f --no-input -c 2.1.1 --config-file instance.yaml gh:plone/cookiecutter-zope-instance + +.PHONY: config +config: instance/etc/zope.ini + +.PHONY: install +install: $(VENV_FOLDER) config ## Install Plone and dependencies + + +.PHONY: clean +clean: ## Clean installation and instance (data left intact) + @echo "$(RED)==> Cleaning environment and build$(RESET)" + @rm -rf $(VENV_FOLDER) pyvenv.cfg .installed.cfg instance/etc .venv .pytest_cache .ruff_cache constraints* requirements* + +.PHONY: remove-data +remove-data: ## Remove all content + @echo "$(RED)==> Removing all content$(RESET)" + rm -rf $(VENV_FOLDER) instance/var + +.PHONY: start +start: $(VENV_FOLDER) instance/etc/zope.ini ## Start a Plone instance on localhost:8080 + @$(BIN_FOLDER)/runwsgi instance/etc/zope.ini + +.PHONY: console +console: $(VENV_FOLDER) instance/etc/zope.ini ## Start a console into a Plone instance + @$(BIN_FOLDER)/zconsole debug instance/etc/zope.conf + +.PHONY: create-site +create-site: $(VENV_FOLDER) instance/etc/zope.ini ## Create a new site from scratch + @$(BIN_FOLDER)/zconsole run instance/etc/zope.conf ./scripts/create_site.py + +# Example Content +.PHONY: update-example-content +update-example-content: $(VENV_FOLDER) ## Export example content inside package + @echo "$(GREEN)==> Export example content into $(EXAMPLE_CONTENT_FOLDER) $(RESET)" + if [ -d $(EXAMPLE_CONTENT_FOLDER)/content ]; then rm -r $(EXAMPLE_CONTENT_FOLDER)/* ;fi + @$(BIN_FOLDER)/plone-exporter instance/etc/zope.conf $(PLONE_SITE_ID) $(EXAMPLE_CONTENT_FOLDER) + +# QA +.PHONY: lint +lint: ## Check and fix code base according to Plone standards + @echo "$(GREEN)==> Lint codebase$(RESET)" + @uvx ruff@latest check --fix --config $(BACKEND_FOLDER)/pyproject.toml + @uvx pyroma@latest -d . + @uvx check-python-versions@latest . + @uvx zpretty@latest --check src + +.PHONY: format +format: ## Check and fix code base according to Plone standards + @echo "$(GREEN)==> Format codebase$(RESET)" + @uvx ruff@latest check --select I --fix --config $(BACKEND_FOLDER)/pyproject.toml + @uvx ruff@latest format --config $(BACKEND_FOLDER)/pyproject.toml + @uvx zpretty@latest -i src + +# i18n +.PHONY: i18n +i18n: $(VENV_FOLDER) ## Update locales + @echo "$(GREEN)==> Updating locales$(RESET)" + @$(BIN_FOLDER)/python -m $(PACKAGE_NAME).locales + +# Tests +.PHONY: test +test: $(VENV_FOLDER) ## run tests + @$(BIN_FOLDER)/pytest + +.PHONY: test-coverage +test-coverage: $(VENV_FOLDER) ## run tests with coverage + @$(BIN_FOLDER)/pytest --cov=$(PACKAGE_NAME) --cov-report term-missing + +# Build Docker images +.PHONY: build-image +build-image: ## Build Docker Images + @echo "$(GREEN)==> Building image $(IMAGE_NAME) $(RESET)" + @docker build . -t $(IMAGE_NAME) --progress plain -f Dockerfile + +# Acceptance tests +.PHONY: acceptance-backend-start +acceptance-backend-start: ## Start backend acceptance server + ZSERVER_HOST=0.0.0.0 ZSERVER_PORT=55001 LISTEN_PORT=55001 APPLY_PROFILES="{{ cookiecutter.python_package_name }}:default" CONFIGURE_PACKAGES="plone.restapi,plone.volto,plone.volto.cors,{{ cookiecutter.python_package_name }}" $(BIN_FOLDER)/robot-server plone.app.robotframework.testing.VOLTO_ROBOT_TESTING + +.PHONY: acceptance-image-build +acceptance-image-build: ## Build Docker Images + @docker build . -t $(IMAGE_NAME_PREFIX)-backend-acceptance:$(IMAGE_TAG) -f Dockerfile.acceptance --build-arg PLONE_VERSION=$(PLONE_VERSION) + +## Add bobtemplates features (check bobtemplates.plone's documentation to get the list of available features) +add: $(VENV_FOLDER) + @uvx plonecli add -b .mrbob.ini $(filter-out $@,$(MAKECMDGOALS)) diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/README.md b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/README.md new file mode 100644 index 00000000..3a1ebcf3 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/README.md @@ -0,0 +1,87 @@ +# {{ cookiecutter.python_package_name }} + +{{ cookiecutter.description }} + +## Features + +TODO: List our awesome features + +## Installation + +Install {{ cookiecutter.python_package_name }} with uv. + +```shell +uv add {{ cookiecutter.python_package_name }} +``` + +Create the Plone site. + +```shell +make create-site +``` + +## Contribute + +- [Issue tracker]({{ cookiecutter.__repository_url }}/issues) +- [Source code]({{ cookiecutter.__repository_url }}/) + +### Prerequisites ✅ + +- An [operating system](https://6.docs.plone.org/install/create-project-cookieplone.html#prerequisites-for-installation) that runs all the requirements mentioned. +- [uv](https://6.docs.plone.org/install/create-project-cookieplone.html#uv) +- [Make](https://6.docs.plone.org/install/create-project-cookieplone.html#make) +- [Git](https://6.docs.plone.org/install/create-project-cookieplone.html#git) +- [Docker](https://docs.docker.com/get-started/get-docker/) (optional) + +### Installation 🔧 + +1. Clone this repository. + + ```shell + git clone {{ cookiecutter.__repository_git }}.git + cd {{ cookiecutter.__project_slug }}/backend + ``` + +2. Install this code base. + + ```shell + make install + ``` + + +### Add features using `plonecli` or `bobtemplates.plone` + +This package provides markers as strings (``) that are compatible with [`plonecli`](https://github.com/plone/plonecli) and [`bobtemplates.plone`](https://github.com/plone/bobtemplates.plone). +These markers act as hooks to add all kinds of features through subtemplates, including behaviors, control panels, upgrade steps, or other subtemplates from `bobtemplates.plone`. +`plonecli` is a command line client for `bobtemplates.plone`, adding autocompletion and other features. + +To add a feature as a subtemplate to your package, use the following command pattern. + +```shell +make add +``` + +For example, you can add a content type to your package with the following command. + +```shell +make add content_type +``` + +You can add a behavior with the following command. + +```shell +make add behavior +``` + +```{seealso} +You can check the list of available subtemplates in the [`bobtemplates.plone` `README.md` file](https://github.com/plone/bobtemplates.plone/?tab=readme-ov-file#provided-subtemplates). +See also the documentation of [Mockup and Patternslib](https://6.docs.plone.org/classic-ui/mockup.html) for how to build the UI toolkit for Classic UI. +``` + +## License + +The project is licensed under GPLv2. + +## Credits and acknowledgements 🙏 + +{{ cookiecutter.__generator_signature }}. A special thanks to all contributors and supporters! diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml new file mode 100644 index 00000000..d64980c4 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml @@ -0,0 +1,188 @@ +[project] +name = "{{ cookiecutter.python_package_name }}" +dynamic = ["version"] +description = "{{ cookiecutter.description }}" +readme = "README.md" +license = "GPL-2.0-only" +requires-python = ">={{ cookiecutter.__supported_versions_python[0] }}" +authors = [ + { name = "{{ cookiecutter.author }}", email = "{{ cookiecutter.email }}" }, +] +keywords = [ + "CMS", + "Plone", + "Python", +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Framework :: Plone", + {%- for version in cookiecutter.__supported_versions_plone -%} + "Framework :: Plone :: {{ version }}", + {%- endfor %} + "Framework :: Plone :: Addon", + {%- if cookiecutter.__feature_distribution == '1' %} + "Framework :: Plone :: Distribution", + {%- endif %} + "Operating System :: OS Independent", + "Programming Language :: Python", + {%- for version in cookiecutter.__supported_versions_python -%} + "Programming Language :: Python :: {{ version }}", + {%- endfor %} +] +dependencies = [ + "{{ cookiecutter.__backend_base_package_name }}", + "plone.api", + "plone.restapi", + "plone.volto", +] + +[project.optional-dependencies] +test = [ + "horse-with-no-namespace", + "plone.app.testing", + "plone.restapi[test]", + "pytest", + "pytest-cov", + "pytest-plone>=1.0.0a2", +] + +[dependency-groups] +container = [ + "plone.app.upgrade", + "psycopg2==2.9.10", + "relstorage==4.1.1", + "zeo==6.0.0", +] + + +[project.urls] +Homepage = "{{ cookiecutter.__repository_url }}" +Source = "{{ cookiecutter.__repository_url }}" +Tracker = "{{ cookiecutter.__repository_url }}/issues" + +[project.entry-points."plone.autoinclude.plugin"] +target = "plone" + +[tool.uv] +managed = false + +[tool.hatch.version] +path = "src/{{ cookiecutter.__package_path }}/__init__.py" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build] +strict-naming = true + +[tool.hatch.build.targets.sdist] +exclude = [ + "/.github", +] + +[tool.hatch.build.targets.wheel] +packages = ["{{ cookiecutter.python_package_name | package_namespace_path }}"] + +[tool.towncrier] +directory = "news/" +filename = "CHANGELOG.md" +start_string = "\n" +title_format = "## {version} ({project_date})" +template = "news/.changelog_template.jinja" +issue_format = "[#{issue}]({{ cookiecutter.__repository_url }}/issues/{issue})" +underlines = ["", "", ""] + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking changes:" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "New features:" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug fixes:" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal:" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation:" +showcontent = true + +[[tool.towncrier.type]] +directory = "tests" +name = "Tests" +showcontent = true + +[tool.ruff] +target-version = "py{{ cookiecutter.__python_version_identifier }}" +line-length = 88 +fix = true +lint.select = [ + # flake8-2020 + "YTT", + # flake8-bandit + "S", + # flake8-bugbear + "B", + # flake8-builtins + "A", + # flake8-comprehensions + "C4", + # flake8-debugger + "T10", + # flake8-simplify + "SIM", + # mccabe + "C90", + # pycodestyle + "E", "W", + # pyflakes + "F", + # pygrep-hooks + "PGH", + # pyupgrade + "UP", + # ruff + "RUF", +] +lint.ignore = [ + # DoNotAssignLambda + "E731", +] + +[tool.ruff.lint.isort] +case-sensitive = false +no-sections = true +force-single-line = true +from-first = true +lines-after-imports = 2 +lines-between-types = 1 +order-by-type = false + +[tool.ruff.format] +preview = true + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["E501", "RUF001", "S101"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.coverage.run] +source_pkgs = ["{{ cookiecutter.python_package_name }}", "tests"] +branch = true +parallel = true +omit = [ + "src/{{ cookiecutter.__package_path }}/locales/*.py", +] \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/__init__.py b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/__main__.py b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/__main__.py new file mode 100644 index 00000000..a5677b6a --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/__main__.py @@ -0,0 +1,69 @@ +"""Update locales.""" + +from pathlib import Path + +import logging +import re +import subprocess + + +logger = logging.getLogger("i18n") +logger.setLevel(logging.DEBUG) + + +PATTERN = r"^[a-z]{2}.*" + +locale_path = Path(__file__).parent.resolve() +target_path = locale_path.parent.resolve() +domains = [path.name[:-4] for path in locale_path.glob("*.pot")] + +i18ndude = "uvx i18ndude" + +# ignore node_modules files resulting in errors +excludes = '"*.html *json-schema*.xml"' + + +def locale_folder_setup(domain: str): + languages = [path for path in locale_path.glob("*") if path.is_dir()] + for lang_folder in languages: + lc_messages_path = lang_folder / "LC_MESSAGES" + lang = lang_folder.name + if lc_messages_path.exists(): + continue + elif re.match(PATTERN, lang): + lc_messages_path.mkdir() + cmd = ( + f"msginit --locale={lang} " + f"--input={locale_path}/{domain}.pot " + f"--output={locale_path}/{lang}/LC_MESSAGES/{domain}.po" + ) + subprocess.call(cmd, shell=True) # noQA: S602 + + +def _rebuild(domain: str): + cmd = ( + f"{i18ndude} rebuild-pot --pot {locale_path}/{domain}.pot " + f"--exclude {excludes} " + f"--create {domain} {target_path}" + ) + subprocess.call(cmd, shell=True) # noQA: S602 + + +def _sync(domain: str): + cmd = ( + f"{i18ndude} sync --pot {locale_path}/{domain}.pot " + f"{locale_path}/*/LC_MESSAGES/{domain}.po" + ) + subprocess.call(cmd, shell=True) # noQA: S602 + + +def main(): + for domain in domains: + logger.info(f"Updating translations for {domain}") + locale_folder_setup(domain) + _rebuild(domain) + _sync(domain) + + +if __name__ == "__main__": + main() diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/de/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/de/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po new file mode 100644 index 00000000..f023704e --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/de/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po @@ -0,0 +1,15 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-05-25 17:12+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: de\n" +"Language-Name: German\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: DOMAIN\n" diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/es/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/es/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po new file mode 100644 index 00000000..e39c307b --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/es/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po @@ -0,0 +1,15 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-05-25 17:12+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: es\n" +"Language-Name: English\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: DOMAIN\n" diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/eu/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/eu/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po new file mode 100644 index 00000000..5183ebdf --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/eu/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po @@ -0,0 +1,15 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-05-25 17:12+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: eu\n" +"Language-Name: Basque\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: DOMAIN\n" diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/it/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/it/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po new file mode 100644 index 00000000..7fd806d8 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/it/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po @@ -0,0 +1,15 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-05-25 17:12+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: it\n" +"Language-Name: Italian\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: DOMAIN\n" diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/pt_BR/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/pt_BR/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po new file mode 100644 index 00000000..a9822ebe --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/pt_BR/LC_MESSAGES/{{ cookiecutter.python_package_name }}.po @@ -0,0 +1,15 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-05-25 17:12+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: pt_BR\n" +"Language-Name: Portuguese (Brazil)\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: DOMAIN\n" diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/{{ cookiecutter.python_package_name }}.pot b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/{{ cookiecutter.python_package_name }}.pot new file mode 100644 index 00000000..62868491 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/locales/{{ cookiecutter.python_package_name }}.pot @@ -0,0 +1,18 @@ +#--- PLEASE EDIT THE LINES BELOW CORRECTLY --- +#SOME DESCRIPTIVE TITLE. +#FIRST AUTHOR , YEAR. +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2022-05-25 17:12+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0\n" +"Language-Code: en\n" +"Language-Name: English\n" +"Preferred-Encodings: utf-8 latin1\n" +"Domain: {{ cookiecutter.python_package_name }}\n" diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml new file mode 100644 index 00000000..4cb1c804 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles/initial/metadata.xml b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles/initial/metadata.xml new file mode 100644 index 00000000..f768b5a7 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles/initial/metadata.xml @@ -0,0 +1,4 @@ + + + 1000 + diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/__init__.py b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/__init__.py new file mode 100644 index 00000000..230d4c0c --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/__init__.py @@ -0,0 +1,17 @@ +from Products.CMFPlone.interfaces import INonInstallable +from zope.interface import implementer + + +@implementer(INonInstallable) +class HiddenProfiles: + def getNonInstallableProfiles(self): + """Hide initial profile from site-creation and quickinstaller.""" + return [ + "{{ cookiecutter.python_package_name }}:initial", + ] + + def getNonInstallableProducts(self): + """Hide the upgrades package from site-creation and quickinstaller.""" + return [ + "{{ cookiecutter.python_package_name }}.upgrades", + ] diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/.gitkeep b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/__metadata__.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/__metadata__.json new file mode 100644 index 00000000..7281b504 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/__metadata__.json @@ -0,0 +1,40 @@ +{ + "__version__": "1.0.0", + "_blob_files_": [ + "a58ccead718140c1baa98d43595fc3e6/image/plone-foundation.png" + ], + "_data_files_": [ + "plone_site_root/data.json", + "a720393b3c0240e5bd27c43fcd2cfd1e/data.json", + "a58ccead718140c1baa98d43595fc3e6/data.json" + ], + "default_page": {}, + "local_roles": { + "a58ccead718140c1baa98d43595fc3e6": { + "local_roles": { + "admin": [ + "Owner" + ] + } + }, + "a720393b3c0240e5bd27c43fcd2cfd1e": { + "local_roles": { + "admin": [ + "Owner" + ] + } + }, + "plone_site_root": { + "local_roles": { + "admin": [ + "Owner" + ] + } + } + }, + "ordering": { + "a58ccead718140c1baa98d43595fc3e6": 0, + "a720393b3c0240e5bd27c43fcd2cfd1e": 42 + }, + "relations": [] +} \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a58ccead718140c1baa98d43595fc3e6/data.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a58ccead718140c1baa98d43595fc3e6/data.json new file mode 100644 index 00000000..08dbccf1 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a58ccead718140c1baa98d43595fc3e6/data.json @@ -0,0 +1,56 @@ +{ + "@id": "/images/plone-foundation.png", + "@type": "Image", + "UID": "a58ccead718140c1baa98d43595fc3e6", + "allow_discussion": false, + "contributors": [], + "created": "2024-05-27T03:23:37+00:00", + "creators": [ + "admin" + ], + "description": "", + "effective": null, + "exclude_from_nav": false, + "expires": null, + "exportimport.constrains": {}, + "exportimport.conversation": [], + "exportimport.versions": {}, + "id": "plone-foundation.png", + "image": { + "blob_path": "a58ccead718140c1baa98d43595fc3e6/image/plone-foundation.png", + "content-type": "image/png", + "filename": "plone-foundation.png", + "height": 439, + "size": 50737, + "width": 2000 + }, + "is_folderish": false, + "language": "##DEFAULT##", + "layout": "image_view", + "lock": {}, + "modified": "2024-05-27T03:24:00+00:00", + "parent": { + "@id": "/images", + "@type": "Document", + "Subject": [], + "UID": "a720393b3c0240e5bd27c43fcd2cfd1e", + "description": "Site images", + "effective": "1969-12-31T03:00:00+00:00", + "image_field": null, + "image_scales": {}, + "review_state": "private", + "title": "Images", + "type_title": "Page" + }, + "review_state": null, + "rights": "", + "subjects": [ + "Plone" + ], + "title": "Plone Foundation Logo", + "type_title": "Image", + "version": "current", + "workflow_history": {}, + "working_copy": null, + "working_copy_of": null +} \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a58ccead718140c1baa98d43595fc3e6/image/plone-foundation.png b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a58ccead718140c1baa98d43595fc3e6/image/plone-foundation.png new file mode 100644 index 00000000..9516c4c6 Binary files /dev/null and b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a58ccead718140c1baa98d43595fc3e6/image/plone-foundation.png differ diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a720393b3c0240e5bd27c43fcd2cfd1e/data.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a720393b3c0240e5bd27c43fcd2cfd1e/data.json new file mode 100644 index 00000000..0a4788e8 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/a720393b3c0240e5bd27c43fcd2cfd1e/data.json @@ -0,0 +1,98 @@ +{ + "@id": "/images", + "@type": "Document", + "UID": "a720393b3c0240e5bd27c43fcd2cfd1e", + "allow_discussion": false, + "blocks": { + "30f271c5-dadf-4f47-987f-6ce417922c6b": { + "@type": "title" + }, + "ecfe183b-1d44-48f9-8eac-5de33884c7f5": { + "@type": "listing", + "headlineTag": "h2", + "querystring": { + "query": [ + { + "i": "portal_type", + "o": "plone.app.querystring.operation.selection.any", + "v": [ + "Image" + ] + }, + { + "i": "path", + "o": "plone.app.querystring.operation.string.absolutePath", + "v": "/images" + } + ], + "sort_on": "getObjPositionInParent", + "sort_order": "ascending", + "sort_order_boolean": false + }, + "variation": "imageGallery" + } + }, + "blocks_layout": { + "items": [ + "30f271c5-dadf-4f47-987f-6ce417922c6b", + "ecfe183b-1d44-48f9-8eac-5de33884c7f5" + ] + }, + "contributors": [], + "created": "2024-05-27T03:23:26+00:00", + "creators": [ + "admin" + ], + "description": "Site images", + "effective": null, + "exclude_from_nav": true, + "expires": null, + "exportimport.constrains": {}, + "exportimport.conversation": [], + "exportimport.versions": {}, + "id": "images", + "is_folderish": true, + "language": "##DEFAULT##", + "layout": "document_view", + "lock": { + "locked": false, + "stealable": true + }, + "modified": "2024-05-27T03:26:27+00:00", + "parent": { + "@id": "/", + "@type": "Plone Site", + "UID": "plone_site_root", + "description": "", + "title": "Project Title", + "type_title": "Plone Site" + }, + "preview_caption": null, + "preview_image": null, + "review_state": "published", + "rights": "", + "subjects": [], + "title": "Images", + "type_title": "Page", + "version": "current", + "workflow_history": { + "simple_publication_workflow": [ + { + "action": null, + "actor": "admin", + "comments": "", + "review_state": "private", + "time": "2024-05-27T03:23:26+00:00" + }, + { + "action": "publish", + "actor": "admin", + "comments": "", + "review_state": "published", + "time": "2024-05-27T03:50:30+00:00" + } + ] + }, + "working_copy": null, + "working_copy_of": null +} diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/plone_site_root/data.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/plone_site_root/data.json new file mode 100644 index 00000000..d0097e7d --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/content/plone_site_root/data.json @@ -0,0 +1,267 @@ +{ + "@id": "/Plone", + "@type": "Plone Site", + "UID": "plone_site_root", + "allow_discussion": null, + "blocks": { + "47c92ffe-c201-493c-97c9-a2d4e0beab69": { + "@type": "slate", + "plaintext": "Plone is a powerful content management system built on a rock-solid application stack written in the Python and JavaScript programming languages.", + "value": [ + { + "children": [ + { + "text": "Plone is a powerful content management system built on a rock-solid application stack written in the " + }, + { + "children": [ + { + "text": "Python" + } + ], + "type": "em" + }, + { + "text": " and " + }, + { + "children": [ + { + "text": "JavaScript" + } + ], + "type": "em" + }, + { + "text": " programming languages." + } + ], + "type": "p" + } + ] + }, + "5f31e9e4-173f-4ec1-ab48-64e9e2789b76": { + "@type": "title" + }, + "68357972-bd25-47e2-a098-c7932a923182": { + "@type": "image", + "alt": "Plone Foundation Logo", + "href": [ + { + "@id": "https://plone.org/foundation", + "title": "plone.org/foundation" + } + ], + "image_field": "image", + "url": "resolveuid/a58ccead718140c1baa98d43595fc3e6" + }, + "ae41d44d-c4b1-426c-acf6-2e28cb9a97a0": { + "@type": "slate", + "plaintext": "Find out more about Plone", + "value": [ + { + "children": [ + { + "text": "Find out more about Plone" + } + ], + "type": "h3" + } + ] + }, + "d9707a27-2565-4d7d-9daa-6c61d60ce8fd": { + "@type": "slate", + "plaintext": " The features of Plone Plone Documentation Plone Training Plone Community Forum Add-ons for Plone (backend) Add-ons for Volto (frontend) ", + "value": [ + { + "children": [ + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "The features of Plone" + } + ], + "data": { + "url": "https://plone.org/why-plone/features" + }, + "type": "link" + }, + { + "text": "" + } + ], + "type": "li" + }, + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Plone Documentation" + } + ], + "data": { + "url": "https://docs.plone.org/" + }, + "type": "link" + }, + { + "text": "" + } + ], + "type": "li" + }, + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Plone Training" + } + ], + "data": { + "url": "https://training.plone.org/" + }, + "type": "link" + }, + { + "text": "" + } + ], + "type": "li" + }, + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Plone Community Forum" + } + ], + "data": { + "url": "https://community.plone.org/" + }, + "type": "link" + }, + { + "text": "" + } + ], + "type": "li" + }, + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Add-ons for Plone (backend)" + } + ], + "data": { + "url": "https://github.com/collective/awesome-plone#readme" + }, + "type": "link" + }, + { + "text": "" + } + ], + "type": "li" + }, + { + "children": [ + { + "text": "" + }, + { + "children": [ + { + "text": "Add-ons for Volto (frontend)" + } + ], + "data": { + "url": "https://github.com/collective/awesome-volto#readme" + }, + "type": "link" + }, + { + "text": "" + } + ], + "type": "li" + } + ], + "type": "ul" + } + ] + }, + "e5ace182-3746-472c-ad81-696425a2b161": { + "@type": "slate", + "plaintext": "Welcome to your new Plone site!", + "value": [ + { + "children": [ + { + "text": "Welcome to your new Plone site!" + } + ], + "type": "h2" + } + ] + } + }, + "blocks_layout": { + "items": [ + "5f31e9e4-173f-4ec1-ab48-64e9e2789b76", + "e5ace182-3746-472c-ad81-696425a2b161", + "ae41d44d-c4b1-426c-acf6-2e28cb9a97a0", + "47c92ffe-c201-493c-97c9-a2d4e0beab69", + "d9707a27-2565-4d7d-9daa-6c61d60ce8fd", + "68357972-bd25-47e2-a098-c7932a923182" + ] + }, + "contributors": [], + "creators": [ + "admin" + ], + "description": "", + "effective": null, + "exclude_from_nav": false, + "expires": null, + "exportimport.constrains": {}, + "exportimport.conversation": [], + "exportimport.versions": {}, + "id": "Plone", + "is_folderish": true, + "items_total": 0, + "language": "##DEFAULT##", + "lock": { + "locked": false, + "stealable": true + }, + "parent": {}, + "review_state": null, + "rights": "", + "subjects": [], + "table_of_contents": null, + "title": "Project Title", + "type_title": "Plone Site", + "workflow_history": {} +} \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/discussions.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/discussions.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/discussions.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/portlets.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/portlets.json new file mode 100644 index 00000000..fe51488c --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/portlets.json @@ -0,0 +1 @@ +[] diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/principals.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/principals.json new file mode 100644 index 00000000..3bc1f3e2 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/principals.json @@ -0,0 +1,4 @@ +{ + "groups": [], + "members": [] +} diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/redirects.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/redirects.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/redirects.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/relations.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/relations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/relations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/translations.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/translations.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/examplecontent/translations.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/initial.py b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/initial.py new file mode 100644 index 00000000..8662bb9d --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/setuphandlers/initial.py @@ -0,0 +1,16 @@ +from {{ cookiecutter.python_package_name }} import logger +from pathlib import Path +from plone import api +from plone.exportimport import importers +from Products.GenericSetup.tool import SetupTool + + +EXAMPLE_CONTENT_FOLDER = Path(__file__).parent / "examplecontent" + + +def create_example_content(portal_setup: SetupTool): + """Import content available at the examplecontent folder.""" + portal = api.portal.get() + importer = importers.get_importer(portal) + for line in importer.import_site(EXAMPLE_CONTENT_FOLDER): + logger.info(line) diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/.dockerignore b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/.dockerignore new file mode 100644 index 00000000..71a9d07b --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/.dockerignore @@ -0,0 +1,6 @@ +*.log +build +cache +cypress +Dockerfile +node_modules diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/Dockerfile b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/Dockerfile new file mode 100644 index 00000000..9bd78f97 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/Dockerfile @@ -0,0 +1,37 @@ +# syntax=docker/dockerfile:1 +ARG VOLTO_VERSION=latest +FROM plone/frontend-builder:${VOLTO_VERSION} AS builder + +COPY --chown=node packages/{{cookiecutter.frontend_addon_name}} /app/packages/{{cookiecutter.frontend_addon_name}} +COPY --chown=node volto.config.js /app/ +COPY --chown=node package.json /app/package.json.temp +COPY --chown=node mrs.developer.json /app/ +COPY --chown=node pnpm-workspace.yaml /app/ + +RUN --mount=type=cache,id=pnpm,target=/app/.pnpm-store,uid=1000 < Cleaning Volto core and node_modules$(RESET)" + rm -rf core node_modules + +.PHONY: install +install: ## Installs the add-on in a development environment + pnpm dlx mrs-developer missdev --no-config --fetch-https + pnpm i + make build-deps + +.PHONY: start +start: ## Starts Volto, allowing reloading of the add-on during development + pnpm start + +.PHONY: build +build: ## Build a production bundle for distribution of the project with the add-on + pnpm build + +core/packages/registry/dist: $(shell find core/packages/registry/src -type f) + pnpm --filter @plone/registry build + +core/packages/components/dist: $(shell find core/packages/components/src -type f) + pnpm --filter @plone/components build + +.PHONY: build-deps +build-deps: core/packages/registry/dist core/packages/components/dist ## Build dependencies + +.PHONY: i18n +i18n: ## Sync i18n + pnpm --filter $(ADDON_NAME) i18n + +.PHONY: ci-i18n +ci-i18n: ## Check if i18n is not synced + pnpm --filter $(ADDON_NAME) i18n && git diff -G'^[^\"POT]' --exit-code + +.PHONY: format +format: ## Format codebase + pnpm lint:fix + pnpm prettier:fix + pnpm stylelint:fix + +.PHONY: lint +lint: ## Lint, or catch and remove problems, in code base + pnpm lint + pnpm prettier + pnpm stylelint --allow-empty-input + +.PHONY: release +release: ## Release the add-on on npmjs.org + pnpm release + +.PHONY: release-dry-run +release-dry-run: ## Dry-run the release of the add-on on npmjs.org + pnpm release + +.PHONY: test +test: ## Run unit tests + pnpm test + +.PHONY: ci-test +ci-test: ## Run unit tests in CI + # Unit Tests need the i18n to be built + VOLTOCONFIG=$(CURRENT_DIR)/volto.config.js pnpm --filter @plone/volto i18n +{%- if cookiecutter.__test_framework == 'jest' %} + CI=1 RAZZLE_JEST_CONFIG=$(CURRENT_DIR)/jest-addon.config.js pnpm run --filter @plone/volto test --passWithNoTests +{%- else %} + CI=1 pnpm run test --passWithNoTests +{%- endif %} + +.PHONY: backend-docker-start +backend-docker-start: ## Starts a Docker-based backend for development + @echo "$(GREEN)==> Start Docker-based Plone Backend$(RESET)" + docker run -it --rm --name=backend -p 8080:8080 -e SITE=Plone $(DOCKER_IMAGE) + +## Storybook +.PHONY: storybook-start +storybook-start: ## Start Storybook server on port 6006 + @echo "$(GREEN)==> Start Storybook$(RESET)" + pnpm run storybook + +.PHONY: storybook-build +storybook-build: ## Build Storybook + @echo "$(GREEN)==> Build Storybook$(RESET)" + mkdir -p $(CURRENT_DIR)/.storybook-build + pnpm run storybook-build -o $(CURRENT_DIR)/.storybook-build + +## Acceptance +.PHONY: acceptance-frontend-dev-start +acceptance-frontend-dev-start: ## Start acceptance frontend in development mode + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm start + +.PHONY: acceptance-frontend-prod-start +acceptance-frontend-prod-start: ## Start acceptance frontend in production mode + RAZZLE_API_PATH=http://127.0.0.1:55001/plone pnpm build && pnpm start:prod + +.PHONY: acceptance-backend-start +acceptance-backend-start: ## Start backend acceptance server + docker run -it --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: ci-acceptance-backend-start +ci-acceptance-backend-start: ## Start backend acceptance server in headless mode for CI + docker run -i --rm -p 55001:55001 $(DOCKER_IMAGE_ACCEPTANCE) + +.PHONY: acceptance-test +acceptance-test: ## Start Cypress in interactive mode + pnpm --filter @plone/volto exec cypress open --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx}' + +.PHONY: ci-acceptance-test +ci-acceptance-test: ## Run cypress tests in headless mode for CI + pnpm --filter @plone/volto exec cypress run --config-file $(CURRENT_DIR)/cypress.config.js --config specPattern=$(CURRENT_DIR)'/cypress/tests/**/*.{js,jsx,ts,tsx}' + +.PHONY: build-image +build-image: ## Build Docker Image + @DOCKER_BUILDKIT=1 docker build . -t $(IMAGE_NAME):$(IMAGE_TAG) -f Dockerfile --build-arg VOLTO_VERSION=$(VOLTO_VERSION) diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/README.md b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/README.md new file mode 100644 index 00000000..17609635 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/README.md @@ -0,0 +1,195 @@ +# {{ cookiecutter.title }} ({{ cookiecutter.__npm_package_name }}) + +{{ cookiecutter.description }} + +[![npm](https://img.shields.io/npm/v/{{ cookiecutter.__npm_package_name }})](https://www.npmjs.com/package/{{ cookiecutter.__npm_package_name }}) +[![](https://img.shields.io/badge/-Storybook-ff4785?logo=Storybook&logoColor=white&style=flat-square)](https://{{ cookiecutter.organization }}.github.io/{{ cookiecutter.frontend_addon_name }}/) +[![CI]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml/badge.svg)]({{ cookiecutter.__repository_url }}/actions/workflows/main.yml) + + +## Features + + + +## Installation + +To install your project, you must choose the method appropriate to your version of Volto. + + +### Volto 18 and later + +Add `{{ cookiecutter.__npm_package_name }}` to your `package.json`. + +```json +"dependencies": { + "{{ cookiecutter.__npm_package_name }}": "*" +} +``` + +Add `{{ cookiecutter.__npm_package_name }}` to your `volto.config.js`. + +```javascript +const addons = ['{{ cookiecutter.__npm_package_name }}']; +``` + +If this package provides a Volto theme, and you want to activate it, then add the following to your `volto.config.js`. + +```javascript +const theme = '{{ cookiecutter.__npm_package_name }}'; +``` + +### Volto 17 and earlier + +Create a new Volto project. +You can skip this step if you already have one. + +``` +npm install -g yo @plone/generator-volto +yo @plone/volto my-volto-project --addon {{ cookiecutter.__npm_package_name }} +cd my-volto-project +``` + +Add `{{ cookiecutter.__npm_package_name }}` to your `package.json`. + +```JSON +"addons": [ + "{{ cookiecutter.__npm_package_name }}" +], + +"dependencies": { + "{{ cookiecutter.__npm_package_name }}": "*" +} +``` + +Download and install the new add-on. + +``` +yarn install +``` + +Start Volto. + +``` +yarn start +``` + +## Test installation + +Visit http://localhost:3000/ in a browser, login, and check the awesome new features. + + +## Development + +The development of this add-on is done in isolation using pnpm workspaces, the latest `mrs-developer`, and other Volto core improvements. +For these reasons, it only works with pnpm and Volto 18. + + +### Prerequisites ✅ + +- An [operating system](https://6.docs.plone.org/install/create-project-cookieplone.html#prerequisites-for-installation) that runs all the requirements mentioned. +- [nvm](https://6.docs.plone.org/install/create-project-cookieplone.html#nvm) +- [Node.js and pnpm](https://6.docs.plone.org/install/create-project.html#node-js) {{ cookiecutter.__node_version }} +- [Make](https://6.docs.plone.org/install/create-project-cookieplone.html#make) +- [Git](https://6.docs.plone.org/install/create-project-cookieplone.html#git) +- [Docker](https://docs.docker.com/get-started/get-docker/) (optional) + +### Installation 🔧 + +1. Clone this repository, then change your working directory. + + ```shell + git clone {{ cookiecutter.__repository_git }}.git + cd {{ cookiecutter.__project_slug }}/frontend + ``` + +2. Install this code base. + + ```shell + make install + ``` + + +### Make convenience commands + +Run `make help` to list the available Make commands. + + +### Set up development environment + +Install package requirements. + +```shell +make install +``` + +### Start developing + +Start the backend. + +```shell +make backend-docker-start +``` + +In a separate terminal session, start the frontend. + +```shell +make start +``` + +### Lint code + +Run ESlint, Prettier, and Stylelint in analyze mode. + +```shell +make lint +``` + +### Format code + +Run ESlint, Prettier, and Stylelint in fix mode. + +```shell +make format +``` + +### i18n + +Extract the i18n messages to locales. + +```shell +make i18n +``` + +### Unit tests + +Run unit tests. + +```shell +make test +``` + +### Run Cypress tests + +Run each of these steps in separate terminal sessions. + +In the first session, start the frontend in development mode. + +```shell +make acceptance-frontend-dev-start +``` + +In the second session, start the backend acceptance server. + +```shell +make acceptance-backend-start +``` + +In the third session, start the Cypress interactive test runner. + +```shell +make acceptance-test +``` + +## License + +The project is licensed under the MIT license. diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/package.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/package.json new file mode 100644 index 00000000..ffdbc59c --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/package.json @@ -0,0 +1,59 @@ +{ + "name": "{{ cookiecutter.__npm_package_name }}-dev", + "version": "{{ cookiecutter.__version_frontend_package }}", + "description": "{{ cookiecutter.description }}", + "author": "{{ cookiecutter.author }}", + "homepage": "{{ cookiecutter.__repository_url }}", + "license": "MIT", + "keywords": [ + "volto-addon", + "volto", + "plone", + "react" + ], + "scripts": { + "preinstall": "npx only-allow pnpm", + "start": "VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto start", + "start:prod": "pnpm --filter @plone/volto start:prod", + "build": "VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build", + "build:deps": "pnpm --filter @plone/registry --filter @plone/components build", + "i18n": "pnpm --filter {{ cookiecutter.__npm_package_name }} i18n && VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto i18n", +{%- if cookiecutter.__test_framework == 'jest' %} + "test": "RAZZLE_JEST_CONFIG=$(pwd)/jest-addon.config.js pnpm --filter @plone/volto test -- --passWithNoTests", +{%- else %} + "test": "pnpm --filter {{ cookiecutter.__npm_package_name }} exec vitest", +{%- endif %} + "lint": "VOLTOCONFIG=$(pwd)/volto.config.js eslint --max-warnings=0 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "lint:fix": "VOLTOCONFIG=$(pwd)/volto.config.js eslint --fix 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "prettier": "prettier --check 'packages/**/src/**/*.{js,jsx,ts,tsx}'", + "prettier:fix": "prettier --write 'packages/**/src/**/*.{js,jsx,ts,tsx}' ", + "stylelint": "stylelint 'packages/**/src/**/*.{css,scss,less}' --allow-empty-input", + "stylelint:fix": "stylelint 'packages/**/src/**/*.{css,scss,less}' --fix --allow-empty-input", + "dry-release": "pnpm --filter {{ cookiecutter.__npm_package_name }} dry-release", + "release": "pnpm --filter {{ cookiecutter.__npm_package_name }} release", + "release-major-alpha": "pnpm --filter {{ cookiecutter.__npm_package_name }} release-major-alpha", + "release-alpha": "pnpm --filter {{ cookiecutter.__npm_package_name }} release-alpha", + "storybook": "VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto storybook dev -p 6006 -c $(pwd)/.storybook", + "storybook-build": "VOLTOCONFIG=$(pwd)/volto.config.js pnpm --filter @plone/volto build-storybook -c $(pwd)/.storybook" + }, + "dependencies": { + "@plone/volto": "workspace:*", + "@plone/registry": "workspace:*", + "{{ cookiecutter.__npm_package_name }}": "workspace:*" + }, + "devDependencies": { + "mrs-developer": "{{ cookiecutter.__version_mrs_developer }}" + }, + "pnpm": { + "overrides": { + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.15", + "react-refresh": "^0.14.2" + }{%- if cookiecutter.volto_version >= '19' %}, + "onlyBuiltDependencies": [ + "cypress", + "lightningcss-cli" + ] + {%- endif %} + }, + "packageManager": "pnpm@{{ cookiecutter.__version_pnpm }}" +} diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/package.json b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/package.json new file mode 100644 index 00000000..46b26955 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/package.json @@ -0,0 +1,41 @@ +{ + "name": "{{ cookiecutter.__npm_package_name }}", + "version": "{{ cookiecutter.__version_frontend_package }}", + "description": "{{ cookiecutter.description }}", + "main": "src/index.ts", + "license": "MIT", + "keywords": [ + "volto-addon", + "volto", + "plone", + "react" + ], + "author": "{{ cookiecutter.author }}", + "homepage": "{{ cookiecutter.__repository_url }}#readme", + "repository": { + "type": "git", + "url": "{{ cookiecutter.__repository_git }}" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "i18n": "rm -rf build/messages && NODE_ENV=production i18n --addon", + "dry-release": "release-it --dry-run", + "release": "release-it", + "release-major-alpha": "release-it major --preRelease=alpha", + "release-alpha": "release-it --preRelease=alpha" + }, + "addons": [], + "dependencies": {}, + "peerDependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@plone/registry": "workspace:*", + "@plone/types": "workspace:*" + }, + "devDependencies": { + "@plone/scripts": "workspace:*", + "release-it": "{{ cookiecutter.__version_release_it }}" + } +} diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/src/config/settings.ts b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/src/config/settings.ts new file mode 100644 index 00000000..7136ad56 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/src/config/settings.ts @@ -0,0 +1,5 @@ +import type { ConfigType } from "@plone/registry"; + +export default function install(config: ConfigType) { + return config; +} diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/towncrier.toml b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/towncrier.toml new file mode 100644 index 00000000..9b11d5f5 --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/packages/{{ cookiecutter.frontend_addon_name }}/towncrier.toml @@ -0,0 +1,33 @@ +[tool.towncrier] +filename = "CHANGELOG.md" +directory = "news/" +title_format = "## {version} ({project_date})" +underlines = ["", "", ""] +template = "./node_modules/@plone/scripts/templates/towncrier_template.jinja" +start_string = "\n" +issue_format = "[#{issue}]({{ cookiecutter.__repository_url }}/issue/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking" +showcontent = true + +[[tool.towncrier.type]] +directory = "feature" +name = "Feature" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bugfix" +showcontent = true + +[[tool.towncrier.type]] +directory = "internal" +name = "Internal" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/volto.config.js b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/volto.config.js new file mode 100644 index 00000000..5d537b5d --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/frontend/volto.config.js @@ -0,0 +1,7 @@ +const addons = ['{{ cookiecutter.__npm_package_name }}']; +const theme = ''; + +module.exports = { + addons, + theme +}; diff --git a/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/repository.toml b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/repository.toml new file mode 100644 index 00000000..4307d41c --- /dev/null +++ b/templates/sub/addon_settings/{{ cookiecutter.__folder_name }}/repository.toml @@ -0,0 +1,33 @@ +[repository] +name = "{{ cookiecutter.__project_slug }}" +changelog = "CHANGELOG.md" +version = "version.txt" +container_images_prefix = "{{ cookiecutter.__container_image_prefix }}" +compose = ["docker-compose.yml"] + +[repository.towncrier] +section = "Project" +settings = "towncrier.toml" + +[backend.package] +name = "{{ cookiecutter.python_package_name }}" +path = "backend" +code_path = "src/{{ cookiecutter.__package_path }}" +changelog = "backend/CHANGELOG.md" +towncrier_settings = "backend/pyproject.toml" +base_package = "Products.CMFPlone" +publish = true + +[frontend.package] +name = "{{ cookiecutter.frontend_addon_name }}" +path = "frontend/packages/{{ cookiecutter.frontend_addon_name }}" +code_path = "src" +changelog = "frontend/packages/{{ cookiecutter.frontend_addon_name }}/CHANGELOG.md" +towncrier_settings = "frontend/packages/{{ cookiecutter.frontend_addon_name }}/towncrier.toml" +base_package = "@plone/volto" +publish = true + +[cookieplone] +template = "{{ cookiecutter.__cookieplone_template }}" +template_version = "{{ cookiecutter.__generator_sha }}" +generated_date = "{{ cookiecutter.__generator_date_long }}" diff --git a/templates/sub/cache/cookiecutter.json b/templates/sub/cache/cookiecutter.json index d550dd01..7e9062dc 100644 --- a/templates/sub/cache/cookiecutter.json +++ b/templates/sub/cache/cookiecutter.json @@ -3,17 +3,7 @@ "project_slug": "{{ cookiecutter.title | slugify }}", "author": "Plone Foundation", "email": "collective@plone.org", - "github_organization": "collective", - "container_registry": ["github", "docker_hub", "gitlab"], "__folder_name": "{{ cookiecutter.project_slug }}", - "__container_registry_prefix": "{{ cookiecutter.container_registry | image_prefix }}", - "__container_image_prefix": "{{ cookiecutter.__container_registry_prefix }}{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}", - "__gha_version_checkout": "v4", - "__gha_version_docker_metadata": "v5", - "__gha_version_docker_qemu": "v3", - "__gha_version_docker_buildx": "v3", - "__gha_version_docker_login": "v3", - "__gha_version_docker_build_push": "v4", "__devops_varnish_version": "7.6", "_copy_without_render": [], "_extensions": [ diff --git a/templates/sub/cache/{{ cookiecutter.__folder_name }}/.github/workflows/varnish.yml b/templates/sub/cache/{{ cookiecutter.__folder_name }}/.github/workflows/varnish.yml index 10ed0c65..320f2cf9 100644 --- a/templates/sub/cache/{{ cookiecutter.__folder_name }}/.github/workflows/varnish.yml +++ b/templates/sub/cache/{{ cookiecutter.__folder_name }}/.github/workflows/varnish.yml @@ -1,69 +1,36 @@ name: Varnish Image Creation on: - push: - paths: - - "devops/varnish/**" - - ".github/workflows/varnish.yml" - workflow_dispatch: - -env: - IMAGE_NAME_PREFIX: {{ cookiecutter.__container_image_prefix }} - IMAGE_NAME_SUFFIX: varnish + workflow_call: + inputs: + base-tag: + required: true + type: string + image-name-prefix: + required: true + type: string + image-name-suffix: + required: true + type: string + working-directory: + required: false + type: string + default: devops/varnish jobs: release: - runs-on: ubuntu-latest + name: "Varnish: Build and publish Container Image" + uses: plone/meta/.github/workflows/container-image-build-push.yml@2.x permissions: contents: read packages: write - - steps: - - - name: Checkout - uses: actions/checkout@{{ cookiecutter.__gha_version_checkout }} - - - name: Docker meta - id: meta - uses: docker/metadata-action@{{ cookiecutter.__gha_version_docker_metadata }} - with: - images: | - {{ "${{ env.IMAGE_NAME_PREFIX }}-${{ env.IMAGE_NAME_SUFFIX }}" }} - labels: | - org.label-schema.docker.cmd=docker run -d -p 80:80 {{ "${{ env.IMAGE_NAME_PREFIX }}-${{ env.IMAGE_NAME_SUFFIX }}" }}:latest - flavor: - latest=false - tags: | - type=ref,event=branch - type=sha - type=raw,value=latest,enable={{ "{{is_default_branch}}" }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@{{ cookiecutter.__gha_version_docker_qemu }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@{{ cookiecutter.__gha_version_docker_buildx }} - - - name: Login to Container Registry - uses: docker/login-action@{{ cookiecutter.__gha_version_docker_login }} - with: - {%- if cookiecutter.container_registry == 'docker_hub' %} - username: {{"${{ secrets.DOCKERHUB_USERNAME }}"}} - password: {{"${{ secrets.DOCKERHUB_TOKEN }}"}} - {%- endif %} - {%- if cookiecutter.container_registry == 'github' %} - registry: ghcr.io - username: {{"${{ github.actor }}"}} - password: {{"${{ secrets.GITHUB_TOKEN }}"}} - {%- endif %} - - - name: Build and push - uses: docker/build-push-action@{{ cookiecutter.__gha_version_docker_build_push }} - with: - platforms: linux/amd64 - context: devops/varnish - file: devops/varnish/Dockerfile - push: {{"${{ github.event_name != 'pull_request' }}"}} - tags: {{"${{ steps.meta.outputs.tags }}"}} - labels: {{"${{ steps.meta.outputs.labels }}"}} + with: + base-tag: {{ "${{ inputs.base-tag }}" }} + image-name-prefix: {{ "${{ inputs.image-name-prefix }}" }} + image-name-suffix: {{ "${{ inputs.image-name-suffix }}" }} + working-directory: {{ "${{ inputs.working-directory }}" }} + push: {{ "${{ github.event_name != 'pull_request' }}" }} + secrets: + username: {{ "${{ github.actor }}" }} + password: {{ "${{ secrets.GITHUB_TOKEN }}" }} diff --git a/templates/sub/cache/{{ cookiecutter.__folder_name }}/devops/varnish/etc/varnish.vcl b/templates/sub/cache/{{ cookiecutter.__folder_name }}/devops/varnish/etc/varnish.vcl index b86acac7..90278282 100644 --- a/templates/sub/cache/{{ cookiecutter.__folder_name }}/devops/varnish/etc/varnish.vcl +++ b/templates/sub/cache/{{ cookiecutter.__folder_name }}/devops/varnish/etc/varnish.vcl @@ -14,7 +14,6 @@ backend traefik_loadbalancer { /* Only allow PURGE from localhost and API-Server */ acl purge { "localhost"; - "backend"; "127.0.0.1"; "172.16.0.0/12"; "10.0.0.0/8"; @@ -31,10 +30,12 @@ sub detect_debug{ # information about requests unset req.http.x-vcl-debug; # Should be changed after switch to live - #if (req.http.x-varnish-debug) { - # set req.http.x-vcl-debug = false; - #} - set req.http.x-vcl-debug = true; + if (req.http.x-varnish-debug) { + set req.http.x-vcl-debug = true; + } else { + set req.http.x-vcl-debug = false; + } + } sub detect_auth{ diff --git a/templates/sub/classic_project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml b/templates/sub/classic_project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml index 5b0a1af4..a35590ed 100644 --- a/templates/sub/classic_project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml +++ b/templates/sub/classic_project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml @@ -48,7 +48,7 @@ test = [ "plone.app.robotframework", "pytest", "pytest-cov", - "pytest-plone>=0.5.0", + "pytest-plone>=1.0.0a2", ] [project.urls] diff --git a/templates/sub/project_settings/cookiecutter.json b/templates/sub/project_settings/cookiecutter.json index cd4b143c..e3eb73d2 100644 --- a/templates/sub/project_settings/cookiecutter.json +++ b/templates/sub/project_settings/cookiecutter.json @@ -11,17 +11,20 @@ "use_prerelease_versions": "{{ 'No' | use_prerelease_versions }}", "plone_version": "{{ 'No' | latest_plone }}", "volto_version": "{{ cookiecutter.use_prerelease_versions | latest_volto }}", - "github_organization": "collective", - "container_registry": ["github", "docker_hub", "gitlab"], + "organization": "collective", + "repository_url": "https://github.com/{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "container_image_prefix": "{{ cookiecutter.organization }}/{{ cookiecutter.project_slug }}", + "cookieplone_template": "", + "generator_sha": "", "__feature_distribution": "0", - "__backend_managed_by_uv": "false", + "__backend_base_package_name": "Products.CMFPlone", + "__backend_base_package_version": "{{ cookiecutter.plone_version }}", "__project_slug": "{{ cookiecutter.project_slug }}", - "__repository_url": "https://github.com/{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}", - "__repository_git": "git@github.com:{{ cookiecutter.github_organization }}/{{ cookiecutter.__project_slug }}", + "__repository_url": "{{ cookiecutter.repository_url }}", + "__repository_git": "git@github.com:{{ cookiecutter.organization }}/{{ cookiecutter.__project_slug }}", + "__container_image_prefix": "{{ cookiecutter.container_image_prefix }}", "__node_version": "{{ cookiecutter.volto_version | node_version_for_volto }}", "__npm_package_name": "{{ cookiecutter.npm_package_name }}", - "__container_registry_prefix": "{{ cookiecutter.container_registry | image_prefix }}", - "__container_image_prefix": "{{ cookiecutter.__container_registry_prefix }}{{ cookiecutter.github_organization }}/{{ cookiecutter.project_slug }}", "__folder_name": "{{ cookiecutter.project_slug }}", "__package_path": "{{ cookiecutter.python_package_name | package_path }}", "__profile_language": "{{ cookiecutter.language_code|gs_language_code }}", @@ -33,8 +36,9 @@ "__version_frontend_package": "1.0.0-alpha.0", "__version_plone_volto": "{{ cookiecutter.volto_version }}", "__version_mrs_developer": "^2.2.0", - "__version_pnpm": "9.1.1", + "__version_pnpm": "{{ '10.20.0' if cookiecutter.volto_version >= '19' else '9.1.1' }}", "__version_release_it": "^17.1.1", + "__test_framework": "{{ 'vitest' if cookiecutter.volto_version >= '19' else 'jest'}}", "_copy_without_render": [], "_extensions": [ "cookieplone.filters.use_prerelease_versions", @@ -51,8 +55,8 @@ "cookieplone.filters.latest_plone" ], "__cookieplone_repository_path": "", - "__cookieplone_template": "", - "__generator_sha": "", + "__cookieplone_template": "{{ cookiecutter.cookieplone_template }}", + "__generator_sha": "{{ cookiecutter.generator_sha }}", "__generator_template_url": "https://github.com/plone/cookieplone-templates/tree/main/{{ cookiecutter.__cookieplone_template }}", "__generator_date_long": "{% now 'utc', '%Y-%m-%d %H:%M:%S' %}", "__generator_signature": "Generated from the [`cookieplone-templates` {{ cookiecutter.__cookieplone_template }} template]({{ cookiecutter.__generator_template_url }}) on {{ cookiecutter.__generator_date_long }}." diff --git a/templates/sub/project_settings/hooks/post_gen_project.py b/templates/sub/project_settings/hooks/post_gen_project.py new file mode 100644 index 00000000..6dfac525 --- /dev/null +++ b/templates/sub/project_settings/hooks/post_gen_project.py @@ -0,0 +1,41 @@ +"""Post generation hook.""" + +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path + +from cookieplone.utils import console, files + +context: OrderedDict = {{cookiecutter}} + + +def handle_remove_files(context: OrderedDict, output_dir: Path): + files_to_remove = [ + "backend/mx.ini", + "backend/src/packagename/profiles/uninstall", + "backend/tests/setup/test_setup_uninstall.py", + ] + output_dir = output_dir + files.remove_files(output_dir, files_to_remove) + + +def main(): + """Final fixes.""" + output_dir = Path().cwd() + actions = [ + [ + handle_remove_files, + "Remove unnecessary files", + True, + ], + ] + for func, title, enabled in actions: + if not int(enabled): + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + +if __name__ == "__main__": + main() diff --git a/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/Dockerfile b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/Dockerfile index 8c06545c..32f6bb51 100644 --- a/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/Dockerfile +++ b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/Dockerfile @@ -1,38 +1,56 @@ -# syntax=docker/dockerfile:1 -ARG PLONE_VERSION={{ cookiecutter.plone_version }} -FROM plone/server-builder:${PLONE_VERSION} AS builder - -WORKDIR /app - - -# Add local code -COPY scripts/ scripts/ -COPY . src +# syntax=docker/dockerfile:1.9 +ARG PYTHON_VERSION={{ cookiecutter.__python_version }} +FROM plone/server-builder:uv-${PYTHON_VERSION} AS builder + +# Install project dependencies based on uv.lock and pyproject.toml +# We expect pyproject.toml to have an entry cointainer inside [dependency-groups] +RUN --mount=type=cache,target=/root/.cache \ + --mount=type=bind,source=uv.lock,target=uv.lock \ + --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ + uv sync \ + --locked \ + --no-dev \ + --no-group test \ + --group container \ + --no-install-project + +COPY . /src +WORKDIR /src + +# Install package +RUN --mount=type=cache,target=/root/.cache \ + uv sync \ + --locked \ + --no-dev \ + --no-group test \ + --group container \ + --no-editable + +# Move skeleton files to /app +RUN < Generate constraints file$(RESET)" - @echo '-c https://dist.plone.org/release/$(PLONE_VERSION)/constraints.txt' > requirements.txt - @uvx mxdev -c mx.ini - -$(VENV_FOLDER): requirements-mxdev.txt ## Install dependencies - @echo "$(GREEN)==> Install environment$(RESET)" -ifdef CI - @uv venv $(VENV_FOLDER) -else - @uv venv --python={{ cookiecutter.__supported_versions_python[0] }} $(VENV_FOLDER) -endif - @uv pip install -r requirements-mxdev.txt +.PHONY: debug-settings +debug-settings: ## Debug settings + @echo "PLONE_VERSION: $(PLONE_VERSION)" + @echo "EXAMPLE_CONTENT_FOLDER: $(EXAMPLE_CONTENT_FOLDER)" + +.PHONY: update-constraints +update-constraints: ## Update backend constraints + @echo "$(GREEN)==> Update backend constraints$(RESET)" + @uvx repoplone deps constraints .PHONY: sync -sync: $(VENV_FOLDER) ## Sync project dependencies +sync: update-constraints ## Sync project dependencies @echo "$(GREEN)==> Sync project dependencies$(RESET)" - @uv pip install -r requirements-mxdev.txt + @uv sync + +.PHONY: install +install: sync config ## Install Plone and dependencies instance/etc/zope.ini instance/etc/zope.conf: instance.yaml ## Create instance configuration @echo "$(GREEN)==> Create instance configuration$(RESET)" @@ -73,9 +74,6 @@ instance/etc/zope.ini instance/etc/zope.conf: instance.yaml ## Create instance c .PHONY: config config: instance/etc/zope.ini -.PHONY: install -install: $(VENV_FOLDER) config ## Install Plone and dependencies - .PHONY: clean clean: ## Clean installation and instance @echo "$(RED)==> Cleaning environment and build$(RESET)" @@ -88,22 +86,22 @@ remove-data: ## Remove all content .PHONY: start start: $(VENV_FOLDER) instance/etc/zope.ini ## Start a Plone instance on localhost:8080 - @$(BIN_FOLDER)/runwsgi instance/etc/zope.ini + @uv run runwsgi instance/etc/zope.ini .PHONY: console console: $(VENV_FOLDER) instance/etc/zope.ini ## Start a console into a Plone instance - @$(BIN_FOLDER)/zconsole debug instance/etc/zope.conf + @uv run zconsole debug instance/etc/zope.conf .PHONY: create-site create-site: $(VENV_FOLDER) instance/etc/zope.ini ## Create a new site from scratch - @$(BIN_FOLDER)/zconsole run instance/etc/zope.conf ./scripts/create_site.py + @uv run zconsole run instance/etc/zope.conf ./scripts/create_site.py # Example Content .PHONY: update-example-content update-example-content: $(VENV_FOLDER) ## Export example content inside package @echo "$(GREEN)==> Export example content into $(EXAMPLE_CONTENT_FOLDER) $(RESET)" if [ -d $(EXAMPLE_CONTENT_FOLDER)/content ]; then rm -r $(EXAMPLE_CONTENT_FOLDER)/* ;fi - @$(BIN_FOLDER)/plone-exporter instance/etc/zope.conf $(PLONE_SITE_ID) $(EXAMPLE_CONTENT_FOLDER) + @uv run plone-exporter instance/etc/zope.conf $(PLONE_SITE_ID) $(EXAMPLE_CONTENT_FOLDER) # QA .PHONY: lint @@ -125,21 +123,22 @@ format: ## Check and fix code base according to Plone standards .PHONY: i18n i18n: $(VENV_FOLDER) ## Update locales @echo "$(GREEN)==> Updating locales$(RESET)" - @$(BIN_FOLDER)/python -m {{ cookiecutter.python_package_name }}.locales + @uv run python -m $(PACKAGE_NAME).locales # Tests .PHONY: test test: $(VENV_FOLDER) ## run tests - @$(BIN_FOLDER)/pytest + @uv run pytest .PHONY: test-coverage test-coverage: $(VENV_FOLDER) ## run tests with coverage - @$(BIN_FOLDER)/pytest --cov={{ cookiecutter.python_package_name }} --cov-report term-missing + @uv run pytest --cov=$(PACKAGE_NAME) --cov-report term-missing # Build Docker images .PHONY: build-image build-image: ## Build Docker Images - @docker build . -t $(IMAGE_NAME_PREFIX)-backend:$(IMAGE_TAG) -f Dockerfile --build-arg PLONE_VERSION=$(PLONE_VERSION) + @echo "$(GREEN)==> Building image $(IMAGE_NAME) $(RESET)" + @docker build . -t $(IMAGE_NAME) --progress plain -f Dockerfile # Acceptance tests .PHONY: acceptance-backend-start diff --git a/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml index 756dc885..2d4c8f07 100644 --- a/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml +++ b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/pyproject.toml @@ -24,7 +24,6 @@ classifiers = [ {%- if cookiecutter.__feature_distribution == '1' %} "Framework :: Plone :: Distribution", {%- endif %} - "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", "Operating System :: OS Independent", "Programming Language :: Python", {%- for version in cookiecutter.__supported_versions_python -%} @@ -32,37 +31,41 @@ classifiers = [ {%- endfor %} ] dependencies = [ - "Products.CMFPlone=={{ cookiecutter.plone_version }}", + "{{ cookiecutter.__backend_base_package_name }}=={{ cookiecutter.__backend_base_package_version }}", "plone.api", "plone.restapi", "plone.volto", ] -[project.optional-dependencies] +[dependency-groups] test = [ "horse-with-no-namespace", "plone.app.testing", "plone.restapi[test]", "pytest", "pytest-cov", - "pytest-plone>=0.5.0", + "pytest-plone>=1.0.0a2", ] +container = [ + "plone.app.upgrade", + "psycopg2==2.9.10", + "relstorage==4.1.1", + "zeo==6.0.0", +] + [project.urls] Homepage = "{{ cookiecutter.__repository_url }}" -PyPI = "https://pypi.org/project/{{ cookiecutter.python_package_name }}" Source = "{{ cookiecutter.__repository_url }}" Tracker = "{{ cookiecutter.__repository_url }}/issues" -{% if cookiecutter.plone_version.startswith('6.1') is true %} [project.entry-points."plone.autoinclude.plugin"] -{%- else %} -[project.entry-points."z3c.autoinclude.plugin"] -{%- endif %} target = "plone" [tool.uv] -managed = {{ cookiecutter.__backend_managed_by_uv }} +managed = true +default-groups = ["test"] +constraint-dependencies = [] [tool.hatch.version] path = "src/{{ cookiecutter.__package_path }}/__init__.py" @@ -173,14 +176,6 @@ preview = true [tool.ruff.lint.per-file-ignores] "tests/*" = ["E501", "RUF001", "S101"] -[tool.check-manifest] -ignore = [ - ".editorconfig", - ".flake8", - "dependabot.yml", - "mx.ini", -] - [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml index 78377534..4cb1c804 100644 --- a/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml +++ b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/backend/src/packagename/profiles.zcml @@ -21,14 +21,6 @@ pre_handler=".setuphandlers.initial.create_example_content" /> - - = '19' %}, + "onlyBuiltDependencies": [ + "cypress", + "lightningcss-cli" + ] + {%- endif %} }, "packageManager": "pnpm@{{ cookiecutter.__version_pnpm }}" } diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/repository.toml b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/repository.toml similarity index 82% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/repository.toml rename to templates/sub/project_settings/{{ cookiecutter.__folder_name }}/repository.toml index df63a519..2c132380 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/repository.toml +++ b/templates/sub/project_settings/{{ cookiecutter.__folder_name }}/repository.toml @@ -1,31 +1,28 @@ [repository] name = "{{ cookiecutter.__project_slug }}" -managed_by_uv = {{ cookiecutter.__backend_managed_by_uv }} changelog = "CHANGELOG.md" version = "version.txt" -compose = "docker-compose.yml" +version_format = "calver" +container_images_prefix = "{{ cookiecutter.__container_image_prefix }}" +compose = ["docker-compose.yml"] [repository.towncrier] section = "Project" settings = "towncrier.toml" -[backend] -path = "backend" - [backend.package] name = "{{ cookiecutter.python_package_name }}" path = "backend" +code_path = "src/{{ cookiecutter.__package_path }}" changelog = "backend/CHANGELOG.md" towncrier_settings = "backend/pyproject.toml" base_package = "Products.CMFPlone" publish = false -[frontend] -path = "frontend" - [frontend.package] name = "{{ cookiecutter.frontend_addon_name }}" path = "frontend/packages/{{ cookiecutter.frontend_addon_name }}" +code_path = "src" changelog = "frontend/packages/{{ cookiecutter.frontend_addon_name }}/CHANGELOG.md" towncrier_settings = "frontend/packages/{{ cookiecutter.frontend_addon_name }}/towncrier.toml" base_package = "@plone/volto" diff --git a/templates/sub/vscode/Makefile b/templates/sub/vscode/Makefile new file mode 100644 index 00000000..299a1a30 --- /dev/null +++ b/templates/sub/vscode/Makefile @@ -0,0 +1,57 @@ +SHELL := /bin/bash +CURRENT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + + +# We like colors +# From: https://coderwall.com/p/izxssa/colored-makefile-for-golang-projects +RED=`tput setaf 1` +GREEN=`tput setaf 2` +RESET=`tput sgr0` +YELLOW=`tput setaf 3` + +.PHONY: all +all: build + +BASE_FOLDER = $(shell git rev-parse --show-toplevel) +VENV_FOLDER = ${BASE_FOLDER}/.venv +BIN_FOLDER = ${VENV_FOLDER}/bin + +TEMPLATE = sub/$(shell basename $(CURRENT_DIR)) +PROJECT_FOLDER_NAME = .vscode + + + +# Add the following 'help' target to your Makefile +# And add help text after each target name starting with '\#\#' +.PHONY: help +help: ## This help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +.PHONY: clean +clean: ## Clean + rm -rf $(PROJECT_FOLDER_NAME) + +$(VENV_FOLDER): ## cookieplone installation + $(MAKE) -C $(BASE_FOLDER) sync + +.PHONY: format +format: $(VENV_FOLDER)## Format code + @echo "$(GREEN)==> Formatting $(TEMPLATE) codebase $(RESET)" + @uv run ruff format --config $(BASE_FOLDER)/pyproject.toml hooks + @uv run ruff check --select I --fix --config $(BASE_FOLDER)/pyproject.toml hooks + +.PHONY: generate +generate: $(VENV_FOLDER) ## Create a sample package + @echo "$(GREEN)==> Creating new test package$(RESET)" + rm -rf $(PROJECT_FOLDER_NAME) + COOKIEPLONE_REPOSITORY=$(BASE_FOLDER) uv run cookieplone $(TEMPLATE) --no-input + +.PHONY: test +test: $(VENV_FOLDER)## Create a sample package and tests it + @echo "$(GREEN)==> Test template$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/sub/$(TEMPLATE) + +.PHONY: test-pdb +test-pdb: $(VENV_FOLDER)## Stop on the first failed test + @echo "$(GREEN)==> Test template, stop on first error$(RESET)" + @uv run pytest $(BASE_FOLDER)/tests/templates/sub/$(TEMPLATE) -x --pdb diff --git a/templates/sub/vscode/cookiecutter.json b/templates/sub/vscode/cookiecutter.json new file mode 100644 index 00000000..63b7924e --- /dev/null +++ b/templates/sub/vscode/cookiecutter.json @@ -0,0 +1,20 @@ +{ + "folder_name": ".vscode", + "is_monorepo": ["1", "0"], + "has_backend": ["1", "0"], + "has_frontend": ["1", "0"], + "has_github": ["1", "0"], + "cookieplone_template": "sub/vscode", + "__folder_name": "{{ cookiecutter.folder_name }}", + "_copy_without_render": [], + "_extensions": [ + ], + "__cookieplone_repository_path": "", + "__path_backend": "{{ 'backend' if cookiecutter.is_monorepo == '1' else './' }}", + "__path_frontend": "{{ 'frontend' if cookiecutter.is_monorepo == '1' else './' }}", + "__cookieplone_template": "{{ cookiecutter.cookieplone_template }}", + "__generator_sha": "", + "__generator_template_url": "https://github.com/plone/cookieplone-templates/tree/main/{{ cookiecutter.__cookieplone_template }}", + "__generator_date_long": "{% now 'utc', '%Y-%m-%d %H:%M:%S' %}", + "__generator_signature": "This was generated by [the cookieplone-templates {{ cookiecutter.__cookieplone_template }} template]({{ cookiecutter.__generator_template_url }}) on {{ cookiecutter.__generator_date_long }}" +} diff --git a/templates/sub/vscode/hooks/post_gen_project.py b/templates/sub/vscode/hooks/post_gen_project.py new file mode 100644 index 00000000..5fac50a5 --- /dev/null +++ b/templates/sub/vscode/hooks/post_gen_project.py @@ -0,0 +1,37 @@ +"""Post generation hook.""" + +from collections import OrderedDict +from copy import deepcopy +from pathlib import Path + +from cookieplone.utils import console, files + +context: OrderedDict = {{cookiecutter}} + + +def handle_remove_files(context: OrderedDict, output_dir: Path): + files_to_remove = [] + output_dir = output_dir + files.remove_files(output_dir, files_to_remove) + + +def main(): + """Final fixes.""" + output_dir = Path().cwd() + actions = [ + [ + handle_remove_files, + "Remove unnecessary files", + True, + ], + ] + for func, title, enabled in actions: + if not int(enabled): + continue + new_context = deepcopy(context) + console.print(f" -> {title}") + func(new_context, output_dir) + + +if __name__ == "__main__": + main() diff --git a/templates/sub/vscode/hooks/pre_gen_project.py b/templates/sub/vscode/hooks/pre_gen_project.py new file mode 100644 index 00000000..6a2e382b --- /dev/null +++ b/templates/sub/vscode/hooks/pre_gen_project.py @@ -0,0 +1,17 @@ +"""Pre generation hook.""" + +from collections import OrderedDict +from pathlib import Path + +output_path = Path().resolve() + +context: OrderedDict = {{cookiecutter}} + + +def main(): + """Validate context.""" + pass + + +if __name__ == "__main__": + main() diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.vscode/extensions.json b/templates/sub/vscode/{{ cookiecutter.__folder_name }}/extensions.json similarity index 55% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.vscode/extensions.json rename to templates/sub/vscode/{{ cookiecutter.__folder_name }}/extensions.json index 77be9b1d..f0c0f27b 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.vscode/extensions.json +++ b/templates/sub/vscode/{{ cookiecutter.__folder_name }}/extensions.json @@ -1,15 +1,22 @@ { "recommendations": [ + "plone.plone-vs-utilities", + "editorconfig.editorconfig", + "ExecutableBookProject.myst-highlight", +{%- if cookiecutter.has_backend == '1' %} "charliermarsh.ruff", + "ms-python.python", +{%- endif %} +{%- if cookiecutter.has_frontend == '1' %} "dbaeumer.vscode-eslint", - "editorconfig.editorconfig", "esbenp.prettier-vscode", - "ExecutableBookProject.myst-highlight", + "stylelint.vscode-stylelint", +{%- endif %} +{%- if cookiecutter.has_github == '1' %} "github.vscode-github-actions", - "ms-python.python", +{%- endif %} + "ms-azuretools.vscode-containers", "ms-vscode-remote.remote-containers", - "redhat.vscode-yaml", - "stylelint.vscode-stylelint", - "plone.plone-vs-utilities" + "redhat.vscode-yaml" ] } \ No newline at end of file diff --git a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.vscode/settings.json b/templates/sub/vscode/{{ cookiecutter.__folder_name }}/settings.json similarity index 69% rename from templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.vscode/settings.json rename to templates/sub/vscode/{{ cookiecutter.__folder_name }}/settings.json index 4664ff97..ccaabd4f 100644 --- a/templates/projects/monorepo/{{ cookiecutter.__folder_name }}/.vscode/settings.json +++ b/templates/sub/vscode/{{ cookiecutter.__folder_name }}/settings.json @@ -1,16 +1,11 @@ { - "eslint.workingDirectories": ["./frontend"], + "files.encoding": "utf8", +{%- if cookiecutter.has_frontend == '1' %} + "eslint.workingDirectories": ["./{{ cookiecutter.__path_frontend }}"], "stylelint.enable": true, "css.validate": false, "less.validate": false, "scss.validate": false, - "ruff.organizeImports": true, - "python.terminal.activateEnvironment": true, - "python.testing.pytestArgs": [ - "backend/tests" - ], - "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true, "[css][less][scss]": { "editor.tabSize": 2, "editor.formatOnSave": true, @@ -19,6 +14,16 @@ }, "editor.defaultFormatter": "esbenp.prettier-vscode" }, +{%- endif %} +{%- if cookiecutter.has_backend == '1' %} + "ruff.organizeImports": true, + "python.terminal.activateEnvironment": true, + "python.testing.pytestArgs": [ + "{{ cookiecutter.__path_backend }}/tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, +{%- endif %} "[markdown]": { "editor.formatOnSave": false } diff --git a/tests/cookieplone-templates/test_templates.py b/tests/cookieplone-templates/test_templates.py index 1c6726af..f5cbc087 100644 --- a/tests/cookieplone-templates/test_templates.py +++ b/tests/cookieplone-templates/test_templates.py @@ -2,7 +2,7 @@ def test_total_templates(all_templates): - assert len(all_templates) == 9 + assert len(all_templates) == 14 def test_all_templates_should_be_listed(all_templates, templates_by_path): @@ -29,6 +29,7 @@ def test_all_templates_should_be_listed(all_templates, templates_by_path): "Project settings to be applied on top of a Classic UI project", True, ), + ("devops_ansible", "Ansible Playbooks for Plone", True), ], ) def test_template_settings( diff --git a/tests/templates/add-ons/frontend/test_frontend_package_json.py b/tests/templates/add-ons/frontend/test_frontend_package_json.py index 150bc21e..5d2b3a3e 100644 --- a/tests/templates/add-ons/frontend/test_frontend_package_json.py +++ b/tests/templates/add-ons/frontend/test_frontend_package_json.py @@ -38,8 +38,8 @@ def test_frontend_root_settings(traverse, cutter_result, load_config, path, expe ("name", "@plone-collective/volto-addon"), ("version", "1.0.0-alpha.0"), ("main", "src/index.ts"), - ("peerDependencies/'@plone/registry'", "workspace:*"), - ("peerDependencies/'@plone/types'", "workspace:*"), + ("devDependencies/'@plone/registry'", "workspace:*"), + ("devDependencies/'@plone/types'", "workspace:*"), ("devDependencies/'@plone/scripts'", "workspace:*"), ], ) diff --git a/tests/templates/ci/github/conftest.py b/tests/templates/ci/github/conftest.py new file mode 100644 index 00000000..f7587677 --- /dev/null +++ b/tests/templates/ci/github/conftest.py @@ -0,0 +1,41 @@ +"""Pytest configuration.""" + +import pytest + + +@pytest.fixture(scope="module") +def template_folder() -> str: + return "ci/github" + + +@pytest.fixture(scope="session") +def context(annotate_context, cookieplone_root) -> dict: + """Cookiecutter context.""" + return annotate_context( + { + "title": "Project Title", + "folder_name": ".github", + "organization": "collective", + "project_slug": "project-title", + "repository_url": "https://github.com/collective/project-title", + "hostname": "project.example.com", + "has_backend": "1", + "has_docs": "1", + "has_frontend": "1", + "has_varnish": "1", + "has_gha_deploy": "1", + }, + cookieplone_root, + "ci_github", + ) + + +@pytest.fixture(scope="session") +def bad_context() -> dict: + """Cookiecutter context with invalid data.""" + return { + "title": " ", + "author": "Plone Collective", + "email": "collective@plone.org", + "volto_version": "---", + } diff --git a/tests/templates/ci/github/test_ci_github_cutter.py b/tests/templates/ci/github/test_ci_github_cutter.py new file mode 100644 index 00000000..6c0162d2 --- /dev/null +++ b/tests/templates/ci/github/test_ci_github_cutter.py @@ -0,0 +1,62 @@ +"""Test cookiecutter generation with all features enabled.""" + +import pytest + + +def test_creation(cookies, template_path, context: dict): + """Generated project should match provided value.""" + result = cookies.bake(extra_context=context, template=template_path) + assert result.exception is None + assert result.exit_code == 0 + assert result.project_path.name == ".github" + assert result.project_path.is_dir() + + +def test_variable_substitution(build_files_list, variable_pattern, cutter_result): + """Check if no file was unprocessed.""" + paths = build_files_list(cutter_result.project_path) + for path in paths: + with open(path) as fh: + for line in fh: + match = {pattern.search(line) for pattern in variable_pattern} + msg = f"cookiecutter variable not replaced in {path}" + assert match == {None}, msg + + +@pytest.mark.parametrize( + "file_path,schema_name", + [ + ["workflows/backend.yml", "github-workflow"], + ["workflows/frontend.yml", "github-workflow"], + ["workflows/config.yml", "github-workflow"], + ["workflows/docs.yml", "github-workflow"], + ["workflows/main.yml", "github-workflow"], + ["workflows/manual_deploy.yml", "github-workflow"], + ["workflows/rtd-pr-preview.yml", "github-workflow"], + ["workflows/varnish.yml", "github-workflow"], + ], +) +def test_json_schema( + cutter_result, schema_validate_file, file_path: str, schema_name: str +): + path = cutter_result.project_path / file_path + assert schema_validate_file(path, schema_name) + + +@pytest.mark.parametrize( + "file_path", + [ + "workflows/backend.yml", + "workflows/frontend.yml", + "workflows/config.yml", + "workflows/docs.yml", + "workflows/main.yml", + "workflows/manual_deploy.yml", + "workflows/rtd-pr-preview.yml", + "workflows/varnish.yml", + ], +) +def test_created_files(cutter_result, file_path: str): + path = (cutter_result.project_path / file_path).resolve() + assert path.exists() + assert path.is_file() diff --git a/tests/templates/ci/github/test_ci_github_variables.py b/tests/templates/ci/github/test_ci_github_variables.py new file mode 100644 index 00000000..bf6b345f --- /dev/null +++ b/tests/templates/ci/github/test_ci_github_variables.py @@ -0,0 +1,19 @@ +ALLOWED_MISSING = [] +ALLOWED_NOT_USED = [ + "__generator_sha", + "__generator_signature", + "has_gha_deploy", + "repository_url", +] + + +def test_no_missing_variables(variables_missing): + """Test no variable is missing from cookiecutter.json""" + assert len(variables_missing) == len(ALLOWED_MISSING) + assert variables_missing == ALLOWED_MISSING + + +def test_not_used_variables(variables_not_used): + """Test variables are used.""" + assert len(variables_not_used) == len(ALLOWED_NOT_USED) + assert variables_not_used == ALLOWED_NOT_USED diff --git a/tests/templates/devops/ansible/__init__.py b/tests/templates/devops/ansible/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/templates/devops/ansible/conftest.py b/tests/templates/devops/ansible/conftest.py new file mode 100644 index 00000000..75d5b0a2 --- /dev/null +++ b/tests/templates/devops/ansible/conftest.py @@ -0,0 +1,81 @@ +"""Pytest configuration for devops_ansible template tests.""" + +from copy import deepcopy + +import pytest + +# List of expected files in the generated output +EXPECTED_FILES = [ + ".gitignore", + ".vale.ini", + "LICENSE.md", + "Makefile", + "pyproject.toml", + "README.md", + "docs/_static/favicon.ico", + "docs/_static/logo.svg", + "docs/_templates/404.html", + "docs/concepts/index.md", + "docs/how-to-guides/index.md", + "docs/reference/index.md", + "docs/tutorials/index.md", + "docs/.readthedocs.yaml", + "docs/conf.py", + "docs/glossary.md", + "docs/index.md", + "docs/robots.txt", + "styles/config/vocabularies/Plone/accept.txt", + "styles/config/vocabularies/Plone/reject.txt", + "styles/config/vocabularies/Base/accept.txt", + "styles/config/vocabularies/Base/reject.txt", +] + + +@pytest.fixture(scope="module") +def template_folder() -> str: + """Path to the documentation_starter template directory.""" + return "docs/starter" + + +@pytest.fixture(scope="session") +def context() -> dict[str, str]: + """Default context for baking the template.""" + return { + "title": "Ansible", + "project_description": "Ansible setup", + "folder_name": "ansible", + "author_name": "Plone Community", + "author_email": "collective@plone.org", + "github_organization": "collective", + "__folder_name": "ansible", + "__repository_url": "https://github.com/collective/ansible", + "initialize_git": "1", + } + + +@pytest.fixture(scope="session") +def no_git_context(default_context) -> dict[str, str]: + """Context with Git initialization disabled.""" + context = deepcopy(default_context) + context["__folder_name"] = "collective.docsnogit" + context["initialize_git"] = "0" + return context + + +@pytest.fixture(scope="session") +def bad_context() -> dict: + """Cookiecutter context with invalid data.""" + return { + "title": "Addon", + "description": "A Tech blog.", + "github_organization": "collective", + "python_package_name": "collective_addon", + "author": "Plone Collective", + "email": "collective@plone.org", + "feature_headless": "1", + } + + +def pytest_generate_tests(metafunc): + if "root_file_path" in metafunc.fixturenames: + metafunc.parametrize("root_file_path", EXPECTED_FILES) diff --git a/tests/templates/devops/ansible/test_devops_cutter.py b/tests/templates/devops/ansible/test_devops_cutter.py new file mode 100644 index 00000000..3cde90f2 --- /dev/null +++ b/tests/templates/devops/ansible/test_devops_cutter.py @@ -0,0 +1,19 @@ +"""Test cookiecutter generation with all features enabled.""" + +from pathlib import Path + + +def test_creation(cookies, template_path, context: dict): + """Generated project should match provided value.""" + result = cookies.bake(extra_context=context, template=template_path) + assert result.exception is None + assert result.exit_code == 0 + assert result.project_path.is_dir() + + +def test_git_initialization(cutter_result): + from cookieplone.utils import git + + path = cutter_result.project_path + repo = git.repo_from_path(path) + assert Path(repo.working_dir) == path diff --git a/tests/templates/devops/ansible/test_devops_variables.py b/tests/templates/devops/ansible/test_devops_variables.py new file mode 100644 index 00000000..42c04d0f --- /dev/null +++ b/tests/templates/devops/ansible/test_devops_variables.py @@ -0,0 +1,16 @@ +ALLOWED_MISSING = [] +ALLOWED_NOT_USED = [ + "__repository_git", +] + + +def test_no_missing_variables(variables_missing): + """Test no variable is missing from cookiecutter.json""" + assert len(variables_missing) == len(ALLOWED_MISSING) + assert variables_missing == ALLOWED_MISSING + + +def test_not_used_variables(variables_not_used): + """Test variables are used.""" + assert len(variables_not_used) == len(ALLOWED_NOT_USED) + assert variables_not_used == ALLOWED_NOT_USED diff --git a/tests/templates/projects/classic/test_classic_variables.py b/tests/templates/projects/classic/test_classic_variables.py index ad0c307f..18225e00 100644 --- a/tests/templates/projects/classic/test_classic_variables.py +++ b/tests/templates/projects/classic/test_classic_variables.py @@ -1,4 +1,9 @@ -ALLOWED_MISSING = ["feature_headless", "initialize_git"] +ALLOWED_MISSING = [ + "configuration_version", + "feature_headless", + "initial_version", + "initialize_git", +] ALLOWED_NOT_USED = ["__devops_host"] diff --git a/tests/templates/projects/monorepo/test_project_backend.py b/tests/templates/projects/monorepo/test_project_backend.py index e63685ec..3a6e25a7 100644 --- a/tests/templates/projects/monorepo/test_project_backend.py +++ b/tests/templates/projects/monorepo/test_project_backend.py @@ -11,7 +11,6 @@ "Dockerfile", "instance.yaml", "Makefile", - "mx.ini", "pyproject.toml", "version.txt", ] @@ -36,7 +35,6 @@ def test_backend_top_level_files(cutter_result, filename: str): "src/plonegov/ploneorgbr/testing.py", "tests/conftest.py", "tests/setup/test_setup_install.py", - "tests/setup/test_setup_uninstall.py", ] @@ -52,6 +50,9 @@ def test_backend_package_files_pytest(cutter_result, filename: str): ".github", ".git", ".meta.toml", + "mx.ini", + "src/plonegov/ploneorgbr/profiles/uninstall", + "tests/setup/test_setup_uninstall.py", ] diff --git a/tests/templates/projects/monorepo/test_project_devops.py b/tests/templates/projects/monorepo/test_project_devops.py index fa78d69b..05883d0d 100644 --- a/tests/templates/projects/monorepo/test_project_devops.py +++ b/tests/templates/projects/monorepo/test_project_devops.py @@ -4,34 +4,54 @@ import yaml ANSIBLE_FILES = [ - "devops/.env_dist", - "devops/.gitignore", - "devops/ansible.cfg", - "devops/etc/docker/daemon/daemon.json.j2", - "devops/etc/docker/systemd/http-proxy.conf.j2", - "devops/etc/keys/.gitkeep", - "devops/inventory/group_vars/all/base.yml", - "devops/inventory/group_vars/all/docker.yml", - "devops/inventory/group_vars/all/packages.yml", - "devops/inventory/group_vars/all/projects.yml", - "devops/inventory/group_vars/all/sshd.yml", - "devops/inventory/group_vars/all/swap.yml", - "devops/inventory/group_vars/all/ufw.yml", - "devops/inventory/group_vars/all/users.yml", - "devops/inventory/hosts.yml", - "devops/Makefile", - "devops/playbooks/setup.yml", - "devops/README.md", - "devops/requirements/collections.yml", - "devops/requirements/requirements.txt", - "devops/requirements/roles.yml", - "devops/tasks/base/task_base_packages.yml", - "devops/tasks/base/task_base_python.yml", - "devops/tasks/base/task_docker.yml", - "devops/tasks/base/task_hostname.yml", - "devops/tasks/base/task_ufw.yml", - "devops/tasks/handlers/common.yml", - "devops/tasks/swarm/task_swarm.yml", + "devops/ansible/.ansible-lint", + "devops/ansible/.editorconfig", + "devops/ansible/.env_dist", + "devops/ansible/.gitignore", + "devops/ansible/.vault_pass", + "devops/ansible/ansible.cfg", + "devops/ansible/etc/.ssh/.gitkeep", + "devops/ansible/etc/.ssh/ansible-ssh-config", + "devops/ansible/etc/base/apt_proxy.j2", + "devops/ansible/etc/base/environment.j2", + "devops/ansible/etc/docker/systemd/http-proxy.conf.j2", + "devops/ansible/etc/keys/.gitkeep", + "devops/ansible/etc/ssh/default_ssh_config.j2", + "devops/ansible/etc/stacks/cronjob.yml", + "devops/ansible/etc/stacks/traefik.yml", + "devops/ansible/inventory/group_vars/all/base.yml", + "devops/ansible/inventory/group_vars/all/disks.yml", + "devops/ansible/inventory/group_vars/all/docker.yml", + "devops/ansible/inventory/group_vars/all/packages.yml", + "devops/ansible/inventory/group_vars/all/proxy.yml", + "devops/ansible/inventory/group_vars/all/sshd.yml", + "devops/ansible/inventory/group_vars/all/stacks.yml", + "devops/ansible/inventory/group_vars/all/swap.yml", + "devops/ansible/inventory/group_vars/all/swarm.yml", + "devops/ansible/inventory/group_vars/all/ufw.yml", + "devops/ansible/inventory/group_vars/all/users.yml", + "devops/ansible/inventory/group_vars/all/vault.yml", + "devops/ansible/inventory/hosts.yml", + "devops/ansible/Makefile", + "devops/ansible/playbooks/_connect.yml", + "devops/ansible/playbooks/deploy.yml", + "devops/ansible/playbooks/setup.yml", + "devops/ansible/playbooks/stacks.yml", + "devops/ansible/pyproject.toml", + "devops/ansible/README.md", + "devops/ansible/requirements.yml", + "devops/ansible/tasks/base/task_base_packages.yml", + "devops/ansible/tasks/base/task_hostname.yml", + "devops/ansible/tasks/base/task_mount_points.yml", + "devops/ansible/tasks/base/task_proxy.yml", + "devops/ansible/tasks/base/task_ssh.yml", + "devops/ansible/tasks/base/task_ufw.yml", + "devops/ansible/tasks/base/task_user.yml", + "devops/ansible/tasks/docker/task_setup.yml", + "devops/ansible/tasks/docker/task_stack.yml", + "devops/ansible/tasks/docker/task_swarm.yml", + "devops/ansible/tasks/handlers/common.yml", + "devops/ansible/tasks/stacks/task_deploy.yml", ] GHA_ACTIONS_CI = [ @@ -92,8 +112,8 @@ def test_project_devops_no_gha_deploy( assert path.exists() is False -def test_ansible_inventory_projects_replacement(cutter_result): +def test_ansible_inventory_stacks_replacement(cutter_result): """Test GHA deploy files are not present.""" folder = cutter_result.project_path - path = folder / "devops/inventory/group_vars/all/projects.yml" + path = folder / "devops/ansible/inventory/group_vars/all/stacks.yml" assert "{{ cookiecutter.hostname }}" not in path.read_text() diff --git a/tests/templates/projects/monorepo/test_project_generation.py b/tests/templates/projects/monorepo/test_project_generation.py index f444014b..c8b11ba0 100644 --- a/tests/templates/projects/monorepo/test_project_generation.py +++ b/tests/templates/projects/monorepo/test_project_generation.py @@ -26,6 +26,19 @@ def test_project_files(cutter_result, filepath: str): assert path.is_file() +def test_version(cutter_result): + """Test project version.""" + from datetime import date + + expected_version = date.today().strftime("%Y%m%d.0") + folder = cutter_result.project_path + path = folder / "version.txt" + assert path.is_file() + version = path.read_text().strip() + assert len(version) == 10 # YYYYMMDD.X + assert version == expected_version + + @pytest.mark.parametrize( "filepath", [f for f in TOP_LEVEL_FILES if f.endswith(".json")] diff --git a/tests/templates/projects/monorepo/test_project_repository_toml.py b/tests/templates/projects/monorepo/test_project_repository_toml.py index 0ea7e378..a6e8eaac 100644 --- a/tests/templates/projects/monorepo/test_project_repository_toml.py +++ b/tests/templates/projects/monorepo/test_project_repository_toml.py @@ -18,6 +18,8 @@ def repository_settings(cutter_result) -> dict: "key,expected", [ ["repository.name", "plone.org.br"], + ["repository.version_format", "calver"], + ["repository.container_images_prefix", "ghcr.io/collective/plone.org.br"], ["repository.towncrier.section", "Project"], ["repository.towncrier.settings", "towncrier.toml"], ["backend.package.name", "plonegov.ploneorgbr"], diff --git a/tests/templates/projects/monorepo/test_project_variables.py b/tests/templates/projects/monorepo/test_project_variables.py index c8bdf2a2..2a641bc8 100644 --- a/tests/templates/projects/monorepo/test_project_variables.py +++ b/tests/templates/projects/monorepo/test_project_variables.py @@ -1,4 +1,25 @@ -ALLOWED_MISSING = ["feature_headless", "initialize_git", "npm_package_name"] +ALLOWED_MISSING = [ + "configuration_version", + "container_image_prefix", + "cookieplone_template", + "feature_headless", + "folder_name", + "generator_sha", + "github_organization", + "has_backend", + "has_docs", + "has_frontend", + "has_github", + "has_varnish", + "hostname_or_ip", + "initial_version", + "initialize_git", + "is_monorepo", + "npm_package_name", + "stack_location", + "stack_name", + "stack_prefix", +] ALLOWED_NOT_USED = [] diff --git a/tests/templates/sub/cache/test_sub_cache_cutter.py b/tests/templates/sub/cache/test_sub_cache_cutter.py index d86b830c..ec0d2357 100644 --- a/tests/templates/sub/cache/test_sub_cache_cutter.py +++ b/tests/templates/sub/cache/test_sub_cache_cutter.py @@ -39,7 +39,6 @@ def test_json_schema( @pytest.mark.parametrize( "file_path", [ - ".github/workflows/varnish.yml", "backend/src/packagename/profiles/default/registry/plone.cachepurging.interfaces.ICachePurgingSettings.xml", "backend/src/packagename/profiles/default/registry/plone.caching.interfaces.ICacheSettings.xml", "devops/varnish/etc/varnish.vcl", diff --git a/tests/templates/sub/project_settings/test_sub_project_settings_cutter.py b/tests/templates/sub/project_settings/test_sub_project_settings_cutter.py index 6b56575f..b62730ff 100644 --- a/tests/templates/sub/project_settings/test_sub_project_settings_cutter.py +++ b/tests/templates/sub/project_settings/test_sub_project_settings_cutter.py @@ -66,6 +66,19 @@ def test_created_files(cutter_result, file_path: str): assert path.is_file() +REMOVED_FILES = [ + "backend/src/packagename/profiles/uninstall/browserlayer.xml", + "backend/tests/setup/test_setup_uninstall.py", + "backend/mx.ini", +] + + +@pytest.mark.parametrize("file_path", REMOVED_FILES) +def test_removed_files(cutter_result, file_path: str): + path = (cutter_result.project_path / file_path).resolve() + assert not path.exists() + + @pytest.mark.parametrize( "file_path", [f for f in GENERATED_FILES if f.endswith(".json")] ) diff --git a/tests/templates/sub/project_settings/test_sub_project_settings_variables.py b/tests/templates/sub/project_settings/test_sub_project_settings_variables.py index 5e0cf791..935e5e98 100644 --- a/tests/templates/sub/project_settings/test_sub_project_settings_variables.py +++ b/tests/templates/sub/project_settings/test_sub_project_settings_variables.py @@ -1,5 +1,5 @@ ALLOWED_MISSING = [] -ALLOWED_NOT_USED = ["__generator_sha", "__version_plone_volto"] +ALLOWED_NOT_USED = ["__version_plone_volto"] def test_no_missing_variables(variables_missing): diff --git a/uv.lock b/uv.lock index 69d67e59..5a0a5bc6 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.10" [[package]] @@ -10,18 +10,18 @@ dependencies = [ { name = "python-dateutil" }, { name = "types-python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, ] [[package]] name = "attrs" version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] [[package]] @@ -32,9 +32,9 @@ dependencies = [ { name = "soupsieve" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516 } +sdist = { url = "https://files.pythonhosted.org/packages/f0/3c/adaf39ce1fb4afdd21b611e3d530b183bb7759c9b673d60db0e347fd4439/beautifulsoup4-4.13.3.tar.gz", hash = "sha256:1bd32405dacc920b42b83ba01644747ed77456a65760e285fbc47633ceddaf8b", size = 619516, upload-time = "2025-02-04T20:05:01.681Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015 }, + { url = "https://files.pythonhosted.org/packages/f9/49/6abb616eb3cbab6a7cca303dc02fdf3836de2e0b834bf966a7f5271a34d8/beautifulsoup4-4.13.3-py3-none-any.whl", hash = "sha256:99045d7d3f08f91f0d656bc9b7efbae189426cd913d830294a15eefa0ea4df16", size = 186015, upload-time = "2025-02-04T20:05:03.729Z" }, ] [[package]] @@ -44,9 +44,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chardet" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a7/fe/7ebfec74d49f97fc55cd38240c7a7d08134002b1e14be8c3897c0dd5e49b/binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", size = 371054 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/fe/7ebfec74d49f97fc55cd38240c7a7d08134002b1e14be8c3897c0dd5e49b/binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061", size = 371054, upload-time = "2017-08-03T15:55:25.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006 }, + { url = "https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4", size = 9006, upload-time = "2017-08-03T15:55:31.23Z" }, ] [[package]] @@ -62,100 +62,100 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a2/47/c9997eb470a7f48f7aaddd3d9a828244a2e4199569e38128715c48059ac1/black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", size = 642299 } +sdist = { url = "https://files.pythonhosted.org/packages/a2/47/c9997eb470a7f48f7aaddd3d9a828244a2e4199569e38128715c48059ac1/black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", size = 642299, upload-time = "2024-04-26T00:32:15.305Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/f6/3adc48c210527a7b651aaed43824a9b8bd04b3fb361a5227bad046e1c876/black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", size = 1631487 }, - { url = "https://files.pythonhosted.org/packages/a2/25/70aa1bec12c841a03e333e312daa0cf2fee50ea6336ac4851c93c0e2b411/black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", size = 1456317 }, - { url = "https://files.pythonhosted.org/packages/e0/7d/7f8df0fdbbbefc4362d3eca6b69b7a8a4249a8a88dabc00a207d31fddcd7/black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", size = 1822765 }, - { url = "https://files.pythonhosted.org/packages/5c/21/1ee97841c469c1551133cbe47448cdba9628c7d9431f74f114f02e3b233c/black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", size = 1409336 }, - { url = "https://files.pythonhosted.org/packages/9b/f7/591d601c3046ceb65b97291dfe87fa25124cffac3d97aaaba89d0f0d7bdf/black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", size = 1615013 }, - { url = "https://files.pythonhosted.org/packages/c9/17/5e0036b265bbf6bc44970d93d48febcbc03701b671db3c9603fd43ebc616/black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", size = 1436163 }, - { url = "https://files.pythonhosted.org/packages/c5/48/34176b522e8cff4620a5d96c2e323ff2413f574870eb25efa8025885e028/black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", size = 1803382 }, - { url = "https://files.pythonhosted.org/packages/74/ce/e8eec1a77edbfa982bee3b5460dcdd4fe0e4e3165fc15d8ec44d04da7776/black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", size = 1417802 }, - { url = "https://files.pythonhosted.org/packages/f4/75/3a29de3bda4006cc280d833b5d961cf7df3810a21f49e7a63a7e551fb351/black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", size = 1645176 }, - { url = "https://files.pythonhosted.org/packages/be/b8/9c152301774fa62a265b035a8ede4d6280827904ea1af8c3be10a28d3187/black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", size = 1446227 }, - { url = "https://files.pythonhosted.org/packages/25/6d/eb15a1b155f755f43766cc473618c6e1de6555d6a1764965643f486dcf01/black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", size = 1832011 }, - { url = "https://files.pythonhosted.org/packages/43/24/942b22571b0171be7c6f701cdc3e3b7221f5b522ef02cf82503a547a657b/black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", size = 1428800 }, - { url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", size = 205925 }, + { url = "https://files.pythonhosted.org/packages/40/f6/3adc48c210527a7b651aaed43824a9b8bd04b3fb361a5227bad046e1c876/black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", size = 1631487, upload-time = "2024-04-26T00:40:28.969Z" }, + { url = "https://files.pythonhosted.org/packages/a2/25/70aa1bec12c841a03e333e312daa0cf2fee50ea6336ac4851c93c0e2b411/black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", size = 1456317, upload-time = "2024-04-26T00:39:10.333Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/7f8df0fdbbbefc4362d3eca6b69b7a8a4249a8a88dabc00a207d31fddcd7/black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", size = 1822765, upload-time = "2024-04-26T00:34:56.436Z" }, + { url = "https://files.pythonhosted.org/packages/5c/21/1ee97841c469c1551133cbe47448cdba9628c7d9431f74f114f02e3b233c/black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", size = 1409336, upload-time = "2024-04-26T00:35:30.392Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f7/591d601c3046ceb65b97291dfe87fa25124cffac3d97aaaba89d0f0d7bdf/black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", size = 1615013, upload-time = "2024-04-26T00:39:49.415Z" }, + { url = "https://files.pythonhosted.org/packages/c9/17/5e0036b265bbf6bc44970d93d48febcbc03701b671db3c9603fd43ebc616/black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", size = 1436163, upload-time = "2024-04-26T00:40:20.267Z" }, + { url = "https://files.pythonhosted.org/packages/c5/48/34176b522e8cff4620a5d96c2e323ff2413f574870eb25efa8025885e028/black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", size = 1803382, upload-time = "2024-04-26T00:34:38.665Z" }, + { url = "https://files.pythonhosted.org/packages/74/ce/e8eec1a77edbfa982bee3b5460dcdd4fe0e4e3165fc15d8ec44d04da7776/black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", size = 1417802, upload-time = "2024-04-26T00:35:08.804Z" }, + { url = "https://files.pythonhosted.org/packages/f4/75/3a29de3bda4006cc280d833b5d961cf7df3810a21f49e7a63a7e551fb351/black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", size = 1645176, upload-time = "2024-04-26T00:42:35.606Z" }, + { url = "https://files.pythonhosted.org/packages/be/b8/9c152301774fa62a265b035a8ede4d6280827904ea1af8c3be10a28d3187/black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", size = 1446227, upload-time = "2024-04-26T00:40:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/25/6d/eb15a1b155f755f43766cc473618c6e1de6555d6a1764965643f486dcf01/black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", size = 1832011, upload-time = "2024-04-26T00:34:37.825Z" }, + { url = "https://files.pythonhosted.org/packages/43/24/942b22571b0171be7c6f701cdc3e3b7221f5b522ef02cf82503a547a657b/black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", size = 1428800, upload-time = "2024-04-26T00:35:55.838Z" }, + { url = "https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", size = 205925, upload-time = "2024-04-26T00:32:12.495Z" }, ] [[package]] name = "certifi" version = "2025.1.31" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" }, ] [[package]] name = "chardet" version = "5.2.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385 }, + { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, ] [[package]] name = "charset-normalizer" version = "3.4.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013, upload-time = "2024-12-24T18:09:43.671Z" }, + { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285, upload-time = "2024-12-24T18:09:48.113Z" }, + { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449, upload-time = "2024-12-24T18:09:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892, upload-time = "2024-12-24T18:09:52.078Z" }, + { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123, upload-time = "2024-12-24T18:09:54.575Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943, upload-time = "2024-12-24T18:09:57.324Z" }, + { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063, upload-time = "2024-12-24T18:09:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578, upload-time = "2024-12-24T18:10:02.357Z" }, + { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629, upload-time = "2024-12-24T18:10:03.678Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778, upload-time = "2024-12-24T18:10:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453, upload-time = "2024-12-24T18:10:08.848Z" }, + { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479, upload-time = "2024-12-24T18:10:10.044Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790, upload-time = "2024-12-24T18:10:11.323Z" }, + { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995, upload-time = "2024-12-24T18:10:12.838Z" }, + { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471, upload-time = "2024-12-24T18:10:14.101Z" }, + { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831, upload-time = "2024-12-24T18:10:15.512Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335, upload-time = "2024-12-24T18:10:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862, upload-time = "2024-12-24T18:10:19.743Z" }, + { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673, upload-time = "2024-12-24T18:10:21.139Z" }, + { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211, upload-time = "2024-12-24T18:10:22.382Z" }, + { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039, upload-time = "2024-12-24T18:10:24.802Z" }, + { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939, upload-time = "2024-12-24T18:10:26.124Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075, upload-time = "2024-12-24T18:10:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340, upload-time = "2024-12-24T18:10:32.679Z" }, + { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205, upload-time = "2024-12-24T18:10:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441, upload-time = "2024-12-24T18:10:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" }, ] [[package]] @@ -165,18 +165,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] @@ -193,14 +193,14 @@ dependencies = [ { name = "requests" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/17/9f2cd228eb949a91915acd38d3eecdc9d8893dde353b603f0db7e9f6be55/cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c", size = 158767 } +sdist = { url = "https://files.pythonhosted.org/packages/52/17/9f2cd228eb949a91915acd38d3eecdc9d8893dde353b603f0db7e9f6be55/cookiecutter-2.6.0.tar.gz", hash = "sha256:db21f8169ea4f4fdc2408d48ca44859349de2647fbe494a9d6c3edfc0542c21c", size = 158767, upload-time = "2024-02-21T18:02:41.949Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", size = 39177 }, + { url = "https://files.pythonhosted.org/packages/b6/d9/0137658a353168ffa9d0fc14b812d3834772040858ddd1cb6eeaf09f7a44/cookiecutter-2.6.0-py3-none-any.whl", hash = "sha256:a54a8e37995e4ed963b3e82831072d1ad4b005af736bb17b99c2cbd9d41b6e2d", size = 39177, upload-time = "2024-02-21T18:02:39.569Z" }, ] [[package]] name = "cookieplone" -version = "0.9.7" +version = "0.9.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "black" }, @@ -214,9 +214,9 @@ dependencies = [ { name = "xmltodict" }, { name = "zpretty" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/af/5b/54cc74d2d8a56bb8742a805c9df1bc748f2e3e45ef534b4f7da8544f7978/cookieplone-0.9.7.tar.gz", hash = "sha256:1c6dbb4e67a04c3f82bc7120973ac4336dbfb91aa2400898e8719c446467531a", size = 34400 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/7d/d89dc5439a527ae582a62bdbd4fce13e6817a1df17d073018a0a2e983a2a/cookieplone-0.9.10.tar.gz", hash = "sha256:906380403ce7cb539d85961523a17c3feee3250f38809441cc211c2351b3b472", size = 35235, upload-time = "2025-10-21T17:52:08.994Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/f8/f165a6f5b3db9dbf82ac33e1f71e884b66fc06ad3d632ffda2265b32e1fc/cookieplone-0.9.7-py3-none-any.whl", hash = "sha256:cf36a7be942efb252fddb210ffdd1e052539a2cdab408cf001915f54404a01d1", size = 29548 }, + { url = "https://files.pythonhosted.org/packages/f3/97/8a86f1f73ecb222079306e56316b1f43fe846cfe84bab9ccee7d43111f91/cookieplone-0.9.10-py3-none-any.whl", hash = "sha256:f60c43c941638acd570b7e4a0824fa3a586a0d35311a38ca643ba369d25d747c", size = 29787, upload-time = "2025-10-21T17:52:07.259Z" }, ] [[package]] @@ -235,11 +235,11 @@ dependencies = [ [package.metadata] requires-dist = [ - { name = "cookieplone", specifier = ">=0.9.7" }, + { name = "cookieplone", specifier = ">=0.9.10" }, { name = "gitpython", specifier = ">=3.1.43" }, { name = "pytest", specifier = ">=8.3.5" }, { name = "pytest-cookies", specifier = ">=0.7.0" }, - { name = "pytest-jsonschema", specifier = ">=1.0.0b1" }, + { name = "pytest-jsonschema", specifier = ">=1.0.0" }, { name = "pytest-md-report", specifier = ">=0.6.3" }, { name = "tomli", specifier = ">=2.0.1" }, ] @@ -252,18 +252,18 @@ dependencies = [ { name = "mbstrdecoder" }, { name = "typepy", extra = ["datetime"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0b/81/8c8b64ae873cb9014815214c07b63b12e3b18835780fb342223cfe3fe7d8/dataproperty-1.1.0.tar.gz", hash = "sha256:b038437a4097d1a1c497695c3586ea34bea67fdd35372b9a50f30bf044d77d04", size = 42574 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/81/8c8b64ae873cb9014815214c07b63b12e3b18835780fb342223cfe3fe7d8/dataproperty-1.1.0.tar.gz", hash = "sha256:b038437a4097d1a1c497695c3586ea34bea67fdd35372b9a50f30bf044d77d04", size = 42574, upload-time = "2024-12-31T14:37:26.033Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c2/e12e95e289e6081a40454199ab213139ef16a528c7c86432de545b05a23a/DataProperty-1.1.0-py3-none-any.whl", hash = "sha256:c61fcb2e2deca35e6d1eb1f251a7f22f0dcde63e80e61f0cc18c19f42abfd25b", size = 27581 }, + { url = "https://files.pythonhosted.org/packages/21/c2/e12e95e289e6081a40454199ab213139ef16a528c7c86432de545b05a23a/DataProperty-1.1.0-py3-none-any.whl", hash = "sha256:c61fcb2e2deca35e6d1eb1f251a7f22f0dcde63e80e61f0cc18c19f42abfd25b", size = 27581, upload-time = "2024-12-31T14:37:22.657Z" }, ] [[package]] name = "exceptiongroup" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883, upload-time = "2024-07-12T22:26:00.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453, upload-time = "2024-07-12T22:25:58.476Z" }, ] [[package]] @@ -273,9 +273,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "smmap" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, ] [[package]] @@ -285,36 +285,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/a1/106fd9fa2dd989b6fb36e5893961f82992cf676381707253e0bf93eb1662/GitPython-3.1.43.tar.gz", hash = "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", size = 214149, upload-time = "2024-03-31T08:07:34.154Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337 }, + { url = "https://files.pythonhosted.org/packages/e9/bd/cc3a402a6439c15c3d4294333e13042b915bbeab54edc457c723931fed3f/GitPython-3.1.43-py3-none-any.whl", hash = "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff", size = 207337, upload-time = "2024-03-31T08:07:31.194Z" }, ] [[package]] name = "idna" version = "3.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, ] [[package]] name = "iniconfig" version = "2.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, ] [[package]] name = "isort" version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } +sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955, upload-time = "2025-02-26T21:13:16.955Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, + { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] [[package]] @@ -324,9 +324,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] [[package]] @@ -339,9 +339,9 @@ dependencies = [ { name = "referencing" }, { name = "rpds-py" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778 } +sdist = { url = "https://files.pythonhosted.org/packages/38/2e/03362ee4034a4c917f697890ccd4aec0800ccf9ded7f511971c75451deec/jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", size = 325778, upload-time = "2024-07-08T18:40:05.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462 }, + { url = "https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566", size = 88462, upload-time = "2024-07-08T18:40:00.165Z" }, ] [[package]] @@ -351,91 +351,91 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "referencing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561 } +sdist = { url = "https://files.pythonhosted.org/packages/10/db/58f950c996c793472e336ff3655b13fbcf1e3b359dcf52dcf3ed3b52c352/jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272", size = 15561, upload-time = "2024-10-08T12:29:32.068Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459 }, + { url = "https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf", size = 18459, upload-time = "2024-10-08T12:29:30.439Z" }, ] [[package]] name = "lxml" version = "5.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/c15ca8e5646e937c148e147244817672cf920b56ac0bf2cc1512ae674be8/lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", size = 3678591 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/4b/73426192004a643c11a644ed2346dbe72da164c8e775ea2e70f60e63e516/lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b", size = 8142766 }, - { url = "https://files.pythonhosted.org/packages/30/c2/3b28f642b43fdf9580d936e8fdd3ec43c01a97ecfe17fd67f76ce9099752/lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b", size = 4422744 }, - { url = "https://files.pythonhosted.org/packages/1f/a5/45279e464174b99d72d25bc018b097f9211c0925a174ca582a415609f036/lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5", size = 5229609 }, - { url = "https://files.pythonhosted.org/packages/f0/e7/10cd8b9e27ffb6b3465b76604725b67b7c70d4e399750ff88de1b38ab9eb/lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3", size = 4943509 }, - { url = "https://files.pythonhosted.org/packages/ce/54/2d6f634924920b17122445136345d44c6d69178c9c49e161aa8f206739d6/lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c", size = 5561495 }, - { url = "https://files.pythonhosted.org/packages/a2/fe/7f5ae8fd1f357fcb21b0d4e20416fae870d654380b6487adbcaaf0df9b31/lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2", size = 4998970 }, - { url = "https://files.pythonhosted.org/packages/af/70/22fecb6f2ca8dc77d14ab6be3cef767ff8340040bc95dca384b5b1cb333a/lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac", size = 5114205 }, - { url = "https://files.pythonhosted.org/packages/63/91/21619cc14f7fd1de3f1bdf86cc8106edacf4d685b540d658d84247a3a32a/lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9", size = 4940823 }, - { url = "https://files.pythonhosted.org/packages/50/0f/27183248fa3cdd2040047ceccd320ff1ed1344167f38a4ac26aed092268b/lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9", size = 5585725 }, - { url = "https://files.pythonhosted.org/packages/c6/8d/9b7388d5b23ed2f239a992a478cbd0ce313aaa2d008dd73c4042b190b6a9/lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79", size = 5082641 }, - { url = "https://files.pythonhosted.org/packages/65/8e/590e20833220eac55b6abcde71d3ae629d38ac1c3543bcc2bfe1f3c2f5d1/lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2", size = 5161219 }, - { url = "https://files.pythonhosted.org/packages/4e/77/cabdf5569fd0415a88ebd1d62d7f2814e71422439b8564aaa03e7eefc069/lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51", size = 5019293 }, - { url = "https://files.pythonhosted.org/packages/49/bd/f0b6d50ea7b8b54aaa5df4410cb1d5ae6ffa016b8e0503cae08b86c24674/lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406", size = 5651232 }, - { url = "https://files.pythonhosted.org/packages/fa/69/1793d00a4e3da7f27349edb5a6f3da947ed921263cd9a243fab11c6cbc07/lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5", size = 5489527 }, - { url = "https://files.pythonhosted.org/packages/d3/c9/e2449129b6cb2054c898df8d850ea4dadd75b4c33695a6c4b0f35082f1e7/lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0", size = 5227050 }, - { url = "https://files.pythonhosted.org/packages/ed/63/e5da540eba6ab9a0d4188eeaa5c85767b77cafa8efeb70da0593d6cd3b81/lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23", size = 3475345 }, - { url = "https://files.pythonhosted.org/packages/08/71/853a3ad812cd24c35b7776977cb0ae40c2b64ff79ad6d6c36c987daffc49/lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c", size = 3805093 }, - { url = "https://files.pythonhosted.org/packages/57/bb/2faea15df82114fa27f2a86eec220506c532ee8ce211dff22f48881b353a/lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f", size = 8161781 }, - { url = "https://files.pythonhosted.org/packages/9f/d3/374114084abb1f96026eccb6cd48b070f85de82fdabae6c2f1e198fa64e5/lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607", size = 4432571 }, - { url = "https://files.pythonhosted.org/packages/0f/fb/44a46efdc235c2dd763c1e929611d8ff3b920c32b8fcd9051d38f4d04633/lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8", size = 5028919 }, - { url = "https://files.pythonhosted.org/packages/3b/e5/168ddf9f16a90b590df509858ae97a8219d6999d5a132ad9f72427454bed/lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a", size = 4769599 }, - { url = "https://files.pythonhosted.org/packages/f9/0e/3e2742c6f4854b202eb8587c1f7ed760179f6a9fcb34a460497c8c8f3078/lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27", size = 5369260 }, - { url = "https://files.pythonhosted.org/packages/b8/03/b2f2ab9e33c47609c80665e75efed258b030717e06693835413b34e797cb/lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666", size = 4842798 }, - { url = "https://files.pythonhosted.org/packages/93/ad/0ecfb082b842358c8a9e3115ec944b7240f89821baa8cd7c0cb8a38e05cb/lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79", size = 4917531 }, - { url = "https://files.pythonhosted.org/packages/64/5b/3e93d8ebd2b7eb984c2ad74dfff75493ce96e7b954b12e4f5fc34a700414/lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65", size = 4791500 }, - { url = "https://files.pythonhosted.org/packages/91/83/7dc412362ee7a0259c7f64349393262525061fad551a1340ef92c59d9732/lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709", size = 5404557 }, - { url = "https://files.pythonhosted.org/packages/1e/41/c337f121d9dca148431f246825e021fa1a3f66a6b975deab1950530fdb04/lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629", size = 4931386 }, - { url = "https://files.pythonhosted.org/packages/a5/73/762c319c4906b3db67e4abc7cfe7d66c34996edb6d0e8cb60f462954d662/lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33", size = 4982124 }, - { url = "https://files.pythonhosted.org/packages/c1/e7/d1e296cb3b3b46371220a31350730948d7bea41cc9123c5fd219dea33c29/lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295", size = 4852742 }, - { url = "https://files.pythonhosted.org/packages/df/90/4adc854475105b93ead6c0c736f762d29371751340dcf5588cfcf8191b8a/lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c", size = 5457004 }, - { url = "https://files.pythonhosted.org/packages/f0/0d/39864efbd231c13eb53edee2ab91c742c24d2f93efe2af7d3fe4343e42c1/lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c", size = 5298185 }, - { url = "https://files.pythonhosted.org/packages/8d/7a/630a64ceb1088196de182e2e33b5899691c3e1ae21af688e394208bd6810/lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff", size = 5032707 }, - { url = "https://files.pythonhosted.org/packages/b2/3d/091bc7b592333754cb346c1507ca948ab39bc89d83577ac8f1da3be4dece/lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2", size = 3474288 }, - { url = "https://files.pythonhosted.org/packages/12/8c/7d47cfc0d04fd4e3639ec7e1c96c2561d5e890eb900de8f76eea75e0964a/lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6", size = 3815031 }, - { url = "https://files.pythonhosted.org/packages/3b/f4/5121aa9ee8e09b8b8a28cf3709552efe3d206ca51a20d6fa471b60bb3447/lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c", size = 8191889 }, - { url = "https://files.pythonhosted.org/packages/0a/ca/8e9aa01edddc74878f4aea85aa9ab64372f46aa804d1c36dda861bf9eabf/lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe", size = 4450685 }, - { url = "https://files.pythonhosted.org/packages/b2/b3/ea40a5c98619fbd7e9349df7007994506d396b97620ced34e4e5053d3734/lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9", size = 5051722 }, - { url = "https://files.pythonhosted.org/packages/3a/5e/375418be35f8a695cadfe7e7412f16520e62e24952ed93c64c9554755464/lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a", size = 4786661 }, - { url = "https://files.pythonhosted.org/packages/79/7c/d258eaaa9560f6664f9b426a5165103015bee6512d8931e17342278bad0a/lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0", size = 5311766 }, - { url = "https://files.pythonhosted.org/packages/03/bc/a041415be4135a1b3fdf017a5d873244cc16689456166fbdec4b27fba153/lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7", size = 4836014 }, - { url = "https://files.pythonhosted.org/packages/32/88/047f24967d5e3fc97848ea2c207eeef0f16239cdc47368c8b95a8dc93a33/lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae", size = 4961064 }, - { url = "https://files.pythonhosted.org/packages/3d/b5/ecf5a20937ecd21af02c5374020f4e3a3538e10a32379a7553fca3d77094/lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519", size = 4778341 }, - { url = "https://files.pythonhosted.org/packages/a4/05/56c359e07275911ed5f35ab1d63c8cd3360d395fb91e43927a2ae90b0322/lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322", size = 5345450 }, - { url = "https://files.pythonhosted.org/packages/b7/f4/f95e3ae12e9f32fbcde00f9affa6b0df07f495117f62dbb796a9a31c84d6/lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468", size = 4908336 }, - { url = "https://files.pythonhosted.org/packages/c5/f8/309546aec092434166a6e11c7dcecb5c2d0a787c18c072d61e18da9eba57/lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367", size = 4986049 }, - { url = "https://files.pythonhosted.org/packages/71/1c/b951817cb5058ca7c332d012dfe8bc59dabd0f0a8911ddd7b7ea8e41cfbd/lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd", size = 4860351 }, - { url = "https://files.pythonhosted.org/packages/31/23/45feba8dae1d35fcca1e51b051f59dc4223cbd23e071a31e25f3f73938a8/lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c", size = 5421580 }, - { url = "https://files.pythonhosted.org/packages/61/69/be245d7b2dbef81c542af59c97fcd641fbf45accf2dc1c325bae7d0d014c/lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f", size = 5285778 }, - { url = "https://files.pythonhosted.org/packages/69/06/128af2ed04bac99b8f83becfb74c480f1aa18407b5c329fad457e08a1bf4/lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645", size = 5054455 }, - { url = "https://files.pythonhosted.org/packages/8a/2d/f03a21cf6cc75cdd083563e509c7b6b159d761115c4142abb5481094ed8c/lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5", size = 3486315 }, - { url = "https://files.pythonhosted.org/packages/2b/9c/8abe21585d20ef70ad9cec7562da4332b764ed69ec29b7389d23dfabcea0/lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf", size = 3816925 }, - { url = "https://files.pythonhosted.org/packages/94/1c/724931daa1ace168e0237b929e44062545bf1551974102a5762c349c668d/lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e", size = 8171881 }, - { url = "https://files.pythonhosted.org/packages/67/0c/857b8fb6010c4246e66abeebb8639eaabba60a6d9b7c606554ecc5cbf1ee/lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd", size = 4440394 }, - { url = "https://files.pythonhosted.org/packages/61/72/c9e81de6a000f9682ccdd13503db26e973b24c68ac45a7029173237e3eed/lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7", size = 5037860 }, - { url = "https://files.pythonhosted.org/packages/24/26/942048c4b14835711b583b48cd7209bd2b5f0b6939ceed2381a494138b14/lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414", size = 4782513 }, - { url = "https://files.pythonhosted.org/packages/e2/65/27792339caf00f610cc5be32b940ba1e3009b7054feb0c4527cebac228d4/lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e", size = 5305227 }, - { url = "https://files.pythonhosted.org/packages/18/e1/25f7aa434a4d0d8e8420580af05ea49c3e12db6d297cf5435ac0a054df56/lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1", size = 4829846 }, - { url = "https://files.pythonhosted.org/packages/fe/ed/faf235e0792547d24f61ee1448159325448a7e4f2ab706503049d8e5df19/lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5", size = 4949495 }, - { url = "https://files.pythonhosted.org/packages/e5/e1/8f572ad9ed6039ba30f26dd4c2c58fb90f79362d2ee35ca3820284767672/lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423", size = 4773415 }, - { url = "https://files.pythonhosted.org/packages/a3/75/6b57166b9d1983dac8f28f354e38bff8d6bcab013a241989c4d54c72701b/lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20", size = 5337710 }, - { url = "https://files.pythonhosted.org/packages/cc/71/4aa56e2daa83bbcc66ca27b5155be2f900d996f5d0c51078eaaac8df9547/lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8", size = 4897362 }, - { url = "https://files.pythonhosted.org/packages/65/10/3fa2da152cd9b49332fd23356ed7643c9b74cad636ddd5b2400a9730d12b/lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9", size = 4977795 }, - { url = "https://files.pythonhosted.org/packages/de/d2/e1da0f7b20827e7b0ce934963cb6334c1b02cf1bb4aecd218c4496880cb3/lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c", size = 4858104 }, - { url = "https://files.pythonhosted.org/packages/a5/35/063420e1b33d3308f5aa7fcbdd19ef6c036f741c9a7a4bd5dc8032486b27/lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b", size = 5416531 }, - { url = "https://files.pythonhosted.org/packages/c3/83/93a6457d291d1e37adfb54df23498101a4701834258c840381dd2f6a030e/lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5", size = 5273040 }, - { url = "https://files.pythonhosted.org/packages/39/25/ad4ac8fac488505a2702656550e63c2a8db3a4fd63db82a20dad5689cecb/lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252", size = 5050951 }, - { url = "https://files.pythonhosted.org/packages/82/74/f7d223c704c87e44b3d27b5e0dde173a2fcf2e89c0524c8015c2b3554876/lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78", size = 3485357 }, - { url = "https://files.pythonhosted.org/packages/80/83/8c54533b3576f4391eebea88454738978669a6cad0d8e23266224007939d/lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332", size = 3814484 }, - { url = "https://files.pythonhosted.org/packages/d2/b4/89a68d05f267f05cc1b8b2f289a8242955705b1b0a9d246198227817ee46/lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725", size = 3936118 }, - { url = "https://files.pythonhosted.org/packages/7f/0d/c034a541e7a1153527d7880c62493a74f2277f38e64de2480cadd0d4cf96/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d", size = 4233690 }, - { url = "https://files.pythonhosted.org/packages/35/5c/38e183c2802f14fbdaa75c3266e11d0ca05c64d78e8cdab2ee84e954a565/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84", size = 4349569 }, - { url = "https://files.pythonhosted.org/packages/18/5b/14f93b359b3c29673d5d282bc3a6edb3a629879854a77541841aba37607f/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2", size = 4236731 }, - { url = "https://files.pythonhosted.org/packages/f6/08/8471de65f3dee70a3a50e7082fd7409f0ac7a1ace777c13fca4aea1a5759/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877", size = 4373119 }, - { url = "https://files.pythonhosted.org/packages/83/29/00b9b0322a473aee6cda87473401c9abb19506cd650cc69a8aa38277ea74/lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499", size = 3487718 }, +sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/c15ca8e5646e937c148e147244817672cf920b56ac0bf2cc1512ae674be8/lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", size = 3678591, upload-time = "2025-02-10T07:51:41.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/4b/73426192004a643c11a644ed2346dbe72da164c8e775ea2e70f60e63e516/lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b", size = 8142766, upload-time = "2025-02-10T07:43:29.188Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/3b28f642b43fdf9580d936e8fdd3ec43c01a97ecfe17fd67f76ce9099752/lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b", size = 4422744, upload-time = "2025-02-10T07:43:34.135Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a5/45279e464174b99d72d25bc018b097f9211c0925a174ca582a415609f036/lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5", size = 5229609, upload-time = "2025-02-10T07:43:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e7/10cd8b9e27ffb6b3465b76604725b67b7c70d4e399750ff88de1b38ab9eb/lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3", size = 4943509, upload-time = "2025-02-10T07:43:40.989Z" }, + { url = "https://files.pythonhosted.org/packages/ce/54/2d6f634924920b17122445136345d44c6d69178c9c49e161aa8f206739d6/lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c", size = 5561495, upload-time = "2025-02-10T07:43:44.054Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fe/7f5ae8fd1f357fcb21b0d4e20416fae870d654380b6487adbcaaf0df9b31/lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2", size = 4998970, upload-time = "2025-02-10T07:43:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/af/70/22fecb6f2ca8dc77d14ab6be3cef767ff8340040bc95dca384b5b1cb333a/lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac", size = 5114205, upload-time = "2025-02-10T07:43:52.251Z" }, + { url = "https://files.pythonhosted.org/packages/63/91/21619cc14f7fd1de3f1bdf86cc8106edacf4d685b540d658d84247a3a32a/lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9", size = 4940823, upload-time = "2025-02-10T07:43:55.024Z" }, + { url = "https://files.pythonhosted.org/packages/50/0f/27183248fa3cdd2040047ceccd320ff1ed1344167f38a4ac26aed092268b/lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9", size = 5585725, upload-time = "2025-02-10T07:44:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c6/8d/9b7388d5b23ed2f239a992a478cbd0ce313aaa2d008dd73c4042b190b6a9/lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79", size = 5082641, upload-time = "2025-02-10T07:44:04.015Z" }, + { url = "https://files.pythonhosted.org/packages/65/8e/590e20833220eac55b6abcde71d3ae629d38ac1c3543bcc2bfe1f3c2f5d1/lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2", size = 5161219, upload-time = "2025-02-10T07:44:07.81Z" }, + { url = "https://files.pythonhosted.org/packages/4e/77/cabdf5569fd0415a88ebd1d62d7f2814e71422439b8564aaa03e7eefc069/lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51", size = 5019293, upload-time = "2025-02-10T07:44:12.974Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/f0b6d50ea7b8b54aaa5df4410cb1d5ae6ffa016b8e0503cae08b86c24674/lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406", size = 5651232, upload-time = "2025-02-10T07:44:16.421Z" }, + { url = "https://files.pythonhosted.org/packages/fa/69/1793d00a4e3da7f27349edb5a6f3da947ed921263cd9a243fab11c6cbc07/lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5", size = 5489527, upload-time = "2025-02-10T07:44:19.479Z" }, + { url = "https://files.pythonhosted.org/packages/d3/c9/e2449129b6cb2054c898df8d850ea4dadd75b4c33695a6c4b0f35082f1e7/lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0", size = 5227050, upload-time = "2025-02-10T07:44:23.247Z" }, + { url = "https://files.pythonhosted.org/packages/ed/63/e5da540eba6ab9a0d4188eeaa5c85767b77cafa8efeb70da0593d6cd3b81/lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23", size = 3475345, upload-time = "2025-02-10T07:44:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/853a3ad812cd24c35b7776977cb0ae40c2b64ff79ad6d6c36c987daffc49/lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c", size = 3805093, upload-time = "2025-02-10T07:44:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/57/bb/2faea15df82114fa27f2a86eec220506c532ee8ce211dff22f48881b353a/lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f", size = 8161781, upload-time = "2025-02-10T07:44:34.288Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d3/374114084abb1f96026eccb6cd48b070f85de82fdabae6c2f1e198fa64e5/lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607", size = 4432571, upload-time = "2025-02-10T07:44:38.8Z" }, + { url = "https://files.pythonhosted.org/packages/0f/fb/44a46efdc235c2dd763c1e929611d8ff3b920c32b8fcd9051d38f4d04633/lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8", size = 5028919, upload-time = "2025-02-10T07:44:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e5/168ddf9f16a90b590df509858ae97a8219d6999d5a132ad9f72427454bed/lxml-5.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a", size = 4769599, upload-time = "2025-02-10T07:44:47.903Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0e/3e2742c6f4854b202eb8587c1f7ed760179f6a9fcb34a460497c8c8f3078/lxml-5.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27", size = 5369260, upload-time = "2025-02-10T07:44:51.614Z" }, + { url = "https://files.pythonhosted.org/packages/b8/03/b2f2ab9e33c47609c80665e75efed258b030717e06693835413b34e797cb/lxml-5.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666", size = 4842798, upload-time = "2025-02-10T07:44:55.296Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/0ecfb082b842358c8a9e3115ec944b7240f89821baa8cd7c0cb8a38e05cb/lxml-5.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79", size = 4917531, upload-time = "2025-02-10T07:44:58.935Z" }, + { url = "https://files.pythonhosted.org/packages/64/5b/3e93d8ebd2b7eb984c2ad74dfff75493ce96e7b954b12e4f5fc34a700414/lxml-5.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65", size = 4791500, upload-time = "2025-02-10T07:45:01.668Z" }, + { url = "https://files.pythonhosted.org/packages/91/83/7dc412362ee7a0259c7f64349393262525061fad551a1340ef92c59d9732/lxml-5.3.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709", size = 5404557, upload-time = "2025-02-10T07:45:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/1e/41/c337f121d9dca148431f246825e021fa1a3f66a6b975deab1950530fdb04/lxml-5.3.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629", size = 4931386, upload-time = "2025-02-10T07:45:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/a5/73/762c319c4906b3db67e4abc7cfe7d66c34996edb6d0e8cb60f462954d662/lxml-5.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33", size = 4982124, upload-time = "2025-02-10T07:45:11.931Z" }, + { url = "https://files.pythonhosted.org/packages/c1/e7/d1e296cb3b3b46371220a31350730948d7bea41cc9123c5fd219dea33c29/lxml-5.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295", size = 4852742, upload-time = "2025-02-10T07:45:14.737Z" }, + { url = "https://files.pythonhosted.org/packages/df/90/4adc854475105b93ead6c0c736f762d29371751340dcf5588cfcf8191b8a/lxml-5.3.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c", size = 5457004, upload-time = "2025-02-10T07:45:18.322Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0d/39864efbd231c13eb53edee2ab91c742c24d2f93efe2af7d3fe4343e42c1/lxml-5.3.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c", size = 5298185, upload-time = "2025-02-10T07:45:21.147Z" }, + { url = "https://files.pythonhosted.org/packages/8d/7a/630a64ceb1088196de182e2e33b5899691c3e1ae21af688e394208bd6810/lxml-5.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff", size = 5032707, upload-time = "2025-02-10T07:45:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/b2/3d/091bc7b592333754cb346c1507ca948ab39bc89d83577ac8f1da3be4dece/lxml-5.3.1-cp311-cp311-win32.whl", hash = "sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2", size = 3474288, upload-time = "2025-02-10T07:45:33.991Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/7d47cfc0d04fd4e3639ec7e1c96c2561d5e890eb900de8f76eea75e0964a/lxml-5.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6", size = 3815031, upload-time = "2025-02-10T07:45:37.695Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f4/5121aa9ee8e09b8b8a28cf3709552efe3d206ca51a20d6fa471b60bb3447/lxml-5.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c", size = 8191889, upload-time = "2025-02-10T07:45:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ca/8e9aa01edddc74878f4aea85aa9ab64372f46aa804d1c36dda861bf9eabf/lxml-5.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe", size = 4450685, upload-time = "2025-02-10T07:45:44.175Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b3/ea40a5c98619fbd7e9349df7007994506d396b97620ced34e4e5053d3734/lxml-5.3.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9", size = 5051722, upload-time = "2025-02-10T07:45:46.981Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/375418be35f8a695cadfe7e7412f16520e62e24952ed93c64c9554755464/lxml-5.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a", size = 4786661, upload-time = "2025-02-10T07:45:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/79/7c/d258eaaa9560f6664f9b426a5165103015bee6512d8931e17342278bad0a/lxml-5.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0", size = 5311766, upload-time = "2025-02-10T07:45:54.049Z" }, + { url = "https://files.pythonhosted.org/packages/03/bc/a041415be4135a1b3fdf017a5d873244cc16689456166fbdec4b27fba153/lxml-5.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7", size = 4836014, upload-time = "2025-02-10T07:45:57.699Z" }, + { url = "https://files.pythonhosted.org/packages/32/88/047f24967d5e3fc97848ea2c207eeef0f16239cdc47368c8b95a8dc93a33/lxml-5.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae", size = 4961064, upload-time = "2025-02-10T07:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b5/ecf5a20937ecd21af02c5374020f4e3a3538e10a32379a7553fca3d77094/lxml-5.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519", size = 4778341, upload-time = "2025-02-10T07:46:03.661Z" }, + { url = "https://files.pythonhosted.org/packages/a4/05/56c359e07275911ed5f35ab1d63c8cd3360d395fb91e43927a2ae90b0322/lxml-5.3.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322", size = 5345450, upload-time = "2025-02-10T07:46:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f4/f95e3ae12e9f32fbcde00f9affa6b0df07f495117f62dbb796a9a31c84d6/lxml-5.3.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468", size = 4908336, upload-time = "2025-02-10T07:46:14.338Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f8/309546aec092434166a6e11c7dcecb5c2d0a787c18c072d61e18da9eba57/lxml-5.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367", size = 4986049, upload-time = "2025-02-10T07:46:18.217Z" }, + { url = "https://files.pythonhosted.org/packages/71/1c/b951817cb5058ca7c332d012dfe8bc59dabd0f0a8911ddd7b7ea8e41cfbd/lxml-5.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd", size = 4860351, upload-time = "2025-02-10T07:46:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/31/23/45feba8dae1d35fcca1e51b051f59dc4223cbd23e071a31e25f3f73938a8/lxml-5.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c", size = 5421580, upload-time = "2025-02-10T07:46:24.292Z" }, + { url = "https://files.pythonhosted.org/packages/61/69/be245d7b2dbef81c542af59c97fcd641fbf45accf2dc1c325bae7d0d014c/lxml-5.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f", size = 5285778, upload-time = "2025-02-10T07:46:28.801Z" }, + { url = "https://files.pythonhosted.org/packages/69/06/128af2ed04bac99b8f83becfb74c480f1aa18407b5c329fad457e08a1bf4/lxml-5.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645", size = 5054455, upload-time = "2025-02-10T07:46:31.665Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/f03a21cf6cc75cdd083563e509c7b6b159d761115c4142abb5481094ed8c/lxml-5.3.1-cp312-cp312-win32.whl", hash = "sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5", size = 3486315, upload-time = "2025-02-10T07:46:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9c/8abe21585d20ef70ad9cec7562da4332b764ed69ec29b7389d23dfabcea0/lxml-5.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf", size = 3816925, upload-time = "2025-02-10T07:46:37.285Z" }, + { url = "https://files.pythonhosted.org/packages/94/1c/724931daa1ace168e0237b929e44062545bf1551974102a5762c349c668d/lxml-5.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e", size = 8171881, upload-time = "2025-02-10T07:46:40.653Z" }, + { url = "https://files.pythonhosted.org/packages/67/0c/857b8fb6010c4246e66abeebb8639eaabba60a6d9b7c606554ecc5cbf1ee/lxml-5.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd", size = 4440394, upload-time = "2025-02-10T07:46:44.037Z" }, + { url = "https://files.pythonhosted.org/packages/61/72/c9e81de6a000f9682ccdd13503db26e973b24c68ac45a7029173237e3eed/lxml-5.3.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7", size = 5037860, upload-time = "2025-02-10T07:46:47.919Z" }, + { url = "https://files.pythonhosted.org/packages/24/26/942048c4b14835711b583b48cd7209bd2b5f0b6939ceed2381a494138b14/lxml-5.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414", size = 4782513, upload-time = "2025-02-10T07:46:50.696Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/27792339caf00f610cc5be32b940ba1e3009b7054feb0c4527cebac228d4/lxml-5.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e", size = 5305227, upload-time = "2025-02-10T07:46:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/25f7aa434a4d0d8e8420580af05ea49c3e12db6d297cf5435ac0a054df56/lxml-5.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1", size = 4829846, upload-time = "2025-02-10T07:46:56.262Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/faf235e0792547d24f61ee1448159325448a7e4f2ab706503049d8e5df19/lxml-5.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5", size = 4949495, upload-time = "2025-02-10T07:46:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e1/8f572ad9ed6039ba30f26dd4c2c58fb90f79362d2ee35ca3820284767672/lxml-5.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423", size = 4773415, upload-time = "2025-02-10T07:47:03.53Z" }, + { url = "https://files.pythonhosted.org/packages/a3/75/6b57166b9d1983dac8f28f354e38bff8d6bcab013a241989c4d54c72701b/lxml-5.3.1-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20", size = 5337710, upload-time = "2025-02-10T07:47:06.385Z" }, + { url = "https://files.pythonhosted.org/packages/cc/71/4aa56e2daa83bbcc66ca27b5155be2f900d996f5d0c51078eaaac8df9547/lxml-5.3.1-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8", size = 4897362, upload-time = "2025-02-10T07:47:09.24Z" }, + { url = "https://files.pythonhosted.org/packages/65/10/3fa2da152cd9b49332fd23356ed7643c9b74cad636ddd5b2400a9730d12b/lxml-5.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9", size = 4977795, upload-time = "2025-02-10T07:47:12.101Z" }, + { url = "https://files.pythonhosted.org/packages/de/d2/e1da0f7b20827e7b0ce934963cb6334c1b02cf1bb4aecd218c4496880cb3/lxml-5.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c", size = 4858104, upload-time = "2025-02-10T07:47:15.998Z" }, + { url = "https://files.pythonhosted.org/packages/a5/35/063420e1b33d3308f5aa7fcbdd19ef6c036f741c9a7a4bd5dc8032486b27/lxml-5.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b", size = 5416531, upload-time = "2025-02-10T07:47:19.862Z" }, + { url = "https://files.pythonhosted.org/packages/c3/83/93a6457d291d1e37adfb54df23498101a4701834258c840381dd2f6a030e/lxml-5.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5", size = 5273040, upload-time = "2025-02-10T07:47:24.29Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/ad4ac8fac488505a2702656550e63c2a8db3a4fd63db82a20dad5689cecb/lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252", size = 5050951, upload-time = "2025-02-10T07:47:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/82/74/f7d223c704c87e44b3d27b5e0dde173a2fcf2e89c0524c8015c2b3554876/lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78", size = 3485357, upload-time = "2025-02-10T07:47:29.738Z" }, + { url = "https://files.pythonhosted.org/packages/80/83/8c54533b3576f4391eebea88454738978669a6cad0d8e23266224007939d/lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332", size = 3814484, upload-time = "2025-02-10T07:47:33.3Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/89a68d05f267f05cc1b8b2f289a8242955705b1b0a9d246198227817ee46/lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725", size = 3936118, upload-time = "2025-02-10T07:50:09.752Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0d/c034a541e7a1153527d7880c62493a74f2277f38e64de2480cadd0d4cf96/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d", size = 4233690, upload-time = "2025-02-10T07:50:14.467Z" }, + { url = "https://files.pythonhosted.org/packages/35/5c/38e183c2802f14fbdaa75c3266e11d0ca05c64d78e8cdab2ee84e954a565/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84", size = 4349569, upload-time = "2025-02-10T07:50:17.602Z" }, + { url = "https://files.pythonhosted.org/packages/18/5b/14f93b359b3c29673d5d282bc3a6edb3a629879854a77541841aba37607f/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2", size = 4236731, upload-time = "2025-02-10T07:50:22.422Z" }, + { url = "https://files.pythonhosted.org/packages/f6/08/8471de65f3dee70a3a50e7082fd7409f0ac7a1ace777c13fca4aea1a5759/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877", size = 4373119, upload-time = "2025-02-10T07:50:27.462Z" }, + { url = "https://files.pythonhosted.org/packages/83/29/00b9b0322a473aee6cda87473401c9abb19506cd650cc69a8aa38277ea74/lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499", size = 3487718, upload-time = "2025-02-10T07:50:31.231Z" }, ] [[package]] @@ -445,67 +445,67 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, ] [[package]] name = "markupsafe" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120 }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032 }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057 }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359 }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306 }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094 }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521 }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] [[package]] @@ -515,81 +515,81 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chardet" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/ab/05ae008357c8bdb6245ebf8a101d99f26c096e0ea20800b318153da23796/mbstrdecoder-1.1.4.tar.gz", hash = "sha256:8105ef9cf6b7d7d69fe7fd6b68a2d8f281ca9b365d7a9b670be376b2e6c81b21", size = 14527 } +sdist = { url = "https://files.pythonhosted.org/packages/31/ab/05ae008357c8bdb6245ebf8a101d99f26c096e0ea20800b318153da23796/mbstrdecoder-1.1.4.tar.gz", hash = "sha256:8105ef9cf6b7d7d69fe7fd6b68a2d8f281ca9b365d7a9b670be376b2e6c81b21", size = 14527, upload-time = "2025-01-18T10:07:31.089Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/ac/5ce64a1d4cce00390beab88622a290420401f1cabf05caf2fc0995157c21/mbstrdecoder-1.1.4-py3-none-any.whl", hash = "sha256:03dae4ec50ec0d2ff4743e63fdbd5e0022815857494d35224b60775d3d934a8c", size = 7933 }, + { url = "https://files.pythonhosted.org/packages/30/ac/5ce64a1d4cce00390beab88622a290420401f1cabf05caf2fc0995157c21/mbstrdecoder-1.1.4-py3-none-any.whl", hash = "sha256:03dae4ec50ec0d2ff4743e63fdbd5e0022815857494d35224b60775d3d934a8c", size = 7933, upload-time = "2025-01-18T10:07:29.562Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] name = "mypy-extensions" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433, upload-time = "2023-02-04T12:11:27.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695, upload-time = "2023-02-04T12:11:25.002Z" }, ] [[package]] name = "packaging" version = "24.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, ] [[package]] name = "pathspec" version = "0.12.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] [[package]] name = "pathvalidate" version = "3.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/87/c7a2f51cc62df0495acb0ed2533a7c74cc895e569a1b020ee5f6e9fa4e21/pathvalidate-3.2.3.tar.gz", hash = "sha256:59b5b9278e30382d6d213497623043ebe63f10e29055be4419a9c04c721739cb", size = 61717 } +sdist = { url = "https://files.pythonhosted.org/packages/92/87/c7a2f51cc62df0495acb0ed2533a7c74cc895e569a1b020ee5f6e9fa4e21/pathvalidate-3.2.3.tar.gz", hash = "sha256:59b5b9278e30382d6d213497623043ebe63f10e29055be4419a9c04c721739cb", size = 61717, upload-time = "2025-01-03T14:06:42.789Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/14/c5a0e1a947909810fc4c043b84cac472b70e438148d34f5393be1bac663f/pathvalidate-3.2.3-py3-none-any.whl", hash = "sha256:5eaf0562e345d4b6d0c0239d0f690c3bd84d2a9a3c4c73b99ea667401b27bee1", size = 24130 }, + { url = "https://files.pythonhosted.org/packages/50/14/c5a0e1a947909810fc4c043b84cac472b70e438148d34f5393be1bac663f/pathvalidate-3.2.3-py3-none-any.whl", hash = "sha256:5eaf0562e345d4b6d0c0239d0f690c3bd84d2a9a3c4c73b99ea667401b27bee1", size = 24130, upload-time = "2025-01-03T14:06:39.568Z" }, ] [[package]] name = "platformdirs" version = "4.3.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } +sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291, upload-time = "2025-03-19T20:36:10.989Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, + { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499, upload-time = "2025-03-19T20:36:09.038Z" }, ] [[package]] name = "pluggy" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, ] [[package]] name = "pygments" version = "2.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] [[package]] @@ -605,9 +605,9 @@ dependencies = [ { name = "tcolorpy" }, { name = "typepy", extra = ["datetime"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f6/a1/617730f290f04d347103ab40bf67d317df6691b14746f6e1ea039fb57062/pytablewriter-1.2.1.tar.gz", hash = "sha256:7bd0f4f397e070e3b8a34edcf1b9257ccbb18305493d8350a5dbc9957fced959", size = 619241 } +sdist = { url = "https://files.pythonhosted.org/packages/f6/a1/617730f290f04d347103ab40bf67d317df6691b14746f6e1ea039fb57062/pytablewriter-1.2.1.tar.gz", hash = "sha256:7bd0f4f397e070e3b8a34edcf1b9257ccbb18305493d8350a5dbc9957fced959", size = 619241, upload-time = "2025-01-01T15:37:00.04Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/21/4c/c199512f01c845dfe5a7840ab3aae6c60463b5dc2a775be72502dfd9170a/pytablewriter-1.2.1-py3-none-any.whl", hash = "sha256:e906ff7ff5151d70a5f66e0f7b75642a7f2dce8d893c265b79cc9cf6bc04ddb4", size = 91083 }, + { url = "https://files.pythonhosted.org/packages/21/4c/c199512f01c845dfe5a7840ab3aae6c60463b5dc2a775be72502dfd9170a/pytablewriter-1.2.1-py3-none-any.whl", hash = "sha256:e906ff7ff5151d70a5f66e0f7b75642a7f2dce8d893c265b79cc9cf6bc04ddb4", size = 91083, upload-time = "2025-01-01T15:36:55.63Z" }, ] [[package]] @@ -622,9 +622,9 @@ dependencies = [ { name = "pluggy" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, ] [[package]] @@ -635,14 +635,14 @@ dependencies = [ { name = "cookiecutter" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/18/2e/11a3e1abb4bbf10e0af3f194ba4c55600de3fe52417ef3594c18d28ecdbe/pytest-cookies-0.7.0.tar.gz", hash = "sha256:1aaa6b4def8238d0d1709d3d773b423351bfb671c1e3438664d824e0859d6308", size = 8840 } +sdist = { url = "https://files.pythonhosted.org/packages/18/2e/11a3e1abb4bbf10e0af3f194ba4c55600de3fe52417ef3594c18d28ecdbe/pytest-cookies-0.7.0.tar.gz", hash = "sha256:1aaa6b4def8238d0d1709d3d773b423351bfb671c1e3438664d824e0859d6308", size = 8840, upload-time = "2023-03-22T11:07:29.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/f7/438af2f3a6c58f81d22c126707ee5d079f653a76961f4fb7d995e526a9c4/pytest_cookies-0.7.0-py3-none-any.whl", hash = "sha256:52770f090d77b16428f6a24a208e6be76addb2e33458035714087b4de49389ea", size = 6386 }, + { url = "https://files.pythonhosted.org/packages/5f/f7/438af2f3a6c58f81d22c126707ee5d079f653a76961f4fb7d995e526a9c4/pytest_cookies-0.7.0-py3-none-any.whl", hash = "sha256:52770f090d77b16428f6a24a208e6be76addb2e33458035714087b4de49389ea", size = 6386, upload-time = "2023-03-22T11:07:28.068Z" }, ] [[package]] name = "pytest-jsonschema" -version = "1.0.0b1" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "jsonschema" }, @@ -650,9 +650,9 @@ dependencies = [ { name = "ruamel-yaml" }, { name = "tomli", marker = "python_full_version < '3.12'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/65/e6/d6062480a60005050ae0c41b326ca777848dfd6d9821931685bccde688dc/pytest_jsonschema-1.0.0b1.tar.gz", hash = "sha256:6b4f0d890e0eeee64aefd3ea62b272a78930b3a645a9ab876c284767244ebd3c", size = 159225 } +sdist = { url = "https://files.pythonhosted.org/packages/d9/2e/ced4e814c7b685dd441fd57f6d79d516b5931abce8b25c9d5cf6f8769aa3/pytest_jsonschema-1.0.0.tar.gz", hash = "sha256:65af57cb3acd1d8e897b8e73dfd408d07f5aa90bab55126c9f9fb77480be627b", size = 168929, upload-time = "2025-11-07T19:32:34.826Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/52/63/db04d0f85933a21d2103c2a75074662dfccafef85bcf5e2a648786acbf9c/pytest_jsonschema-1.0.0b1-py3-none-any.whl", hash = "sha256:bbed170a555c71cb1a317f9f2b86430c7a39922384cbf07f99be43427018b853", size = 101935 }, + { url = "https://files.pythonhosted.org/packages/e6/77/8726adee08a43955c99f5e8733362c5a131b012eab59bb8029020fe9621a/pytest_jsonschema-1.0.0-py3-none-any.whl", hash = "sha256:5c47a18983556df5fa25c8094ea90bb4506455c846d7209eee6b9142538d253e", size = 111782, upload-time = "2025-11-07T19:32:32.841Z" }, ] [[package]] @@ -665,9 +665,9 @@ dependencies = [ { name = "tcolorpy" }, { name = "typepy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/ec/5864e21c74e6ab2c493578bd6bbef775c4db4c1e69a2aad8830408341170/pytest_md_report-0.6.3.tar.gz", hash = "sha256:7e91ed0c0421b1493c1126ef996bdb521882a96a20cbd5b2cf0a38945495a852", size = 283723 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/ec/5864e21c74e6ab2c493578bd6bbef775c4db4c1e69a2aad8830408341170/pytest_md_report-0.6.3.tar.gz", hash = "sha256:7e91ed0c0421b1493c1126ef996bdb521882a96a20cbd5b2cf0a38945495a852", size = 283723, upload-time = "2025-01-02T09:10:48.201Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/ad/99382de65e29fa6230b592f176c4290084667dbf1404c0ef266a34bb2c46/pytest_md_report-0.6.3-py3-none-any.whl", hash = "sha256:374126c013b29aab4dc585a318c4bbc5213fabafc101f9812f428cc4847b6a92", size = 13928 }, + { url = "https://files.pythonhosted.org/packages/22/ad/99382de65e29fa6230b592f176c4290084667dbf1404c0ef266a34bb2c46/pytest_md_report-0.6.3-py3-none-any.whl", hash = "sha256:374126c013b29aab4dc585a318c4bbc5213fabafc101f9812f428cc4847b6a92", size = 13928, upload-time = "2025-01-02T09:10:45.227Z" }, ] [[package]] @@ -677,9 +677,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] @@ -689,62 +689,62 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "text-unidecode" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921 } +sdist = { url = "https://files.pythonhosted.org/packages/87/c7/5e1547c44e31da50a460df93af11a535ace568ef89d7a811069ead340c4a/python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856", size = 10921, upload-time = "2024-02-08T18:32:45.488Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051 }, + { url = "https://files.pythonhosted.org/packages/a4/62/02da182e544a51a5c3ccf4b03ab79df279f9c60c5e82d5e8bec7ca26ac11/python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8", size = 10051, upload-time = "2024-02-08T18:32:43.911Z" }, ] [[package]] name = "pytz" version = "2025.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] [[package]] name = "pyyaml" version = "6.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, ] [[package]] @@ -756,9 +756,9 @@ dependencies = [ { name = "rpds-py" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744 } +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, ] [[package]] @@ -771,9 +771,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] [[package]] @@ -785,94 +785,94 @@ dependencies = [ { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, + { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, ] [[package]] name = "rpds-py" version = "0.23.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/79/2ce611b18c4fd83d9e3aecb5cba93e1917c050f556db39842889fa69b79f/rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707", size = 26806 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/fe/e5326459863bd525122f4e9c80ac8d7c6cfa171b7518d04cc27c12c209b0/rpds_py-0.23.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2a54027554ce9b129fc3d633c92fa33b30de9f08bc61b32c053dc9b537266fed", size = 372123 }, - { url = "https://files.pythonhosted.org/packages/f9/db/f10a3795f7a89fb27594934012d21c61019bbeb516c5bdcfbbe9e9e617a7/rpds_py-0.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5ef909a37e9738d146519657a1aab4584018746a18f71c692f2f22168ece40c", size = 356778 }, - { url = "https://files.pythonhosted.org/packages/21/27/0d3678ad7f432fa86f8fac5f5fc6496a4d2da85682a710d605219be20063/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee9d6f0b38efb22ad94c3b68ffebe4c47865cdf4b17f6806d6c674e1feb4246", size = 385775 }, - { url = "https://files.pythonhosted.org/packages/99/a0/1786defa125b2ad228027f22dff26312ce7d1fee3c7c3c2682f403db2062/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7356a6da0562190558c4fcc14f0281db191cdf4cb96e7604c06acfcee96df15", size = 391181 }, - { url = "https://files.pythonhosted.org/packages/f1/5c/1240934050a7ffd020a915486d0cc4c7f6e7a2442a77aedf13664db55d36/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9441af1d25aed96901f97ad83d5c3e35e6cd21a25ca5e4916c82d7dd0490a4fa", size = 444607 }, - { url = "https://files.pythonhosted.org/packages/b7/1b/cee6905b47817fd0a377716dbe4df35295de46df46ee2ff704538cc371b0/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d8abf7896a91fb97e7977d1aadfcc2c80415d6dc2f1d0fca5b8d0df247248f3", size = 445550 }, - { url = "https://files.pythonhosted.org/packages/54/f7/f0821ca34032892d7a67fcd5042f50074ff2de64e771e10df01085c88d47/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b08027489ba8fedde72ddd233a5ea411b85a6ed78175f40285bd401bde7466d", size = 386148 }, - { url = "https://files.pythonhosted.org/packages/eb/ef/2afe53bc857c4bcba336acfd2629883a5746e7291023e017ac7fc98d85aa/rpds_py-0.23.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fee513135b5a58f3bb6d89e48326cd5aa308e4bcdf2f7d59f67c861ada482bf8", size = 416780 }, - { url = "https://files.pythonhosted.org/packages/ae/9a/38d2236cf669789b8a3e1a014c9b6a8d7b8925b952c92e7839ae2749f9ac/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35d5631ce0af26318dba0ae0ac941c534453e42f569011585cb323b7774502a5", size = 558265 }, - { url = "https://files.pythonhosted.org/packages/e6/0a/f2705530c42578f20ed0b5b90135eecb30eef6e2ba73e7ba69087fad2dba/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a20cb698c4a59c534c6701b1c24a968ff2768b18ea2991f886bd8985ce17a89f", size = 585270 }, - { url = "https://files.pythonhosted.org/packages/29/4e/3b597dc84ed82c3d757ac9aa620de224a94e06d2e102069795ae7e81c015/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e9c206a1abc27e0588cf8b7c8246e51f1a16a103734f7750830a1ccb63f557a", size = 553850 }, - { url = "https://files.pythonhosted.org/packages/00/cc/6498b6f79e4375e6737247661e52a2d18f6accf4910e0c8da978674b4241/rpds_py-0.23.1-cp310-cp310-win32.whl", hash = "sha256:d9f75a06ecc68f159d5d7603b734e1ff6daa9497a929150f794013aa9f6e3f12", size = 220660 }, - { url = "https://files.pythonhosted.org/packages/17/2b/08db023d23e8c7032c99d8d2a70d32e450a868ab73d16e3ff5290308a665/rpds_py-0.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:f35eff113ad430b5272bbfc18ba111c66ff525828f24898b4e146eb479a2cdda", size = 232551 }, - { url = "https://files.pythonhosted.org/packages/1c/67/6e5d4234bb9dee062ffca2a5f3c7cd38716317d6760ec235b175eed4de2c/rpds_py-0.23.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b79f5ced71efd70414a9a80bbbfaa7160da307723166f09b69773153bf17c590", size = 372264 }, - { url = "https://files.pythonhosted.org/packages/a7/0a/3dedb2daee8e783622427f5064e2d112751d8276ee73aa5409f000a132f4/rpds_py-0.23.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9e799dac1ffbe7b10c1fd42fe4cd51371a549c6e108249bde9cd1200e8f59b4", size = 356883 }, - { url = "https://files.pythonhosted.org/packages/ed/fc/e1acef44f9c24b05fe5434b235f165a63a52959ac655e3f7a55726cee1a4/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721f9c4011b443b6e84505fc00cc7aadc9d1743f1c988e4c89353e19c4a968ee", size = 385624 }, - { url = "https://files.pythonhosted.org/packages/97/0a/a05951f6465d01622720c03ef6ef31adfbe865653e05ed7c45837492f25e/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f88626e3f5e57432e6191cd0c5d6d6b319b635e70b40be2ffba713053e5147dd", size = 391500 }, - { url = "https://files.pythonhosted.org/packages/ea/2e/cca0583ec0690ea441dceae23c0673b99755710ea22f40bccf1e78f41481/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:285019078537949cecd0190f3690a0b0125ff743d6a53dfeb7a4e6787af154f5", size = 444869 }, - { url = "https://files.pythonhosted.org/packages/cc/e6/95cda68b33a6d814d1e96b0e406d231ed16629101460d1740e92f03365e6/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92f5654157de1379c509b15acec9d12ecf6e3bc1996571b6cb82a4302060447", size = 444930 }, - { url = "https://files.pythonhosted.org/packages/5f/a7/e94cdb73411ae9c11414d3c7c9a6ad75d22ad4a8d094fb45a345ba9e3018/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e768267cbe051dd8d1c5305ba690bb153204a09bf2e3de3ae530de955f5b5580", size = 386254 }, - { url = "https://files.pythonhosted.org/packages/dd/c5/a4a943d90a39e85efd1e04b1ad5129936786f9a9aa27bb7be8fc5d9d50c9/rpds_py-0.23.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5334a71f7dc1160382d45997e29f2637c02f8a26af41073189d79b95d3321f1", size = 417090 }, - { url = "https://files.pythonhosted.org/packages/0c/a0/80d0013b12428d1fce0ab4e71829400b0a32caec12733c79e6109f843342/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6adb81564af0cd428910f83fa7da46ce9ad47c56c0b22b50872bc4515d91966", size = 557639 }, - { url = "https://files.pythonhosted.org/packages/a6/92/ec2e6980afb964a2cd7a99cbdef1f6c01116abe94b42cbe336ac93dd11c2/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cafa48f2133d4daa028473ede7d81cd1b9f9e6925e9e4003ebdf77010ee02f35", size = 584572 }, - { url = "https://files.pythonhosted.org/packages/3d/ce/75b6054db34a390789a82523790717b27c1bd735e453abb429a87c4f0f26/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fced9fd4a07a1ded1bac7e961ddd9753dd5d8b755ba8e05acba54a21f5f1522", size = 553028 }, - { url = "https://files.pythonhosted.org/packages/cc/24/f45abe0418c06a5cba0f846e967aa27bac765acd927aabd857c21319b8cc/rpds_py-0.23.1-cp311-cp311-win32.whl", hash = "sha256:243241c95174b5fb7204c04595852fe3943cc41f47aa14c3828bc18cd9d3b2d6", size = 220862 }, - { url = "https://files.pythonhosted.org/packages/2d/a6/3c0880e8bbfc36451ef30dc416266f6d2934705e468db5d21c8ba0ab6400/rpds_py-0.23.1-cp311-cp311-win_amd64.whl", hash = "sha256:11dd60b2ffddba85715d8a66bb39b95ddbe389ad2cfcf42c833f1bcde0878eaf", size = 232953 }, - { url = "https://files.pythonhosted.org/packages/f3/8c/d17efccb9f5b9137ddea706664aebae694384ae1d5997c0202093e37185a/rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c", size = 364369 }, - { url = "https://files.pythonhosted.org/packages/6e/c0/ab030f696b5c573107115a88d8d73d80f03309e60952b64c584c70c659af/rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba", size = 349965 }, - { url = "https://files.pythonhosted.org/packages/b3/55/b40170f5a079c4fb0b6a82b299689e66e744edca3c3375a8b160fb797660/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31", size = 389064 }, - { url = "https://files.pythonhosted.org/packages/ab/1c/b03a912c59ec7c1e16b26e587b9dfa8ddff3b07851e781e8c46e908a365a/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149", size = 397741 }, - { url = "https://files.pythonhosted.org/packages/52/6f/151b90792b62fb6f87099bcc9044c626881fdd54e31bf98541f830b15cea/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c", size = 448784 }, - { url = "https://files.pythonhosted.org/packages/71/2a/6de67c0c97ec7857e0e9e5cd7c52405af931b303eb1e5b9eff6c50fd9a2e/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5", size = 440203 }, - { url = "https://files.pythonhosted.org/packages/db/5e/e759cd1c276d98a4b1f464b17a9bf66c65d29f8f85754e27e1467feaa7c3/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc", size = 391611 }, - { url = "https://files.pythonhosted.org/packages/1c/1e/2900358efcc0d9408c7289769cba4c0974d9db314aa884028ed7f7364f61/rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35", size = 423306 }, - { url = "https://files.pythonhosted.org/packages/23/07/6c177e6d059f5d39689352d6c69a926ee4805ffdb6f06203570234d3d8f7/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b", size = 562323 }, - { url = "https://files.pythonhosted.org/packages/70/e4/f9097fd1c02b516fff9850792161eb9fc20a2fd54762f3c69eae0bdb67cb/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef", size = 588351 }, - { url = "https://files.pythonhosted.org/packages/87/39/5db3c6f326bfbe4576ae2af6435bd7555867d20ae690c786ff33659f293b/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad", size = 557252 }, - { url = "https://files.pythonhosted.org/packages/fd/14/2d5ad292f144fa79bafb78d2eb5b8a3a91c358b6065443cb9c49b5d1fedf/rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057", size = 222181 }, - { url = "https://files.pythonhosted.org/packages/a3/4f/0fce63e0f5cdd658e71e21abd17ac1bc9312741ebb8b3f74eeed2ebdf771/rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165", size = 237426 }, - { url = "https://files.pythonhosted.org/packages/13/9d/b8b2c0edffb0bed15be17b6d5ab06216f2f47f9ee49259c7e96a3ad4ca42/rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935", size = 363672 }, - { url = "https://files.pythonhosted.org/packages/bd/c2/5056fa29e6894144d7ba4c938b9b0445f75836b87d2dd00ed4999dc45a8c/rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4", size = 349602 }, - { url = "https://files.pythonhosted.org/packages/b0/bc/33779a1bb0ee32d8d706b173825aab75c628521d23ce72a7c1e6a6852f86/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6", size = 388746 }, - { url = "https://files.pythonhosted.org/packages/62/0b/71db3e36b7780a619698ec82a9c87ab44ad7ca7f5480913e8a59ff76f050/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10", size = 397076 }, - { url = "https://files.pythonhosted.org/packages/bb/2e/494398f613edf77ba10a916b1ddea2acce42ab0e3b62e2c70ffc0757ce00/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122", size = 448399 }, - { url = "https://files.pythonhosted.org/packages/dd/53/4bd7f5779b1f463243ee5fdc83da04dd58a08f86e639dbffa7a35f969a84/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4", size = 439764 }, - { url = "https://files.pythonhosted.org/packages/f6/55/b3c18c04a460d951bf8e91f2abf46ce5b6426fb69784166a6a25827cb90a/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013", size = 390662 }, - { url = "https://files.pythonhosted.org/packages/2a/65/cc463044a3cbd616029b2aa87a651cdee8288d2fdd7780b2244845e934c1/rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64", size = 422680 }, - { url = "https://files.pythonhosted.org/packages/fa/8e/1fa52990c7836d72e8d70cd7753f2362c72fbb0a49c1462e8c60e7176d0b/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8", size = 561792 }, - { url = "https://files.pythonhosted.org/packages/57/b8/fe3b612979b1a29d0c77f8585903d8b3a292604b26d4b300e228b8ac6360/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957", size = 588127 }, - { url = "https://files.pythonhosted.org/packages/44/2d/fde474de516bbc4b9b230f43c98e7f8acc5da7fc50ceed8e7af27553d346/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93", size = 556981 }, - { url = "https://files.pythonhosted.org/packages/18/57/767deeb27b81370bbab8f74ef6e68d26c4ea99018f3c71a570e506fede85/rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd", size = 221936 }, - { url = "https://files.pythonhosted.org/packages/7d/6c/3474cfdd3cafe243f97ab8474ea8949236eb2a1a341ca55e75ce00cd03da/rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70", size = 237145 }, - { url = "https://files.pythonhosted.org/packages/ec/77/e985064c624230f61efa0423759bb066da56ebe40c654f8b5ba225bd5d63/rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731", size = 359623 }, - { url = "https://files.pythonhosted.org/packages/62/d9/a33dcbf62b29e40559e012d525bae7d516757cf042cc9234bd34ca4b6aeb/rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5", size = 345900 }, - { url = "https://files.pythonhosted.org/packages/92/eb/f81a4be6397861adb2cb868bb6a28a33292c2dcac567d1dc575226055e55/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a", size = 386426 }, - { url = "https://files.pythonhosted.org/packages/09/47/1f810c9b5e83be005341201b5389f1d240dfa440346ea7189f9b3fd6961d/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e", size = 392314 }, - { url = "https://files.pythonhosted.org/packages/83/bd/bc95831432fd6c46ed8001f01af26de0763a059d6d7e6d69e3c5bf02917a/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f", size = 447706 }, - { url = "https://files.pythonhosted.org/packages/19/3e/567c04c226b1802dc6dc82cad3d53e1fa0a773258571c74ac5d8fbde97ed/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219", size = 437060 }, - { url = "https://files.pythonhosted.org/packages/fe/77/a77d2c6afe27ae7d0d55fc32f6841502648070dc8d549fcc1e6d47ff8975/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722", size = 389347 }, - { url = "https://files.pythonhosted.org/packages/3f/47/6b256ff20a74cfebeac790ab05586e0ac91f88e331125d4740a6c86fc26f/rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e", size = 415554 }, - { url = "https://files.pythonhosted.org/packages/fc/29/d4572469a245bc9fc81e35166dca19fc5298d5c43e1a6dd64bf145045193/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6", size = 557418 }, - { url = "https://files.pythonhosted.org/packages/9c/0a/68cf7228895b1a3f6f39f51b15830e62456795e61193d2c8b87fd48c60db/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b", size = 583033 }, - { url = "https://files.pythonhosted.org/packages/14/18/017ab41dcd6649ad5db7d00155b4c212b31ab05bd857d5ba73a1617984eb/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5", size = 554880 }, - { url = "https://files.pythonhosted.org/packages/2e/dd/17de89431268da8819d8d51ce67beac28d9b22fccf437bc5d6d2bcd1acdb/rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7", size = 219743 }, - { url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415 }, - { url = "https://files.pythonhosted.org/packages/95/a9/6fafd35fc6bac05f59bcbc800b57cef877911ff1c015397c519fec888642/rpds_py-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1f8afa346ccd59e4e5630d5abb67aba6a9812fddf764fd7eb11f382a345f8cc", size = 373463 }, - { url = "https://files.pythonhosted.org/packages/5b/ac/44f00029b8fbe0903a19e9a87a9b86063bf8700df2cc58868373d378418c/rpds_py-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fad784a31869747df4ac968a351e070c06ca377549e4ace94775aaa3ab33ee06", size = 358400 }, - { url = "https://files.pythonhosted.org/packages/5e/9c/3da199346c68d785f10dccab123b74c8c5f73be3f742c9e33d1116e07931/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5a96fcac2f18e5a0a23a75cd27ce2656c66c11c127b0318e508aab436b77428", size = 386815 }, - { url = "https://files.pythonhosted.org/packages/d3/45/8f6533c33c0d33da8c2c8b2fb8f2ee90b23c05c679b86b0ac6aee4653749/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e77febf227a1dc3220159355dba68faa13f8dca9335d97504abf428469fb18b", size = 392974 }, - { url = "https://files.pythonhosted.org/packages/ca/56/6a9ac1bf0455ba07385d8fe98c571c519b4f2000cff6581487bf9fab9272/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26bb3e8de93443d55e2e748e9fd87deb5f8075ca7bc0502cfc8be8687d69a2ec", size = 446019 }, - { url = "https://files.pythonhosted.org/packages/f4/83/5d9a3f9731cdccf49088bcc4ce821a5cf50bd1737cdad83e9959a7b9054d/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db7707dde9143a67b8812c7e66aeb2d843fe33cc8e374170f4d2c50bd8f2472d", size = 445811 }, - { url = "https://files.pythonhosted.org/packages/44/50/f2e0a98c62fc1fe68b176caca587714dc5c8bb2c3d1dd1eeb2bd4cc787ac/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eedaaccc9bb66581d4ae7c50e15856e335e57ef2734dbc5fd8ba3e2a4ab3cb6", size = 388070 }, - { url = "https://files.pythonhosted.org/packages/f2/d0/4981878f8f157e6dbea01d95e0119bf3d6b4c2c884fe64a9e6987f941104/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28358c54fffadf0ae893f6c1050e8f8853e45df22483b7fff2f6ab6152f5d8bf", size = 419173 }, - { url = "https://files.pythonhosted.org/packages/ce/13/fc971c470da96b270d2f64fedee987351bd935dc3016932a5cdcb1a88a2a/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:633462ef7e61d839171bf206551d5ab42b30b71cac8f10a64a662536e057fdef", size = 559048 }, - { url = "https://files.pythonhosted.org/packages/42/02/be91e1de139ec8b4f9fec4192fd779ba48af281cfc762c0ca4c15b945484/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a98f510d86f689fcb486dc59e6e363af04151e5260ad1bdddb5625c10f1e95f8", size = 584773 }, - { url = "https://files.pythonhosted.org/packages/27/28/3af8a1956df3edc41d884267d766dc096496dafc83f02f764a475eca0b4a/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e0397dd0b3955c61ef9b22838144aa4bef6f0796ba5cc8edfc64d468b93798b4", size = 555153 }, - { url = "https://files.pythonhosted.org/packages/5e/bb/e45f51c4e1327dea3c72b846c6de129eebacb7a6cb309af7af35d0578c80/rpds_py-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b", size = 233827 }, +sdist = { url = "https://files.pythonhosted.org/packages/0a/79/2ce611b18c4fd83d9e3aecb5cba93e1917c050f556db39842889fa69b79f/rpds_py-0.23.1.tar.gz", hash = "sha256:7f3240dcfa14d198dba24b8b9cb3b108c06b68d45b7babd9eefc1038fdf7e707", size = 26806, upload-time = "2025-02-21T15:04:23.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/fe/e5326459863bd525122f4e9c80ac8d7c6cfa171b7518d04cc27c12c209b0/rpds_py-0.23.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2a54027554ce9b129fc3d633c92fa33b30de9f08bc61b32c053dc9b537266fed", size = 372123, upload-time = "2025-02-21T15:01:14.816Z" }, + { url = "https://files.pythonhosted.org/packages/f9/db/f10a3795f7a89fb27594934012d21c61019bbeb516c5bdcfbbe9e9e617a7/rpds_py-0.23.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5ef909a37e9738d146519657a1aab4584018746a18f71c692f2f22168ece40c", size = 356778, upload-time = "2025-02-21T15:01:16.788Z" }, + { url = "https://files.pythonhosted.org/packages/21/27/0d3678ad7f432fa86f8fac5f5fc6496a4d2da85682a710d605219be20063/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ee9d6f0b38efb22ad94c3b68ffebe4c47865cdf4b17f6806d6c674e1feb4246", size = 385775, upload-time = "2025-02-21T15:01:18.192Z" }, + { url = "https://files.pythonhosted.org/packages/99/a0/1786defa125b2ad228027f22dff26312ce7d1fee3c7c3c2682f403db2062/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7356a6da0562190558c4fcc14f0281db191cdf4cb96e7604c06acfcee96df15", size = 391181, upload-time = "2025-02-21T15:01:20.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5c/1240934050a7ffd020a915486d0cc4c7f6e7a2442a77aedf13664db55d36/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9441af1d25aed96901f97ad83d5c3e35e6cd21a25ca5e4916c82d7dd0490a4fa", size = 444607, upload-time = "2025-02-21T15:01:22.221Z" }, + { url = "https://files.pythonhosted.org/packages/b7/1b/cee6905b47817fd0a377716dbe4df35295de46df46ee2ff704538cc371b0/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d8abf7896a91fb97e7977d1aadfcc2c80415d6dc2f1d0fca5b8d0df247248f3", size = 445550, upload-time = "2025-02-21T15:01:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/54/f7/f0821ca34032892d7a67fcd5042f50074ff2de64e771e10df01085c88d47/rpds_py-0.23.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b08027489ba8fedde72ddd233a5ea411b85a6ed78175f40285bd401bde7466d", size = 386148, upload-time = "2025-02-21T15:01:26.23Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ef/2afe53bc857c4bcba336acfd2629883a5746e7291023e017ac7fc98d85aa/rpds_py-0.23.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fee513135b5a58f3bb6d89e48326cd5aa308e4bcdf2f7d59f67c861ada482bf8", size = 416780, upload-time = "2025-02-21T15:01:27.778Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9a/38d2236cf669789b8a3e1a014c9b6a8d7b8925b952c92e7839ae2749f9ac/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:35d5631ce0af26318dba0ae0ac941c534453e42f569011585cb323b7774502a5", size = 558265, upload-time = "2025-02-21T15:01:28.979Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0a/f2705530c42578f20ed0b5b90135eecb30eef6e2ba73e7ba69087fad2dba/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a20cb698c4a59c534c6701b1c24a968ff2768b18ea2991f886bd8985ce17a89f", size = 585270, upload-time = "2025-02-21T15:01:30.879Z" }, + { url = "https://files.pythonhosted.org/packages/29/4e/3b597dc84ed82c3d757ac9aa620de224a94e06d2e102069795ae7e81c015/rpds_py-0.23.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e9c206a1abc27e0588cf8b7c8246e51f1a16a103734f7750830a1ccb63f557a", size = 553850, upload-time = "2025-02-21T15:01:32.269Z" }, + { url = "https://files.pythonhosted.org/packages/00/cc/6498b6f79e4375e6737247661e52a2d18f6accf4910e0c8da978674b4241/rpds_py-0.23.1-cp310-cp310-win32.whl", hash = "sha256:d9f75a06ecc68f159d5d7603b734e1ff6daa9497a929150f794013aa9f6e3f12", size = 220660, upload-time = "2025-02-21T15:01:33.643Z" }, + { url = "https://files.pythonhosted.org/packages/17/2b/08db023d23e8c7032c99d8d2a70d32e450a868ab73d16e3ff5290308a665/rpds_py-0.23.1-cp310-cp310-win_amd64.whl", hash = "sha256:f35eff113ad430b5272bbfc18ba111c66ff525828f24898b4e146eb479a2cdda", size = 232551, upload-time = "2025-02-21T15:01:35.529Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/6e5d4234bb9dee062ffca2a5f3c7cd38716317d6760ec235b175eed4de2c/rpds_py-0.23.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b79f5ced71efd70414a9a80bbbfaa7160da307723166f09b69773153bf17c590", size = 372264, upload-time = "2025-02-21T15:01:37.918Z" }, + { url = "https://files.pythonhosted.org/packages/a7/0a/3dedb2daee8e783622427f5064e2d112751d8276ee73aa5409f000a132f4/rpds_py-0.23.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c9e799dac1ffbe7b10c1fd42fe4cd51371a549c6e108249bde9cd1200e8f59b4", size = 356883, upload-time = "2025-02-21T15:01:39.131Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/e1acef44f9c24b05fe5434b235f165a63a52959ac655e3f7a55726cee1a4/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721f9c4011b443b6e84505fc00cc7aadc9d1743f1c988e4c89353e19c4a968ee", size = 385624, upload-time = "2025-02-21T15:01:40.589Z" }, + { url = "https://files.pythonhosted.org/packages/97/0a/a05951f6465d01622720c03ef6ef31adfbe865653e05ed7c45837492f25e/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f88626e3f5e57432e6191cd0c5d6d6b319b635e70b40be2ffba713053e5147dd", size = 391500, upload-time = "2025-02-21T15:01:42.584Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2e/cca0583ec0690ea441dceae23c0673b99755710ea22f40bccf1e78f41481/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:285019078537949cecd0190f3690a0b0125ff743d6a53dfeb7a4e6787af154f5", size = 444869, upload-time = "2025-02-21T15:01:44.004Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e6/95cda68b33a6d814d1e96b0e406d231ed16629101460d1740e92f03365e6/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b92f5654157de1379c509b15acec9d12ecf6e3bc1996571b6cb82a4302060447", size = 444930, upload-time = "2025-02-21T15:01:46.191Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a7/e94cdb73411ae9c11414d3c7c9a6ad75d22ad4a8d094fb45a345ba9e3018/rpds_py-0.23.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e768267cbe051dd8d1c5305ba690bb153204a09bf2e3de3ae530de955f5b5580", size = 386254, upload-time = "2025-02-21T15:01:48.038Z" }, + { url = "https://files.pythonhosted.org/packages/dd/c5/a4a943d90a39e85efd1e04b1ad5129936786f9a9aa27bb7be8fc5d9d50c9/rpds_py-0.23.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c5334a71f7dc1160382d45997e29f2637c02f8a26af41073189d79b95d3321f1", size = 417090, upload-time = "2025-02-21T15:01:50.252Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a0/80d0013b12428d1fce0ab4e71829400b0a32caec12733c79e6109f843342/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6adb81564af0cd428910f83fa7da46ce9ad47c56c0b22b50872bc4515d91966", size = 557639, upload-time = "2025-02-21T15:01:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/a6/92/ec2e6980afb964a2cd7a99cbdef1f6c01116abe94b42cbe336ac93dd11c2/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cafa48f2133d4daa028473ede7d81cd1b9f9e6925e9e4003ebdf77010ee02f35", size = 584572, upload-time = "2025-02-21T15:01:53.13Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ce/75b6054db34a390789a82523790717b27c1bd735e453abb429a87c4f0f26/rpds_py-0.23.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fced9fd4a07a1ded1bac7e961ddd9753dd5d8b755ba8e05acba54a21f5f1522", size = 553028, upload-time = "2025-02-21T15:01:54.84Z" }, + { url = "https://files.pythonhosted.org/packages/cc/24/f45abe0418c06a5cba0f846e967aa27bac765acd927aabd857c21319b8cc/rpds_py-0.23.1-cp311-cp311-win32.whl", hash = "sha256:243241c95174b5fb7204c04595852fe3943cc41f47aa14c3828bc18cd9d3b2d6", size = 220862, upload-time = "2025-02-21T15:01:56.966Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a6/3c0880e8bbfc36451ef30dc416266f6d2934705e468db5d21c8ba0ab6400/rpds_py-0.23.1-cp311-cp311-win_amd64.whl", hash = "sha256:11dd60b2ffddba85715d8a66bb39b95ddbe389ad2cfcf42c833f1bcde0878eaf", size = 232953, upload-time = "2025-02-21T15:02:00.297Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8c/d17efccb9f5b9137ddea706664aebae694384ae1d5997c0202093e37185a/rpds_py-0.23.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3902df19540e9af4cc0c3ae75974c65d2c156b9257e91f5101a51f99136d834c", size = 364369, upload-time = "2025-02-21T15:02:02.396Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c0/ab030f696b5c573107115a88d8d73d80f03309e60952b64c584c70c659af/rpds_py-0.23.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66f8d2a17e5838dd6fb9be6baaba8e75ae2f5fa6b6b755d597184bfcd3cb0eba", size = 349965, upload-time = "2025-02-21T15:02:04.527Z" }, + { url = "https://files.pythonhosted.org/packages/b3/55/b40170f5a079c4fb0b6a82b299689e66e744edca3c3375a8b160fb797660/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112b8774b0b4ee22368fec42749b94366bd9b536f8f74c3d4175d4395f5cbd31", size = 389064, upload-time = "2025-02-21T15:02:06.633Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1c/b03a912c59ec7c1e16b26e587b9dfa8ddff3b07851e781e8c46e908a365a/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0df046f2266e8586cf09d00588302a32923eb6386ced0ca5c9deade6af9a149", size = 397741, upload-time = "2025-02-21T15:02:08.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/6f/151b90792b62fb6f87099bcc9044c626881fdd54e31bf98541f830b15cea/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3288930b947cbebe767f84cf618d2cbe0b13be476e749da0e6a009f986248c", size = 448784, upload-time = "2025-02-21T15:02:09.473Z" }, + { url = "https://files.pythonhosted.org/packages/71/2a/6de67c0c97ec7857e0e9e5cd7c52405af931b303eb1e5b9eff6c50fd9a2e/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce473a2351c018b06dd8d30d5da8ab5a0831056cc53b2006e2a8028172c37ce5", size = 440203, upload-time = "2025-02-21T15:02:11.745Z" }, + { url = "https://files.pythonhosted.org/packages/db/5e/e759cd1c276d98a4b1f464b17a9bf66c65d29f8f85754e27e1467feaa7c3/rpds_py-0.23.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d550d7e9e7d8676b183b37d65b5cd8de13676a738973d330b59dc8312df9c5dc", size = 391611, upload-time = "2025-02-21T15:02:13.76Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1e/2900358efcc0d9408c7289769cba4c0974d9db314aa884028ed7f7364f61/rpds_py-0.23.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e14f86b871ea74c3fddc9a40e947d6a5d09def5adc2076ee61fb910a9014fb35", size = 423306, upload-time = "2025-02-21T15:02:15.096Z" }, + { url = "https://files.pythonhosted.org/packages/23/07/6c177e6d059f5d39689352d6c69a926ee4805ffdb6f06203570234d3d8f7/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf5be5ba34e19be579ae873da515a2836a2166d8d7ee43be6ff909eda42b72b", size = 562323, upload-time = "2025-02-21T15:02:17.379Z" }, + { url = "https://files.pythonhosted.org/packages/70/e4/f9097fd1c02b516fff9850792161eb9fc20a2fd54762f3c69eae0bdb67cb/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7031d493c4465dbc8d40bd6cafefef4bd472b17db0ab94c53e7909ee781b9ef", size = 588351, upload-time = "2025-02-21T15:02:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/87/39/5db3c6f326bfbe4576ae2af6435bd7555867d20ae690c786ff33659f293b/rpds_py-0.23.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55ff4151cfd4bc635e51cfb1c59ac9f7196b256b12e3a57deb9e5742e65941ad", size = 557252, upload-time = "2025-02-21T15:02:21.875Z" }, + { url = "https://files.pythonhosted.org/packages/fd/14/2d5ad292f144fa79bafb78d2eb5b8a3a91c358b6065443cb9c49b5d1fedf/rpds_py-0.23.1-cp312-cp312-win32.whl", hash = "sha256:a9d3b728f5a5873d84cba997b9d617c6090ca5721caaa691f3b1a78c60adc057", size = 222181, upload-time = "2025-02-21T15:02:23.353Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4f/0fce63e0f5cdd658e71e21abd17ac1bc9312741ebb8b3f74eeed2ebdf771/rpds_py-0.23.1-cp312-cp312-win_amd64.whl", hash = "sha256:b03a8d50b137ee758e4c73638b10747b7c39988eb8e6cd11abb7084266455165", size = 237426, upload-time = "2025-02-21T15:02:25.326Z" }, + { url = "https://files.pythonhosted.org/packages/13/9d/b8b2c0edffb0bed15be17b6d5ab06216f2f47f9ee49259c7e96a3ad4ca42/rpds_py-0.23.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4caafd1a22e5eaa3732acb7672a497123354bef79a9d7ceed43387d25025e935", size = 363672, upload-time = "2025-02-21T15:02:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c2/5056fa29e6894144d7ba4c938b9b0445f75836b87d2dd00ed4999dc45a8c/rpds_py-0.23.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:178f8a60fc24511c0eb756af741c476b87b610dba83270fce1e5a430204566a4", size = 349602, upload-time = "2025-02-21T15:02:27.82Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bc/33779a1bb0ee32d8d706b173825aab75c628521d23ce72a7c1e6a6852f86/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c632419c3870507ca20a37c8f8f5352317aca097639e524ad129f58c125c61c6", size = 388746, upload-time = "2025-02-21T15:02:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/62/0b/71db3e36b7780a619698ec82a9c87ab44ad7ca7f5480913e8a59ff76f050/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:698a79d295626ee292d1730bc2ef6e70a3ab135b1d79ada8fde3ed0047b65a10", size = 397076, upload-time = "2025-02-21T15:02:31.255Z" }, + { url = "https://files.pythonhosted.org/packages/bb/2e/494398f613edf77ba10a916b1ddea2acce42ab0e3b62e2c70ffc0757ce00/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271fa2184cf28bdded86bb6217c8e08d3a169fe0bbe9be5e8d96e8476b707122", size = 448399, upload-time = "2025-02-21T15:02:32.637Z" }, + { url = "https://files.pythonhosted.org/packages/dd/53/4bd7f5779b1f463243ee5fdc83da04dd58a08f86e639dbffa7a35f969a84/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b91cceb5add79ee563bd1f70b30896bd63bc5f78a11c1f00a1e931729ca4f1f4", size = 439764, upload-time = "2025-02-21T15:02:34.372Z" }, + { url = "https://files.pythonhosted.org/packages/f6/55/b3c18c04a460d951bf8e91f2abf46ce5b6426fb69784166a6a25827cb90a/rpds_py-0.23.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a6cb95074777f1ecda2ca4fa7717caa9ee6e534f42b7575a8f0d4cb0c24013", size = 390662, upload-time = "2025-02-21T15:02:36.616Z" }, + { url = "https://files.pythonhosted.org/packages/2a/65/cc463044a3cbd616029b2aa87a651cdee8288d2fdd7780b2244845e934c1/rpds_py-0.23.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50fb62f8d8364978478b12d5f03bf028c6bc2af04082479299139dc26edf4c64", size = 422680, upload-time = "2025-02-21T15:02:38.02Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/1fa52990c7836d72e8d70cd7753f2362c72fbb0a49c1462e8c60e7176d0b/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8f7e90b948dc9dcfff8003f1ea3af08b29c062f681c05fd798e36daa3f7e3e8", size = 561792, upload-time = "2025-02-21T15:02:41.184Z" }, + { url = "https://files.pythonhosted.org/packages/57/b8/fe3b612979b1a29d0c77f8585903d8b3a292604b26d4b300e228b8ac6360/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b98b6c953e5c2bda51ab4d5b4f172617d462eebc7f4bfdc7c7e6b423f6da957", size = 588127, upload-time = "2025-02-21T15:02:42.641Z" }, + { url = "https://files.pythonhosted.org/packages/44/2d/fde474de516bbc4b9b230f43c98e7f8acc5da7fc50ceed8e7af27553d346/rpds_py-0.23.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2893d778d4671ee627bac4037a075168b2673c57186fb1a57e993465dbd79a93", size = 556981, upload-time = "2025-02-21T15:02:43.969Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/767deeb27b81370bbab8f74ef6e68d26c4ea99018f3c71a570e506fede85/rpds_py-0.23.1-cp313-cp313-win32.whl", hash = "sha256:2cfa07c346a7ad07019c33fb9a63cf3acb1f5363c33bc73014e20d9fe8b01cdd", size = 221936, upload-time = "2025-02-21T15:02:45.339Z" }, + { url = "https://files.pythonhosted.org/packages/7d/6c/3474cfdd3cafe243f97ab8474ea8949236eb2a1a341ca55e75ce00cd03da/rpds_py-0.23.1-cp313-cp313-win_amd64.whl", hash = "sha256:3aaf141d39f45322e44fc2c742e4b8b4098ead5317e5f884770c8df0c332da70", size = 237145, upload-time = "2025-02-21T15:02:47.461Z" }, + { url = "https://files.pythonhosted.org/packages/ec/77/e985064c624230f61efa0423759bb066da56ebe40c654f8b5ba225bd5d63/rpds_py-0.23.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:759462b2d0aa5a04be5b3e37fb8183615f47014ae6b116e17036b131985cb731", size = 359623, upload-time = "2025-02-21T15:02:49.02Z" }, + { url = "https://files.pythonhosted.org/packages/62/d9/a33dcbf62b29e40559e012d525bae7d516757cf042cc9234bd34ca4b6aeb/rpds_py-0.23.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3e9212f52074fc9d72cf242a84063787ab8e21e0950d4d6709886fb62bcb91d5", size = 345900, upload-time = "2025-02-21T15:02:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/92/eb/f81a4be6397861adb2cb868bb6a28a33292c2dcac567d1dc575226055e55/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e9f3a3ac919406bc0414bbbd76c6af99253c507150191ea79fab42fdb35982a", size = 386426, upload-time = "2025-02-21T15:02:53.489Z" }, + { url = "https://files.pythonhosted.org/packages/09/47/1f810c9b5e83be005341201b5389f1d240dfa440346ea7189f9b3fd6961d/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c04ca91dda8a61584165825907f5c967ca09e9c65fe8966ee753a3f2b019fe1e", size = 392314, upload-time = "2025-02-21T15:02:55.721Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/bc95831432fd6c46ed8001f01af26de0763a059d6d7e6d69e3c5bf02917a/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4ab923167cfd945abb9b51a407407cf19f5bee35001221f2911dc85ffd35ff4f", size = 447706, upload-time = "2025-02-21T15:02:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/19/3e/567c04c226b1802dc6dc82cad3d53e1fa0a773258571c74ac5d8fbde97ed/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed6f011bedca8585787e5082cce081bac3d30f54520097b2411351b3574e1219", size = 437060, upload-time = "2025-02-21T15:03:00.953Z" }, + { url = "https://files.pythonhosted.org/packages/fe/77/a77d2c6afe27ae7d0d55fc32f6841502648070dc8d549fcc1e6d47ff8975/rpds_py-0.23.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959bb9928c5c999aba4a3f5a6799d571ddc2c59ff49917ecf55be2bbb4e3722", size = 389347, upload-time = "2025-02-21T15:03:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/3f/47/6b256ff20a74cfebeac790ab05586e0ac91f88e331125d4740a6c86fc26f/rpds_py-0.23.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed7de3c86721b4e83ac440751329ec6a1102229aa18163f84c75b06b525ad7e", size = 415554, upload-time = "2025-02-21T15:03:03.816Z" }, + { url = "https://files.pythonhosted.org/packages/fc/29/d4572469a245bc9fc81e35166dca19fc5298d5c43e1a6dd64bf145045193/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb89edee2fa237584e532fbf78f0ddd1e49a47c7c8cfa153ab4849dc72a35e6", size = 557418, upload-time = "2025-02-21T15:03:05.489Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/68cf7228895b1a3f6f39f51b15830e62456795e61193d2c8b87fd48c60db/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7e5413d2e2d86025e73f05510ad23dad5950ab8417b7fc6beaad99be8077138b", size = 583033, upload-time = "2025-02-21T15:03:07.493Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/017ab41dcd6649ad5db7d00155b4c212b31ab05bd857d5ba73a1617984eb/rpds_py-0.23.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d31ed4987d72aabdf521eddfb6a72988703c091cfc0064330b9e5f8d6a042ff5", size = 554880, upload-time = "2025-02-21T15:03:08.967Z" }, + { url = "https://files.pythonhosted.org/packages/2e/dd/17de89431268da8819d8d51ce67beac28d9b22fccf437bc5d6d2bcd1acdb/rpds_py-0.23.1-cp313-cp313t-win32.whl", hash = "sha256:f3429fb8e15b20961efca8c8b21432623d85db2228cc73fe22756c6637aa39e7", size = 219743, upload-time = "2025-02-21T15:03:10.429Z" }, + { url = "https://files.pythonhosted.org/packages/68/15/6d22d07e063ce5e9bfbd96db9ec2fbb4693591b4503e3a76996639474d02/rpds_py-0.23.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d6f6512a90bd5cd9030a6237f5346f046c6f0e40af98657568fa45695d4de59d", size = 235415, upload-time = "2025-02-21T15:03:12.664Z" }, + { url = "https://files.pythonhosted.org/packages/95/a9/6fafd35fc6bac05f59bcbc800b57cef877911ff1c015397c519fec888642/rpds_py-0.23.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c1f8afa346ccd59e4e5630d5abb67aba6a9812fddf764fd7eb11f382a345f8cc", size = 373463, upload-time = "2025-02-21T15:03:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ac/44f00029b8fbe0903a19e9a87a9b86063bf8700df2cc58868373d378418c/rpds_py-0.23.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fad784a31869747df4ac968a351e070c06ca377549e4ace94775aaa3ab33ee06", size = 358400, upload-time = "2025-02-21T15:03:38.752Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9c/3da199346c68d785f10dccab123b74c8c5f73be3f742c9e33d1116e07931/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5a96fcac2f18e5a0a23a75cd27ce2656c66c11c127b0318e508aab436b77428", size = 386815, upload-time = "2025-02-21T15:03:40.216Z" }, + { url = "https://files.pythonhosted.org/packages/d3/45/8f6533c33c0d33da8c2c8b2fb8f2ee90b23c05c679b86b0ac6aee4653749/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e77febf227a1dc3220159355dba68faa13f8dca9335d97504abf428469fb18b", size = 392974, upload-time = "2025-02-21T15:03:42.286Z" }, + { url = "https://files.pythonhosted.org/packages/ca/56/6a9ac1bf0455ba07385d8fe98c571c519b4f2000cff6581487bf9fab9272/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26bb3e8de93443d55e2e748e9fd87deb5f8075ca7bc0502cfc8be8687d69a2ec", size = 446019, upload-time = "2025-02-21T15:03:43.811Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/5d9a3f9731cdccf49088bcc4ce821a5cf50bd1737cdad83e9959a7b9054d/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db7707dde9143a67b8812c7e66aeb2d843fe33cc8e374170f4d2c50bd8f2472d", size = 445811, upload-time = "2025-02-21T15:03:45.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/50/f2e0a98c62fc1fe68b176caca587714dc5c8bb2c3d1dd1eeb2bd4cc787ac/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1eedaaccc9bb66581d4ae7c50e15856e335e57ef2734dbc5fd8ba3e2a4ab3cb6", size = 388070, upload-time = "2025-02-21T15:03:46.905Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d0/4981878f8f157e6dbea01d95e0119bf3d6b4c2c884fe64a9e6987f941104/rpds_py-0.23.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28358c54fffadf0ae893f6c1050e8f8853e45df22483b7fff2f6ab6152f5d8bf", size = 419173, upload-time = "2025-02-21T15:03:48.552Z" }, + { url = "https://files.pythonhosted.org/packages/ce/13/fc971c470da96b270d2f64fedee987351bd935dc3016932a5cdcb1a88a2a/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:633462ef7e61d839171bf206551d5ab42b30b71cac8f10a64a662536e057fdef", size = 559048, upload-time = "2025-02-21T15:03:50.226Z" }, + { url = "https://files.pythonhosted.org/packages/42/02/be91e1de139ec8b4f9fec4192fd779ba48af281cfc762c0ca4c15b945484/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a98f510d86f689fcb486dc59e6e363af04151e5260ad1bdddb5625c10f1e95f8", size = 584773, upload-time = "2025-02-21T15:03:52.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/28/3af8a1956df3edc41d884267d766dc096496dafc83f02f764a475eca0b4a/rpds_py-0.23.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e0397dd0b3955c61ef9b22838144aa4bef6f0796ba5cc8edfc64d468b93798b4", size = 555153, upload-time = "2025-02-21T15:03:55.21Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bb/e45f51c4e1327dea3c72b846c6de129eebacb7a6cb309af7af35d0578c80/rpds_py-0.23.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:75307599f0d25bf6937248e5ac4e3bde5ea72ae6618623b86146ccc7845ed00b", size = 233827, upload-time = "2025-02-21T15:03:56.853Z" }, ] [[package]] @@ -882,132 +882,132 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447 } +sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729 }, + { url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" }, ] [[package]] name = "ruamel-yaml-clib" version = "0.2.12" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301 }, - { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728 }, - { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230 }, - { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712 }, - { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936 }, - { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580 }, - { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393 }, - { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326 }, - { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079 }, - { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 }, - { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 }, - { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 }, - { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 }, - { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 }, - { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 }, - { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 }, - { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 }, - { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 }, - { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433 }, - { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362 }, - { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118 }, - { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497 }, - { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042 }, - { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 }, - { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692 }, - { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 }, - { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 }, - { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 }, - { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 }, - { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 }, - { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 }, - { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 }, - { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 }, - { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059 }, - { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 }, - { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 }, +sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301, upload-time = "2024-10-20T10:12:35.876Z" }, + { url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728, upload-time = "2024-10-20T10:12:37.858Z" }, + { url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230, upload-time = "2024-10-20T10:12:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712, upload-time = "2024-10-20T10:12:41.119Z" }, + { url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936, upload-time = "2024-10-21T11:26:37.419Z" }, + { url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580, upload-time = "2024-10-21T11:26:39.503Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393, upload-time = "2024-12-11T19:58:13.873Z" }, + { url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326, upload-time = "2024-10-20T10:12:42.967Z" }, + { url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079, upload-time = "2024-10-20T10:12:44.117Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224, upload-time = "2024-10-20T10:12:45.162Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480, upload-time = "2024-10-20T10:12:46.758Z" }, + { url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068, upload-time = "2024-10-20T10:12:48.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012, upload-time = "2024-10-20T10:12:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352, upload-time = "2024-10-21T11:26:41.438Z" }, + { url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344, upload-time = "2024-10-21T11:26:43.62Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498, upload-time = "2024-12-11T19:58:15.592Z" }, + { url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205, upload-time = "2024-10-20T10:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185, upload-time = "2024-10-20T10:12:54.652Z" }, + { url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433, upload-time = "2024-10-20T10:12:55.657Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362, upload-time = "2024-10-20T10:12:57.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118, upload-time = "2024-10-20T10:12:58.501Z" }, + { url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497, upload-time = "2024-10-20T10:13:00.211Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042, upload-time = "2024-10-21T11:26:46.038Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831, upload-time = "2024-10-21T11:26:47.487Z" }, + { url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692, upload-time = "2024-12-11T19:58:17.252Z" }, + { url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777, upload-time = "2024-10-20T10:13:01.395Z" }, + { url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523, upload-time = "2024-10-20T10:13:02.768Z" }, + { url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011, upload-time = "2024-10-20T10:13:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488, upload-time = "2024-10-20T10:13:05.906Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066, upload-time = "2024-10-20T10:13:07.26Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785, upload-time = "2024-10-20T10:13:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017, upload-time = "2024-10-21T11:26:48.866Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270, upload-time = "2024-10-21T11:26:50.213Z" }, + { url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059, upload-time = "2024-12-11T19:58:18.846Z" }, + { url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583, upload-time = "2024-10-20T10:13:09.658Z" }, + { url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" }, ] [[package]] name = "ruff" version = "0.11.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511, upload-time = "2025-03-21T13:31:17.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, - { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, - { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, - { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, - { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, - { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, - { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, - { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, - { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, - { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, - { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, - { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, - { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, - { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, - { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, - { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, - { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146, upload-time = "2025-03-21T13:30:26.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092, upload-time = "2025-03-21T13:30:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082, upload-time = "2025-03-21T13:30:39.962Z" }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818, upload-time = "2025-03-21T13:30:42.551Z" }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251, upload-time = "2025-03-21T13:30:45.196Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566, upload-time = "2025-03-21T13:30:47.516Z" }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721, upload-time = "2025-03-21T13:30:49.56Z" }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274, upload-time = "2025-03-21T13:30:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284, upload-time = "2025-03-21T13:30:54.24Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861, upload-time = "2025-03-21T13:30:56.757Z" }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560, upload-time = "2025-03-21T13:30:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091, upload-time = "2025-03-21T13:31:01.45Z" }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133, upload-time = "2025-03-21T13:31:04.013Z" }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514, upload-time = "2025-03-21T13:31:06.166Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835, upload-time = "2025-03-21T13:31:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713, upload-time = "2025-03-21T13:31:13.148Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990, upload-time = "2025-03-21T13:31:15.206Z" }, ] [[package]] name = "semver" version = "3.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/6c/a536cc008f38fd83b3c1b98ce19ead13b746b5588c9a0cb9dd9f6ea434bc/semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc", size = 214988 } +sdist = { url = "https://files.pythonhosted.org/packages/41/6c/a536cc008f38fd83b3c1b98ce19ead13b746b5588c9a0cb9dd9f6ea434bc/semver-3.0.2.tar.gz", hash = "sha256:6253adb39c70f6e51afed2fa7152bcd414c411286088fb4b9effb133885ab4cc", size = 214988, upload-time = "2023-10-09T11:58:25.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/77/0cc7a8a3bc7e53d07e8f47f147b92b0960e902b8254859f4aee5c4d7866b/semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4", size = 17099 }, + { url = "https://files.pythonhosted.org/packages/9a/77/0cc7a8a3bc7e53d07e8f47f147b92b0960e902b8254859f4aee5c4d7866b/semver-3.0.2-py3-none-any.whl", hash = "sha256:b1ea4686fe70b981f85359eda33199d60c53964284e0cfb4977d243e37cf4bf4", size = 17099, upload-time = "2023-10-09T11:58:24.128Z" }, ] [[package]] name = "setuptools" version = "78.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/f4/aa8d364f0dc1f33b2718938648c31202e2db5cd6479a73f0a9ca5a88372d/setuptools-78.0.2.tar.gz", hash = "sha256:137525e6afb9022f019d6e884a319017f9bf879a0d8783985d32cbc8683cab93", size = 1367747 } +sdist = { url = "https://files.pythonhosted.org/packages/4c/f4/aa8d364f0dc1f33b2718938648c31202e2db5cd6479a73f0a9ca5a88372d/setuptools-78.0.2.tar.gz", hash = "sha256:137525e6afb9022f019d6e884a319017f9bf879a0d8783985d32cbc8683cab93", size = 1367747, upload-time = "2025-03-24T19:50:41.896Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/db/2fd473dfe436ad19fda190f4079162d400402aedfcc41e048d38c0a375c6/setuptools-78.0.2-py3-none-any.whl", hash = "sha256:4a612c80e1f1d71b80e4906ce730152e8dec23df439f82731d9d0b608d7b700d", size = 1255965 }, + { url = "https://files.pythonhosted.org/packages/aa/db/2fd473dfe436ad19fda190f4079162d400402aedfcc41e048d38c0a375c6/setuptools-78.0.2-py3-none-any.whl", hash = "sha256:4a612c80e1f1d71b80e4906ce730152e8dec23df439f82731d9d0b608d7b700d", size = 1255965, upload-time = "2025-03-24T19:50:39.943Z" }, ] [[package]] name = "shellingham" version = "1.5.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] [[package]] name = "smmap" version = "5.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, ] [[package]] name = "soupsieve" version = "2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/ce/fbaeed4f9fb8b2daa961f90591662df6a86c1abf25c548329a86920aedfb/soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", size = 101569, upload-time = "2024-08-13T13:39:12.166Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186 }, + { url = "https://files.pythonhosted.org/packages/d1/c2/fe97d779f3ef3b15f05c94a2f1e3d21732574ed441687474db9d342a7315/soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9", size = 36186, upload-time = "2024-08-13T13:39:10.986Z" }, ] [[package]] @@ -1018,36 +1018,36 @@ dependencies = [ { name = "dataproperty" }, { name = "typepy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/35/171c8977162f1163368406deddde4c59673b62bd0cb2f34948a02effb075/tabledata-1.3.4.tar.gz", hash = "sha256:e9649cab129d718f3bff4150083b77f8a78c30f6634a30caf692b10fdc60cb97", size = 25074 } +sdist = { url = "https://files.pythonhosted.org/packages/b2/35/171c8977162f1163368406deddde4c59673b62bd0cb2f34948a02effb075/tabledata-1.3.4.tar.gz", hash = "sha256:e9649cab129d718f3bff4150083b77f8a78c30f6634a30caf692b10fdc60cb97", size = 25074, upload-time = "2024-12-31T14:12:31.198Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/08/64/fa4160151976ee4b2cf0c1217a99443ffaeb991956feddfeac9eee9952f8/tabledata-1.3.4-py3-none-any.whl", hash = "sha256:1f56e433bfdeb89f4487abfa48c4603a3b07c5d3a3c7e05ff73dd018c24bd0d4", size = 11820 }, + { url = "https://files.pythonhosted.org/packages/08/64/fa4160151976ee4b2cf0c1217a99443ffaeb991956feddfeac9eee9952f8/tabledata-1.3.4-py3-none-any.whl", hash = "sha256:1f56e433bfdeb89f4487abfa48c4603a3b07c5d3a3c7e05ff73dd018c24bd0d4", size = 11820, upload-time = "2024-12-31T14:12:28.584Z" }, ] [[package]] name = "tcolorpy" version = "0.1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/cc/44f2d81d8f9093aad81c3467a5bf5718d2b5f786e887b6e4adcfc17ec6b9/tcolorpy-0.1.7.tar.gz", hash = "sha256:0fbf6bf238890bbc2e32662aa25736769a29bf6d880328f310c910a327632614", size = 299437 } +sdist = { url = "https://files.pythonhosted.org/packages/80/cc/44f2d81d8f9093aad81c3467a5bf5718d2b5f786e887b6e4adcfc17ec6b9/tcolorpy-0.1.7.tar.gz", hash = "sha256:0fbf6bf238890bbc2e32662aa25736769a29bf6d880328f310c910a327632614", size = 299437, upload-time = "2024-12-29T15:24:23.847Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/a2/ed023f2edd1e011b4d99b6727bce8253842d66c3fbf9ed0a26fc09a92571/tcolorpy-0.1.7-py3-none-any.whl", hash = "sha256:26a59d52027e175a37e0aba72efc99dda43f074db71f55b316d3de37d3251378", size = 8096 }, + { url = "https://files.pythonhosted.org/packages/05/a2/ed023f2edd1e011b4d99b6727bce8253842d66c3fbf9ed0a26fc09a92571/tcolorpy-0.1.7-py3-none-any.whl", hash = "sha256:26a59d52027e175a37e0aba72efc99dda43f074db71f55b316d3de37d3251378", size = 8096, upload-time = "2024-12-29T15:24:21.33Z" }, ] [[package]] name = "text-unidecode" version = "1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885 } +sdist = { url = "https://files.pythonhosted.org/packages/ab/e2/e9a00f0ccb71718418230718b3d900e71a5d16e701a3dae079a21e9cd8f8/text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93", size = 76885, upload-time = "2019-08-30T21:36:45.405Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154 }, + { url = "https://files.pythonhosted.org/packages/a6/a5/c0b6468d3824fe3fde30dbb5e1f687b291608f9473681bbf7dabbf5a87d7/text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", size = 78154, upload-time = "2019-08-30T21:37:03.543Z" }, ] [[package]] name = "tomli" version = "2.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, ] [[package]] @@ -1057,9 +1057,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mbstrdecoder" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/79/59/4c39942077d7de285f762a91024dbda731be693591732977358f77d120fb/typepy-1.3.4.tar.gz", hash = "sha256:89c1f66de6c6133209c43a94d23431d320ba03ef5db18f241091ea594035d9de", size = 39558 } +sdist = { url = "https://files.pythonhosted.org/packages/79/59/4c39942077d7de285f762a91024dbda731be693591732977358f77d120fb/typepy-1.3.4.tar.gz", hash = "sha256:89c1f66de6c6133209c43a94d23431d320ba03ef5db18f241091ea594035d9de", size = 39558, upload-time = "2024-12-29T09:18:15.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/31/e393c3830bdedd01735bd195c85ac3034b6bcaf6c18142bab60a4047ca36/typepy-1.3.4-py3-none-any.whl", hash = "sha256:d5ed3e0c7f49521bff0603dd08cf8d453371cf68d65a29d3d0038552ccc46e2e", size = 31449 }, + { url = "https://files.pythonhosted.org/packages/ee/31/e393c3830bdedd01735bd195c85ac3034b6bcaf6c18142bab60a4047ca36/typepy-1.3.4-py3-none-any.whl", hash = "sha256:d5ed3e0c7f49521bff0603dd08cf8d453371cf68d65a29d3d0038552ccc46e2e", size = 31449, upload-time = "2024-12-29T09:18:13.135Z" }, ] [package.optional-dependencies] @@ -1071,7 +1071,7 @@ datetime = [ [[package]] name = "typer" -version = "0.12.3" +version = "0.17.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -1079,45 +1079,45 @@ dependencies = [ { name = "shellingham" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/0a/d55af35db5f50f486e3eda0ada747eed773859e2699d3ce570b682a9b70a/typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482", size = 94276 } +sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b5/11cf2e34fbb11b937e006286ab5b8cfd334fde1c8fa4dd7f491226931180/typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914", size = 47209 }, + { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, ] [[package]] name = "types-python-dateutil" version = "2.9.0.20241206" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/60/47d92293d9bc521cd2301e423a358abfac0ad409b3a1606d8fbae1321961/types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb", size = 13802, upload-time = "2024-12-06T02:56:41.019Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384 }, + { url = "https://files.pythonhosted.org/packages/0f/b3/ca41df24db5eb99b00d97f89d7674a90cb6b3134c52fb8121b6d8d30f15c/types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53", size = 14384, upload-time = "2024-12-06T02:56:39.412Z" }, ] [[package]] name = "typing-extensions" version = "4.12.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321, upload-time = "2024-06-07T18:52:15.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438, upload-time = "2024-06-07T18:52:13.582Z" }, ] [[package]] name = "urllib3" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" }, ] [[package]] name = "xmltodict" version = "0.13.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/0d/40df5be1e684bbaecdb9d1e0e40d5d482465de6b00cbb92b84ee5d243c7f/xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", size = 33813 } +sdist = { url = "https://files.pythonhosted.org/packages/39/0d/40df5be1e684bbaecdb9d1e0e40d5d482465de6b00cbb92b84ee5d243c7f/xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56", size = 33813, upload-time = "2022-05-08T07:00:04.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/db/fd0326e331726f07ff7f40675cd86aa804bfd2e5016c727fa761c934990e/xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852", size = 9971 }, + { url = "https://files.pythonhosted.org/packages/94/db/fd0326e331726f07ff7f40675cd86aa804bfd2e5016c727fa761c934990e/xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852", size = 9971, upload-time = "2022-05-08T07:00:02.898Z" }, ] [[package]] @@ -1129,4 +1129,4 @@ dependencies = [ { name = "lxml" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/23/c4/41b713c4ce58fd86dc8c1cdcf47c262451fa886173d477c63844f6111945/zpretty-3.1.0.tar.gz", hash = "sha256:6ce274007e61eb449a4d07f58d2ec31b07b1aea0bbe1d37e423dc245a16b2293", size = 36441 } +sdist = { url = "https://files.pythonhosted.org/packages/23/c4/41b713c4ce58fd86dc8c1cdcf47c262451fa886173d477c63844f6111945/zpretty-3.1.0.tar.gz", hash = "sha256:6ce274007e61eb449a4d07f58d2ec31b07b1aea0bbe1d37e423dc245a16b2293", size = 36441, upload-time = "2023-06-30T12:51:40.541Z" }