diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93d0f26b..5c4120a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,6 +7,7 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml + exclude: "^conda/recipe/" - id: check-added-large-files - id: check-ast diff --git a/MANIFEST.in b/MANIFEST.in index d4fed2ea..3ea00dd9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,4 @@ recursive-include pysus *.c *.h include pysus/utilities/* include requirements.txt +include LICENSE diff --git a/README.md b/README.md index 5dffa0af..dda5213c 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,19 @@ conda activate pysus poetry install ``` +### Conda-Forge Recipe + +The conda recipe is **auto-generated from `pyproject.toml`**. After releasing a new version, +checkout to the main branch and run: + +```bash +python conda/generate_recipe.py +``` + +This reads `pyproject.toml` and writes `conda/recipe/meta.yaml` with the correct conda-forge package names, version constraints, and the current SHA256 (fetched from PyPI). Never edit `meta.yaml` by hand. + +To submit or update the recipe, copy it into the [pysus-feedstock](https://github.com/conda-forge/pysus-feedstock) repo. + ### Running Tests Run code linters: diff --git a/conda/generate_recipe.py b/conda/generate_recipe.py new file mode 100644 index 00000000..a3377244 --- /dev/null +++ b/conda/generate_recipe.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Generate conda v1 recipe (recipe.yaml) via grayskull. + +Usage: + python conda/generate_recipe.py + +Runs ``grayskull pypi --strict-conda-forge --use-v1-format pysus``, +patches the result with project-specific overrides, and writes to +``conda/recipe/recipe.yaml``. +""" + +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +from ruamel.yaml import YAML + +RECIPE_PATH = Path(__file__).resolve().parent / "recipe" / "recipe.yaml" + +MAINTAINERS: list[str] = [ + "fccoelho", + "luabida", + "esloch", +] + +CONDA_NAME_MAP: dict[str, str] = { + "wget": "python-wget", + "dotenv": "python-dotenv", + "python-duckdb": "duckdb", +} + +SKIP_DEPS: set[str] = { + "python-magic-bin", +} + +CONDA_VERSION_OVERRIDES: dict[str, str] = { + "duckdb-engine": ">=0.15.0", +} + +yaml = YAML() +yaml.preserve_quotes = True +yaml.indent(mapping=2, sequence=4, offset=2) + + +def strip_post(spec: str) -> str: + return re.sub(r"\.post\d+", "", spec) + + +def drop_upper_bounds(spec: str) -> str: + return re.sub(r"\s*,?\s*<[=>]?\s*[\d.*]+", "", spec) + + +def drop_trailing_dotstar(spec: str) -> str: + return re.sub(r"\.\*$", "", spec) + + +def clean_constraint(spec: str) -> str: + spec = strip_post(spec) + spec = drop_upper_bounds(spec) + return spec + + +def patch_recipe(recipe: dict) -> dict: + version = recipe["context"]["version"] + recipe["context"] = {"version": version, "python_min": "3.11"} + + about = recipe["about"] + about["homepage"] = "https://github.com/AlertaDengue/PySUS" + about["documentation"] = "https://pysus.readthedocs.io/" + about["repository"] = "https://github.com/AlertaDengue/PySUS" + about["description"] = ( + "PySUS is a Python package for downloading, parsing, and analyzing " + "Brazil's Public Health data (DATASUS). It provides tools for " + "fetching data from various sources including FTP servers, DadosGov " + "API, and DuckLake (S3-based catalog), also contains utilities for " + "reading DBC/DBF file formats present in DATASUS." + ) + + recipe["extra"]["recipe-maintainers"] = MAINTAINERS + + recipe["requirements"]["host"] = [ + "python ${{ python_min }}.*" if d.startswith("python ") else d + for d in recipe["requirements"]["host"] + ] + + run_deps: list[str] = recipe["requirements"]["run"] + new_run: list[str] = [] + for dep in run_deps: + parts = dep.split(None, 1) + name = parts[0] + constraint = parts[1] if len(parts) > 1 else "" + + if name == "python": + new_run.append("python >=${{ python_min }}") + continue + + if name in SKIP_DEPS: + continue + + if name in CONDA_NAME_MAP: + name = CONDA_NAME_MAP[name] + + if name in CONDA_VERSION_OVERRIDES: + constraint = CONDA_VERSION_OVERRIDES[name] + elif constraint: + constraint = clean_constraint(constraint) + + new_run.append(f"{name} {constraint}".strip()) + + recipe["requirements"]["run"] = new_run + + recipe["tests"] = [ + { + "python": { + "imports": ["pysus"], + "pip_check": True, + "python_version": [ + "${{ python_min }}.*", + "*", + ], + }, + "requirements": {"run": ["pip"]}, + "script": ["pysus --help"], + } + ] + + return recipe + + +def main() -> None: + with tempfile.TemporaryDirectory() as tmpdir: + result = subprocess.run( + [ + "grayskull", + "pypi", + "--strict-conda-forge", + "--use-v1-format", + "pysus", + "-o", + tmpdir, + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(result.stderr, file=sys.stderr) + sys.exit(1) + + src = Path(tmpdir) / "pysus" / "recipe.yaml" + recipe = yaml.load(src.read_text()) + recipe = patch_recipe(recipe) + + RECIPE_PATH.parent.mkdir(parents=True, exist_ok=True) + with open(RECIPE_PATH, "w") as fh: + yaml.dump(recipe, fh) + print(f"Wrote {RECIPE_PATH}") + + +if __name__ == "__main__": + main() diff --git a/conda/recipe/recipe.yaml b/conda/recipe/recipe.yaml new file mode 100644 index 00000000..82eaede7 --- /dev/null +++ b/conda/recipe/recipe.yaml @@ -0,0 +1,86 @@ +schema_version: 1 + +context: + version: "2.6.3" + python_min: '3.11' +package: + name: pysus + version: ${{ version }} + +source: + url: https://pypi.org/packages/source/p/pysus/pysus-${{ version }}.tar.gz + sha256: e537a501befafc055406f3ed820175e863f95033189a4a2c130ade18087bc076 + +build: + number: 0 + noarch: python + script: ${{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation + python: + entry_points: + - pysus = pysus.cli:app + +requirements: + host: + - python ${{ python_min }}.* + - poetry-core + - pip + run: + - python >=${{ python_min }} + - python-dateutil 2.8.2.* + - fastparquet >=2023.10.1 + - pyarrow >=11.0.0 + - numpy >=2.4.0 + - tqdm >=4.67.0 + - python-wget >=3.2.0 + - loguru >=0.6.0 + - unidecode >=1.3.6 + - dateparser >=1.1.8 + - pandas >=2.2.2 + - typing_extensions >=4.10.0 + - pydantic >=2.12.5 + - duckdb >=1.4.4 + - duckdb-engine >=0.15.0 + - sqlalchemy >=2.0.48 + - python-magic + - chardet >=7.4.0 + - anyio >=4.13.0 + - httpx >=0.28.0 + - aioftp >=0.21.4 + - dbfread 2.0.7.* + - bigtree >=0.12.2 + - pyreaddbc >=2.0.4 + - python-dotenv >=0.9.9 + - boto3 >=1.42.89 + - typer >=0.24.1 + - humanize >=4.8.0 +tests: + - python: + imports: + - pysus + pip_check: true + python_version: + - ${{ python_min }}.* + - '*' + requirements: + run: + - pip + script: + - pysus --help +about: + summary: Tools for dealing with Brazil's Public health data + license: GPL-3.0-only + license_file: LICENSE + homepage: https://github.com/AlertaDengue/PySUS + + documentation: https://pysus.readthedocs.io/ + repository: https://github.com/AlertaDengue/PySUS + description: PySUS is a Python package for downloading, parsing, and analyzing + Brazil's Public Health data (DATASUS). It provides tools for fetching data + from various sources including FTP servers, DadosGov API, and DuckLake + (S3-based catalog), also contains utilities for reading DBC/DBF file formats + present in DATASUS. +extra: + recipe-maintainers: + - fccoelho + - luabida + - esloch diff --git a/pyproject.toml b/pyproject.toml index b6ecd720..fe246088 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,8 @@ name = "pysus" version = "2.6.3" # changed by semantic-release description = "Tools for dealing with Brazil's Public health data" authors = ["Flavio Codeco Coelho ", "Luã Bida Vacaro "] -license = "GPL" +license = "GPL-3.0-only" +readme = "README.md" packages = [{ include = "pysus"}]