📦

cultiva-nlp  —  Guía de Empaquetado Python

CULTIVA IA · IA-Ingenieria-MLOps · v0.1.0 · hatchling + PyPI
Python ≥ 3.9 hatchling PyPI Ready CLI: click spacy · langdetect MIT License GitHub Actions
Estructura del repositorio (src-layout)
cultiva-nlp/ ├── pyproject.toml # config única PEP 621 ├── README.md ├── CHANGELOG.md ├── LICENSE # MIT ├── .gitignore ├── src/ │ └── cultiva_nlp/ │ ├── __init__.py # __version__ │ ├── cleaner.py # limpieza texto ES │ ├── detector.py # detección idioma │ ├── extractor.py # entidades NER │ ├── cli.py # entry-point CLI │ └── py.typed # marcador PEP 561 ├── tests/ │ ├── __init__.py │ ├── test_cleaner.py │ ├── test_detector.py │ └── test_extractor.py └── .github/workflows/ └── publish.yml # CD → PyPI
¿Por qué src-layout?
  • ✅ Importar el paquete instalado, no el fuente
  • ✅ Evita colisiones de nombres en tests
  • ✅ Estándar recomendado por PyPA 2024
  • ✅ Compatible con pip install -e .
Backend: hatchling
  • ⚡ Sin setup.py ni MANIFEST.in
  • ⚡ Auto-detección de paquetes en src/
  • ⚡ Versionado dinámico vía __version__
  • ⚡ Compatible con PEP 517/518/660
pyproject.toml completo
pyproject.toml
TOML · PEP 621
# ─── Build system ──────────────────────────────────────────
[build-system]
requires      = ["hatchling>=1.18"]
build-backend = "hatchling.build"

# ─── Proyecto ───────────────────────────────────────────────
[project]
name            = "cultiva-nlp"
version         = "0.1.0"
description     = "Utilidades NLP en español para proyectos CULTIVA IA"
readme          = "README.md"
requires-python = ">=3.9"
license         = { text = "MIT" }
authors         = [{ name = "Álvaro Gimeno", email = "alvaro@cultivaia.es" }]
keywords        = ["nlp", "español", "text-processing", "ner", "langdetect"]
classifiers     = [
    "Development Status :: 3 - Alpha",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10",
    "Programming Language :: Python :: 3.11",
    "Programming Language :: Python :: 3.12",
    "Topic :: Scientific/Engineering :: Artificial Intelligence",
    "Natural Language :: Spanish",
]
dependencies = [
    "spacy>=3.5,<4.0",
    "langdetect>=1.0.9",
    "click>=8.1",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.4",
    "pytest-cov>=4.1",
    "ruff>=0.3",
    "mypy>=1.8",
    "types-requests",
]
docs = ["mkdocs>=1.5", "mkdocs-material>=9.4"]

[project.urls]
Homepage      = "https://github.com/cultivaia/cultiva-nlp"
Documentation = "https://cultivaia.github.io/cultiva-nlp"
"Bug Tracker" = "https://github.com/cultivaia/cultiva-nlp/issues"

[project.scripts]
cultiva-nlp = "cultiva_nlp.cli:main"

# ─── Hatchling: empaqueta todo src/ ─────────────────────────
[tool.hatch.build.targets.wheel]
packages = ["src/cultiva_nlp"]

# ─── Ruff (linter + formatter) ──────────────────────────────
[tool.ruff]
line-length    = 100
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "UP", "ANN"]
ignore = ["ANN101"]

# ─── Mypy ───────────────────────────────────────────────────
[tool.mypy]
python_version      = "3.9"
disallow_untyped_defs = true
warn_return_any     = true

# ─── Pytest ─────────────────────────────────────────────────
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts   = "-v --cov=cultiva_nlp --cov-report=term-missing"

[tool.coverage.run]
source = ["src"]
omit   = ["*/tests/*"]
Módulos principales
src/cultiva_nlp/__init__.py
Python
from importlib.metadata import version

__version__ = version("cultiva-nlp")
__author__  = "Álvaro Gimeno"
__email__   = "alvaro@cultivaia.es"

from .cleaner   import TextCleaner
from .detector  import LanguageDetector
from .extractor import EntityExtractor

__all__ = [
    "TextCleaner",
    "LanguageDetector",
    "EntityExtractor",
    "__version__",
]
src/cultiva_nlp/cli.py
Python · Click
import click
from . import TextCleaner, LanguageDetector, EntityExtractor

@click.group()
@click.version_option()
def cli() -> None:
    """Herramientas NLP para texto en español."""

@cli.command()
@click.argument("text")
def clean(text: str) -> None:
    """Limpia y normaliza TEXT."""
    result = TextCleaner().clean(text)
    click.echo(result)

@cli.command("detect-lang")
@click.argument("text")
def detect_lang(text: str) -> None:
    """Detecta el idioma de TEXT."""
    lang = LanguageDetector().detect(text)
    click.echo(lang)

@cli.command()
@click.argument("text")
@click.option("--type", "entity_type",
              default="all",
              type=click.Choice(["all","ORG","PER","DATE"]))
def extract(text: str, entity_type: str) -> None:
    """Extrae entidades de TEXT."""
    entities = EntityExtractor().extract(text, entity_type)
    for e in entities:
        click.echo(f"{e['type']:8} {e['text']}")

def main() -> None:
    cli()
Flujo de build y publicación
🔧
Instalar deps
pip install build twine
🏗️
Build
python -m build
Validar
twine check dist/*
🧪
TestPyPI
twine upload --repo testpypi
🚀
PyPI
twine upload dist/*
Comandos locales — paso a paso
bash
# 1. Instalar en modo editable (desarrollo)
pip install -e ".[dev]"

# 2. Verificar que el CLI funciona
cultiva-nlp --version
# → cultiva-nlp, version 0.1.0

cultiva-nlp clean "Hola!! Esto es un   texto con  espacios 😎"
# → Hola esto es un texto con espacios

cultiva-nlp detect-lang "Bonjour tout le monde"
# → fr

cultiva-nlp extract "Apple compró Beats en 2014 por 3.000M€" --type all
# → ORG      Apple
# → ORG      Beats
# → DATE     2014

# 3. Linting y tipos
ruff check src/ tests/
mypy src/

# 4. Tests con cobertura
pytest --cov=cultiva_nlp --cov-report=html

# 5. Build artefactos
python -m build
# dist/
#   cultiva_nlp-0.1.0.tar.gz      (sdist)
#   cultiva_nlp-0.1.0-py3-none-any.whl  (wheel)

# 6. Validar paquete
twine check dist/*

# 7. Publicar en TestPyPI primero
twine upload --repository testpypi dist/*

# 8. Probar instalación desde TestPyPI
pip install --index-url https://test.pypi.org/simple/ cultiva-nlp

# 9. Publicar en PyPI (producción)
twine upload dist/*
GitHub Actions — CD automático a PyPI
.github/workflows/publish.yml
YAML · GitHub Actions
name: Publish cultiva-nlp to PyPI

on:
  release:
    types: [created]
  workflow_dispatch:  # disparo manual

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.9", "3.11", "3.12"]
    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install cultiva-nlp[dev]
        run: pip install -e ".[dev]"
      - name: Lint (ruff)
        run: ruff check src/ tests/
      - name: Type check (mypy)
        run: mypy src/
      - name: Tests
        run: pytest

  publish:
    needs: test
    runs-on: ubuntu-latest
    environment:
      name: pypi
      url: https://pypi.org/p/cultiva-nlp
    permissions:
      id-token: write  # OIDC trusted publishing
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Build
        run: |
          pip install build
          python -m build
      - name: Publish (OIDC — sin secrets)
        uses: pypa/gh-action-pypi-publish@release/v1
OIDC Trusted Publishing — Se usa pypa/gh-action-pypi-publish con permisos OIDC, eliminando la necesidad de almacenar PYPI_API_TOKEN como secreto. Requiere configurar el "trusted publisher" en la web de PyPI apuntando al repo.
Comparativa de backends de build
Backend setup.py necesario PEP 621 nativo Editable installs Extensiones C Recomendado para
hatchling ✓ PEP 660 Paquetes Python puros modernos
setuptools opcional Proyectos con C/Cython o legado
flit Librerías Python puras minimalistas
poetry parcial Gestión de deps + empaquetado todo-en-uno
maturin ✓ Rust Extensiones Rust/PyO3
Checklist de release
Antes del primer release
1

Registrar cuenta en PyPI + TestPyPI

Crear cuenta en pypi.org y test.pypi.org con 2FA activado.

2

Configurar Trusted Publisher en PyPI

En PyPI > tu cuenta > Publishing: añadir repo cultivaia/cultiva-nlp, workflow publish.yml.

3

Verificar nombre único

Comprobar que cultiva-nlp no existe: pip index versions cultiva-nlp

4

Añadir py.typed

Archivo vacío en src/cultiva_nlp/py.typed para declarar soporte PEP 561 (type hints).

Cada nuevo release
1

Actualizar versión en pyproject.toml

Seguir SemVer: MAJOR.MINOR.PATCH (0.1.0 → 0.2.0 → 1.0.0)

2

Añadir entrada en CHANGELOG.md

Fecha, versión y lista de cambios (Added / Changed / Fixed / Removed).

3

Crear GitHub Release

Tag v0.2.0 → el workflow publish.yml se dispara automáticamente.

4

Verificar en PyPI

pip install cultiva-nlp==0.2.0 en entorno limpio.

Configuración local (.pypirc)
~/.pypirc
INI
[distutils]
index-servers =
    pypi
    testpypi

[pypi]
username = __token__
password = pypi-AgE...token_real_aqui

[testpypi]
repository = https://test.pypi.org/legacy/
username   = __token__
password   = pypi-AgE...test_token_aqui
Seguridad: Nunca subas ~/.pypirc al repositorio. Añade *.pypirc a tu ~/.gitignore global. En CI, usa OIDC Trusted Publishing en lugar de tokens.
Alternativa keyring: pip install keyring y luego keyring set https://upload.pypi.org/legacy/ __token__ para guardar el token de forma segura en el llavero del sistema operativo.