Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added armasec_lite/py.typed
Empty file.
63 changes: 59 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,60 @@ name = "armasec-lite"
version = "0.1.3"
description = "Injectable FastAPI auth via OIDC, with three dependencies"
authors = [{name = "Vantage Compute", email = "info@vantagecompute.ai"}]
license = {text = "MIT"}
maintainers = [{name = "Vantage Compute", email = "info@vantagecompute.ai"}]
# PEP 639 SPDX expression, not the older `{text = "MIT"}` table. Metadata 2.4 forbids a
# distribution from carrying both a License-Expression and a `License ::` classifier, so
# the "License :: OSI Approved :: MIT License" classifier was removed with this change.
# Keeping both makes PyPI reject the upload.
license = "MIT"
license-files = ["LICENSE"]
readme = "README.md"
requires-python = ">=3.12"
keywords = ["fastapi", "auth", "oidc", "oauth2", "security", "jwt"]
keywords = [
"fastapi",
"auth",
"authentication",
"authorization",
"oidc",
"openid-connect",
"oauth2",
"keycloak",
"auth0",
"security",
"jwt",
"jwks",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Intended Audience :: System Administrators",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Framework :: FastAPI",
"Framework :: Pytest",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Security",
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: System :: Systems Administration :: Authentication/Directory",
"Typing :: Typed",
]
dependencies = [
"fastapi>=0.141.1,<1",
"cryptography>=50.0.1,<51",
"pydantic>=2.13.5,<3",
]

# PyPI renders these as the project page's sidebar links, and it recognises these five
# names specifically, giving each its own icon. A sixth name under some other label would
# render as a plain bullet, so it is not worth adding one.
[project.urls]
Homepage = "https://github.com/vantagecompute/armasec-lite"
Homepage = "https://docs.vantagecompute.ai/developer/armasec-lite/"
Documentation = "https://docs.vantagecompute.ai/developer/armasec-lite/"
Repository = "https://github.com/vantagecompute/armasec-lite"
Issues = "https://github.com/vantagecompute/armasec-lite/issues"
Changelog = "https://github.com/vantagecompute/armasec-lite/releases"

[project.optional-dependencies]
test = ["pytest>=9.1.1,<10"]
Expand Down Expand Up @@ -79,3 +110,27 @@ build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["armasec_lite"]

# An explicit allow-list, because hatchling's default is to ship everything the ROOT
# .gitignore does not exclude, and it does not read nested ignore files. `node_modules`
# is ignored by docusaurus/.gitignore, so the default sdist swept in 574MB of it and
# compressed to a 115.7MB tarball: over PyPI's 100MB per-file limit, and therefore a
# release that fails at upload rather than at build. Naming the contents is also the
# only way the list stays reviewable as the repository grows a fourth or fifth
# top-level directory of benchmark harness and docs site.
[tool.hatch.build.targets.sdist]
include = [
"armasec_lite",
"tests",
"examples",
"README.md",
"LICENSE",
]
# `include` alone is not sufficient. Hatchling matches `README*` and `LICEN[CS]E*`
# recursively for the sdist, so an include list naming only the two root files still
# pulled in 1888 of them from under docusaurus/node_modules. These two entries are what
# actually take the tarball from 115.7MB to 105KB.
exclude = [
"docusaurus",
"legacy_comparison_compose",
]
80 changes: 80 additions & 0 deletions tests/unit/test_packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,83 @@ def test_importing_armasec_lite_does_not_load_pem_serialization():
[sys.executable, "-c", code], capture_output=True, text=True, check=True
)
assert result.stdout.strip() == "False"


def test_distribution_metadata_carries_the_pypi_project_page_fields():
"""
The published metadata must fill in every field PyPI renders on a project page.

PyPI builds its sidebar from installed metadata, not from the repository, so a field
left out of `pyproject.toml` shows up as a missing link or an empty Meta section on
the release page rather than as a build failure. This test is what makes that
omission visible here instead of after an upload.

`License-Expression` rather than a `License ::` classifier is deliberate: PEP 639
metadata (2.4) rejects a distribution that carries both, so the classifier was
removed when the SPDX expression was added.
"""
metadata = importlib.metadata.metadata("armasec-lite")

assert metadata["Summary"]
assert metadata.get("Author") or metadata.get("Author-email")
assert metadata["Requires-Python"]
assert metadata["Description-Content-Type"] == "text/markdown"

urls = {
value.split(",", 1)[0].strip(): value.split(",", 1)[1].strip()
for value in metadata.get_all("Project-URL") or []
}
assert {"Homepage", "Documentation", "Repository", "Issues", "Changelog"} <= urls.keys()

classifiers = set(metadata.get_all("Classifier") or [])
assert "Typing :: Typed" in classifiers
assert "Framework :: FastAPI" in classifiers
assert "Framework :: Pytest" in classifiers
assert not [c for c in classifiers if c.startswith("License ::")]


def test_the_package_ships_a_py_typed_marker():
"""
Without `py.typed`, PEP 561 tells a consumer's type checker to ignore the package.

Every signature in `armasec_lite` is annotated and the project runs mypy in strict
mode, so the annotations are known good. The marker is what lets a downstream
project actually see them: mypy and pyright both skip an installed package that has
no marker, silently, and treat every symbol it exports as `Any`.
"""
import pathlib

marker = pathlib.Path(armasec_lite.__file__).parent / "py.typed"
assert marker.is_file()


def test_the_sdist_ships_an_explicit_allow_list():
"""
The source distribution must name what it contains rather than take hatchling's default.

Hatchling's default sdist is "everything the root `.gitignore` does not exclude", and
it does not read nested ignore files. `docusaurus/node_modules` is ignored by
`docusaurus/.gitignore`, so the default swept in 574MB of it and produced a 115.7MB
tarball, over PyPI's 100MB per-file limit. The failure surfaces at upload, after a
tag has been pushed.

This checks the declaration rather than building a tarball, so it stays a
millisecond-scale unit test. It cannot prove the build output is small; it catches
the regression that would make it large again, which is someone deleting the section
or adding a directory of harness data to it.
"""
import pathlib
import tomllib

pyproject = pathlib.Path(__file__).parents[2] / "pyproject.toml"
config = tomllib.loads(pyproject.read_text())
sdist = config["tool"]["hatch"]["build"]["targets"]["sdist"]

assert sdist["include"], "an empty include list means hatchling falls back to its default"
assert "armasec_lite" in sdist["include"]
assert not [e for e in sdist["include"] if e.startswith(("docusaurus", "legacy_"))]

# The include list alone does not do it: hatchling matches README* and LICEN[CS]E*
# recursively, which caught 1888 of them under docusaurus/node_modules even with
# only the two root files named above.
assert {"docusaurus", "legacy_comparison_compose"} <= set(sdist["exclude"])