From a80b7551b41eb1ca394f2a3f72b2b5a529b16bc5 Mon Sep 17 00:00:00 2001 From: Jens Tirsvad Nielsen Date: Tue, 13 Oct 2026 15:28:17 +0800 Subject: [PATCH] Add project scaffold: gitignore, pyproject, constants, Doxyfile, CI Closes #2 Closes #3 Closes #4 Closes #5 Closes #6 Refs #7 Task: MIL-001#1 Task: MIL-001#2 Task: MIL-001#3 Task: MIL-001#4 Task: MIL-001#5 Task: MIL-001#6 --- .gitea/workflows/ci.yml | 46 ++++++++++++++++++++++++++++++++++++++ .gitignore | 35 +++++++++++++++++++++++++++++ .yamllint.yml | 8 +++++++ Doxyfile | 20 +++++++++++++++++ docs/project-plan.md | 12 ++++++---- pyproject.toml | 47 +++++++++++++++++++++++++++++++++++++++ src/squirrel/__init__.py | 5 +++++ src/squirrel/constants.py | 27 ++++++++++++++++++++++ tests/test_constants.py | 43 +++++++++++++++++++++++++++++++++++ 9 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 .gitea/workflows/ci.yml create mode 100644 .gitignore create mode 100644 .yamllint.yml create mode 100644 Doxyfile create mode 100644 pyproject.toml create mode 100644 src/squirrel/__init__.py create mode 100644 src/squirrel/constants.py create mode 100644 tests/test_constants.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..a9460a4 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,46 @@ +# Continuous integration for the squirrel census project: runs the pytest tests +# on Python 3.13 for every push and pull request. Read by Gitea Actions (a +# runner must be registered for the repository). It changes nothing outside the +# repository and uses no secret. Decision: MIL-001 task "Add CI workflow". +# Host: Gitea Actions; uses only github.* contexts, so it also runs on GitHub. + +name: Tests + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +env: + PYTHON_VERSION: "3.13" + PIP_VERSION: "26.2.1" + +concurrency: + group: tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + pytest: + name: Run the tests + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out the code + uses: actions/checkout@v4.2.2 + + - name: Set up Python + uses: actions/setup-python@v5.6.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Create the virtual environment and install + run: | + python -m venv .venv + .venv/bin/python -m pip install --upgrade "pip==${PIP_VERSION}" + .venv/bin/python -m pip install -e ".[dev]" + + - name: Run the tests + run: .venv/bin/python -m pytest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6bef812 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Virtual environments +.venv/ +venv/ +env/ + +# Packaging and builds +build/ +dist/ +*.egg-info/ +.eggs/ + +# Test and tool caches +.pytest_cache/ +.coverage +htmlcov/ +.mypy_cache/ +.ruff_cache/ + +# Secrets: personal tokens used only by the maintainer, never by the project +.env + +# Generated source documentation (Doxygen) +docs/doxygen/ + +# Editors and operating systems +.idea/ +.vscode/ +.DS_Store +Thumbs.db diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..54fcc0d --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,8 @@ +# yamllint configuration for the workflow files in .gitea/workflows/. +extends: default +rules: + document-start: disable + line-length: + max: 100 + truthy: + check-keys: false diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..c900876 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,20 @@ +# Doxygen configuration for the squirrel census project. +# Build the source documentation with: doxygen Doxyfile +# Output goes to docs/doxygen/ (ignored by git). Everything not listed here +# keeps the Doxygen default. + +PROJECT_NAME = "Squirrel Census" +PROJECT_BRIEF = "Count squirrels by primary fur colour" +PROJECT_NUMBER = 0.1.0 +OUTPUT_DIRECTORY = docs/doxygen +INPUT = src README.md +FILE_PATTERNS = *.py *.md +RECURSIVE = YES +USE_MDFILE_AS_MAINPAGE = README.md +OPTIMIZE_OUTPUT_JAVA = YES +EXTRACT_ALL = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +QUIET = YES +WARN_IF_UNDOCUMENTED = YES +WARN_AS_ERROR = NO diff --git a/docs/project-plan.md b/docs/project-plan.md index 2484029..1b80157 100644 --- a/docs/project-plan.md +++ b/docs/project-plan.md @@ -31,10 +31,10 @@ This plan schedules the four phases that deliver the squirrel census assignment: | Gateway | Document | Window | Decision date | Owner | Stories | Main deliverable | Milestone | | --- | --- | --- | --- | --- | --- | --- | --- | -| G1 Project scaffold and tooling | [MIL-001] | 2026-10-13 to 2026-10-14 | 2026-10-14 | S01 | none | `.gitignore`, `pyproject.toml`, constants, Doxyfile, continuous integration (CI), repository metadata | | -| G2 Squirrel census analysis | [MIL-002] | 2026-10-14 to 2026-10-15 | 2026-10-15 | S01 | US-001 | Package, tests, census file, `squirrel_count.csv` | | -| G3 README, source documentation and screenshots | [MIL-003] | 2026-10-15 to 2026-10-16 | 2026-10-16 | S01 | none | README, Doxygen comments, screenshots | | -| G4 Wiki and publication | [MIL-004] | 2026-10-16 | 2026-10-16 | S01 | none | Wiki in en, da and ms | | +| G1 Project scaffold and tooling | [MIL-001] | 2026-10-13 to 2026-10-14 | 2026-10-14 | S01 | none | `.gitignore`, `pyproject.toml`, constants, Doxyfile, continuous integration (CI), repository metadata | [Milestone 124][MS-124] | +| G2 Squirrel census analysis | [MIL-002] | 2026-10-14 to 2026-10-15 | 2026-10-15 | S01 | US-001 | Package, tests, census file, `squirrel_count.csv` | [Milestone 125][MS-125] | +| G3 README, source documentation and screenshots | [MIL-003] | 2026-10-15 to 2026-10-16 | 2026-10-16 | S01 | none | README, Doxygen comments, screenshots | [Milestone 126][MS-126] | +| G4 Wiki and publication | [MIL-004] | 2026-10-16 | 2026-10-16 | S01 | none | Wiki in en, da and ms | [Milestone 127][MS-127] | ```plantuml @startgantt @@ -93,3 +93,7 @@ A No-Go at a gateway stops the later gateways; their dates move by the days the [MIL-003]: ./milestones/mil-003-documentation.md [MIL-004]: ./milestones/mil-004-wiki-publication.md [f1b3c03]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-squirrel/commit/f1b3c034dc3880b7884646b4a4ed28e5f98fbcd3 +[MS-124]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-squirrel/milestone/124 +[MS-125]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-squirrel/milestone/125 +[MS-126]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-squirrel/milestone/126 +[MS-127]: https://git.tirsystem.com/Tirsvad-Udemy-100-days-of-code/025-squirrel/milestone/127 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..319be8b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,47 @@ +# Project configuration for the squirrel census assignment (day 25). +# Read by pip, setuptools and pytest. The decision to use the standard library +# only is recorded in docs/adr/adr-0001-standard-library-instead-of-pandas.md. + +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "squirrel" +version = "0.1.0" +description = "Count the squirrels of the 2018 Central Park Squirrel Census by primary fur colour." +readme = "README.md" +requires-python = ">=3.13" +license = "AGPL-3.0-or-later" +license-files = ["LICENSE"] +dependencies = [] +classifiers = [ + "Programming Language :: Python :: 3.13", + "Intended Audience :: Education", +] + +[project.optional-dependencies] +dev = ["pytest==9.1.1", "ruff==0.17.0", "mypy==2.4.0"] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.pytest.ini_options] +pythonpath = ["src"] +testpaths = ["tests"] + +[tool.ruff] +line-length = 88 +src = ["src", "tests"] +extend-exclude = ["docs"] + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B", "D"] +ignore = ["D203", "D213"] + +[tool.mypy] +strict = true +files = ["src", "tests"] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["D"] diff --git a/src/squirrel/__init__.py b/src/squirrel/__init__.py new file mode 100644 index 0000000..b5f689a --- /dev/null +++ b/src/squirrel/__init__.py @@ -0,0 +1,5 @@ +## +# @file __init__.py +# @brief Package root of the squirrel census analysis. + +"""Count the squirrels of the 2018 Central Park Squirrel Census by fur colour.""" diff --git a/src/squirrel/constants.py b/src/squirrel/constants.py new file mode 100644 index 0000000..426f5ca --- /dev/null +++ b/src/squirrel/constants.py @@ -0,0 +1,27 @@ +## +# @file constants.py +# @brief Literals used by the squirrel census analysis, each defined once. + +"""Literals used by the squirrel census analysis, each defined once.""" + +import pathlib + +## @brief Default location of the census CSV file, relative to the working directory. +DEFAULT_INPUT_PATH: pathlib.Path = ( + pathlib.Path("data") / "2018_Central_Park_Squirrel_Census_-_Squirrel_Data.csv" +) + +## @brief Default location of the summary CSV file that is written. +DEFAULT_OUTPUT_PATH: pathlib.Path = pathlib.Path("squirrel_count.csv") + +## @brief Text encoding of the input and output files. +FILE_ENCODING: str = "utf-8" + +## @brief Name of the census column that holds the primary fur colour. +FUR_COLOR_COLUMN: str = "Primary Fur Color" + +## @brief Fur colours that are counted, in the order they appear in the summary. +FUR_COLORS: tuple[str, ...] = ("Gray", "Cinnamon", "Black") + +## @brief Header of the summary file: the colour column, then the count column. +SUMMARY_COLUMNS: tuple[str, str] = ("Fur Color", "Count") diff --git a/tests/test_constants.py b/tests/test_constants.py new file mode 100644 index 0000000..35527f7 --- /dev/null +++ b/tests/test_constants.py @@ -0,0 +1,43 @@ +"""Tests for the squirrel.constants module.""" + +import ast +import pathlib +import sys + +from squirrel import constants + + +def test_fur_colors_are_gray_cinnamon_black_in_order() -> None: + assert constants.FUR_COLORS == ("Gray", "Cinnamon", "Black") + + +def test_summary_columns_are_fur_color_and_count() -> None: + assert constants.SUMMARY_COLUMNS == ("Fur Color", "Count") + + +def test_default_input_path_has_census_file_name() -> None: + assert constants.DEFAULT_INPUT_PATH.name == ( + "2018_Central_Park_Squirrel_Census_-_Squirrel_Data.csv" + ) + assert constants.DEFAULT_INPUT_PATH.parent == pathlib.Path("data") + + +def test_default_output_path_has_summary_file_name() -> None: + assert constants.DEFAULT_OUTPUT_PATH == pathlib.Path("squirrel_count.csv") + + +def test_file_encoding_and_column_name_values() -> None: + assert constants.FILE_ENCODING == "utf-8" + assert constants.FUR_COLOR_COLUMN == "Primary Fur Color" + + +def test_module_imports_only_standard_library() -> None: + source = pathlib.Path(constants.__file__).read_text(encoding="utf-8") + imported: set[str] = set() + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Import): + imported.update(alias.name.split(".")[0] for alias in node.names) + elif isinstance(node, ast.ImportFrom) and node.module is not None: + imported.add(node.module.split(".")[0]) + + assert imported <= sys.stdlib_module_names