From cd4cf4cf148bddf340a5c10ad180d63bfc8c3262 Mon Sep 17 00:00:00 2001 From: Bradley Reynolds Date: Mon, 12 Aug 2024 20:18:44 -0500 Subject: [PATCH] MVP (#2) * MVP Signed-off-by: GitHub * Setup Trusted Publishing Signed-off-by: GitHub * Copy/paste typo Signed-off-by: GitHub --------- Signed-off-by: GitHub --- .devcontainer/devcontainer.json | 9 +++ .github/dependabot.yaml | 19 +++++ .github/workflows/dependency-review.yaml | 20 +++++ .github/workflows/python-ci.yaml | 38 +++++++++ .github/workflows/python-publish-pypi.yaml | 63 +++++++++++++++ .gitignore | 20 +++++ .pre-commit-config.yaml | 14 ++++ README.md | 1 + docs/Makefile | 20 +++++ docs/make.bat | 35 ++++++++ docs/source/changelog.rst | 5 ++ docs/source/conf.py | 93 ++++++++++++++++++++++ docs/source/index.rst | 25 ++++++ noxfile.py | 58 ++++++++++++++ pyproject.toml | 50 ++++++++++++ src/crazylibs/__init__.py | 1 + src/crazylibs/__main__.py | 6 ++ src/crazylibs/cli.py | 34 ++++++++ src/crazylibs/py.typed | 1 + src/crazylibs/stories/__init__.py | 5 ++ src/crazylibs/stories/_story.py | 10 +++ src/crazylibs/stories/a_day_at_the_zoo.py | 41 ++++++++++ tests/test_app.py | 12 +++ 23 files changed, 580 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/dependabot.yaml create mode 100644 .github/workflows/dependency-review.yaml create mode 100644 .github/workflows/python-ci.yaml create mode 100644 .github/workflows/python-publish-pypi.yaml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/changelog.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 noxfile.py create mode 100644 pyproject.toml create mode 100644 src/crazylibs/__init__.py create mode 100644 src/crazylibs/__main__.py create mode 100644 src/crazylibs/cli.py create mode 100644 src/crazylibs/py.typed create mode 100644 src/crazylibs/stories/__init__.py create mode 100644 src/crazylibs/stories/_story.py create mode 100644 src/crazylibs/stories/a_day_at_the_zoo.py create mode 100644 tests/test_app.py diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c52f35d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,9 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12", + "installTools": false + } + } +} diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..c912694 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + ci-dependencies: + patterns: + - "*" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + groups: + python-dependencies: + patterns: + - "*" diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml new file mode 100644 index 0000000..fa005a7 --- /dev/null +++ b/.github/workflows/dependency-review.yaml @@ -0,0 +1,20 @@ +name: "Dependency Review" + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + + steps: + - name: "Checkout Repository" + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + + - name: "Dependency Review" + uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 + with: + config-file: darbiadev/.github/.github/dependency-review-config.yaml@main diff --git a/.github/workflows/python-ci.yaml b/.github/workflows/python-ci.yaml new file mode 100644 index 0000000..c621112 --- /dev/null +++ b/.github/workflows/python-ci.yaml @@ -0,0 +1,38 @@ +name: "Python CI" + +on: + push: + branches: + - main + pull_request: + +jobs: + pre-commit: + uses: darbiadev/.github/.github/workflows/generic-precommit.yaml@12e07d61ed37c908baa73f8d5550281b3ed9cddd # v13.1.2 + + lint: + needs: pre-commit + uses: darbiadev/.github/.github/workflows/python-lint.yaml@12e07d61ed37c908baa73f8d5550281b3ed9cddd # v13.1.2 + + test: + needs: lint + strategy: + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.12" ] + + uses: darbiadev/.github/.github/workflows/python-test.yaml@12e07d61ed37c908baa73f8d5550281b3ed9cddd # v13.1.2 + with: + os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + secrets: + codecov-token: ${{ secrets.CODECOV_TOKEN }} + + docs: + # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages + permissions: + contents: read + pages: write + id-token: write + + uses: darbiadev/.github/.github/workflows/github-pages-python-sphinx.yaml@12e07d61ed37c908baa73f8d5550281b3ed9cddd # v13.1.2 diff --git a/.github/workflows/python-publish-pypi.yaml b/.github/workflows/python-publish-pypi.yaml new file mode 100644 index 0000000..d5fc94a --- /dev/null +++ b/.github/workflows/python-publish-pypi.yaml @@ -0,0 +1,63 @@ +name: "Publish Python 🐍 distributions 📦 to PyPI" + +on: + release: + types: [published] + +jobs: + build: + name: "Build distribution 📦" + runs-on: ubuntu-latest + + steps: + - name: "Checkout repository" + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + + - name: "Set up Python 3.x" + uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 + with: + python-version: "3.x" + cache: "pip" + cache-dependency-path: "pyproject.toml" + + - name: "Install pypa/build" + run: >- + python -m + pip install + build + --user + + - name: "Build a binary wheel and a source tarball" + run: >- + python -m + build + --outdir dist/ + + - name: "Upload packages" + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: python-package-distributions + path: dist/ + + publish-to-pypi: + name: "Publish Python 🐍 distribution 📦 to PyPI" + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/crazylibs/${{ github.ref_name }} + permissions: + id-token: write + + steps: + - name: "Download dists" + uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 + with: + name: python-package-distributions + path: dist/ + + - name: "Publish distribution 📦 to PyPI" + uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14 + with: + verbose: true + print-hash: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0986643 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# JetBrains +.idea + +# Enviorment +venv + +# Packaging +*.egg-info +dist +build + +# Cache +__pycache__ + +# Docs +docs/build + +# Test data +/*.xlsx +/*.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f83f8b3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: check-yaml + - id: check-json + - id: trailing-whitespace + args: [ --markdown-linebreak-ext=md ] + - id: mixed-line-ending + args: [ --fix=lf ] + - id: end-of-file-fixer diff --git a/README.md b/README.md index cedf6ec..817be6f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # crazylibs + A variation of the "Mad Libs" games diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..bed4efb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + $(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 0000000..b926790 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,5 @@ +Changelog +========= + +- :release:`0.1.0 <13th August 2024>` +- :feature:`1` Initialize package diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..39a2233 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,93 @@ +"""Configuration file for the Sphinx documentation builder. + +For the full list of built-in configuration values, see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +from importlib.metadata import metadata + +project_metadata = metadata("crazylibs") +project: str = project_metadata["Name"] +release: str = project_metadata["Version"] +REPO_LINK: str = project_metadata["Project-URL"].replace("repository, ", "") +copyright: str = "Let's build a ..." # noqa: A001 +author: str = "Let's build a ... community" + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named "sphinx.ext.*") or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.linkcode", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "autoapi.extension", + "releases", +] + +autoapi_type: str = "python" +autoapi_dirs: list[str] = ["../../src"] + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +# Add any paths that contain templates here, relative to this directory. +templates_path: list[str] = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns: list[str] = ["_build", "Thumbs.db", ".DS_Store"] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme: str = "furo" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path: list[str] = ["_static"] + +releases_github_path = REPO_LINK.removeprefix("https://github.com/") +releases_release_uri = f"{REPO_LINK}/releases/tag/v%s" + + +def linkcode_resolve(domain: str, info: dict) -> str: + """linkcode_resolve.""" + if domain != "py": + return None + if not info["module"]: + return None + + import importlib + import inspect + import types + + mod = importlib.import_module(info["module"]) + + val = mod + for k in info["fullname"].split("."): + val = getattr(val, k, None) + if val is None: + break + + filename = info["module"].replace(".", "/") + ".py" + + if isinstance( + val, + types.ModuleType + | types.MethodType + | types.FunctionType + | types.TracebackType + | types.FrameType + | types.CodeType, + ): + try: + lines, first = inspect.getsourcelines(val) + last = first + len(lines) - 1 + filename += f"#L{first}-L{last}" + except (OSError, TypeError): + pass + + return f"{REPO_LINK}/blob/main/src/{filename}" diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..c0e5cde --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,25 @@ +crazylibs +========= + +A variation of the "Mad Libs" games + +Module Index +------------ + +.. toctree:: + :maxdepth: 1 + + autoapi/index + +.. toctree:: + :caption: Other: + :hidden: + + changelog + +Extras +------ + +* :ref:`genindex` +* :ref:`search` +* :doc:`changelog` diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..1c7f6a3 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,58 @@ +"""Noxfile.""" + +import shutil +from pathlib import Path + +import nox + +nox.options.default_venv_backend = "none" +nox.options.sessions = ["lints"] + + +CLEANABLE_TARGETS = [ + "./dist", + "./build", + "./.nox", + "./.coverage", + "./.coverage.*", + "./coverage.json", + "./**/.mypy_cache", + "./**/.pytest_cache", + "./**/__pycache__", + "./**/*.pyc", + "./**/*.pyo", +] + + +@nox.session +def install(session: nox.Session) -> None: + """Install the project.""" + session.run("python", "-m", "pip", "install", ".[dev,test]") + + +@nox.session +def tests(session: nox.Session) -> None: + """Run tests.""" + session.run("pytest") + + +@nox.session +def lints(session: nox.Session) -> None: + """Run lints.""" + session.run("pre-commit", "run", "--all-files") + session.run("ruff", "check", "--fix", ".") + session.run("ruff", "format", ".") + session.run("mypy", "--strict", "src/") + + +@nox.session +def clean(_: nox.Session) -> None: + """Clean cache, .pyc, .pyo, and test/build artifact files from project.""" + count = 0 + for searchpath in CLEANABLE_TARGETS: + for filepath in Path().glob(searchpath): + if filepath.is_dir(): + shutil.rmtree(filepath) + else: + filepath.unlink() + count += 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9ca3f77 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,50 @@ +[project] +name = "crazylibs" +version = "0.1.0" +description = "crazylibs" +readme = "README.md" +authors = [{ name = "Bradley Reynolds", email = "bradley.reynolds@darbia.dev" }] +license = { text = "MIT" } +requires-python = ">=3.10" +dependencies = ["typer"] + +[project.optional-dependencies] +dev = ["nox", "pre-commit", "ruff", "mypy"] +docs = ["sphinx", "furo", "sphinx-autoapi", "releases"] +tests = ["pytest", "pytest-randomly"] + +[project.urls] +repository = "https://github.com/letsbuilda/crazylibs" +documentation = "https://docs.letsbuilda.dev/crazylibs/" + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.package-data] +"crazylibs" = ["py.typed"] + +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "CPY001", # (Missing copyright notice at top of file) - No license +] + +[tool.ruff.lint.extend-per-file-ignores] +"docs/*" = [ + "INP001", # (File `docs/*.py` is part of an implicit namespace package. Add an `__init__.py`.) - Docs are not modules +] +"tests/*" = [ + "INP001", # (File `tests/*.py` is part of an implicit namespace package. Add an `__init__.py`.) - Tests are not modules + "S101", # (Use of `assert` detected) - Yes, that's the point +] + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.coverage.run] +source = ["crazylibs"] diff --git a/src/crazylibs/__init__.py b/src/crazylibs/__init__.py new file mode 100644 index 0000000..ff64d31 --- /dev/null +++ b/src/crazylibs/__init__.py @@ -0,0 +1 @@ +"""A variation of the "Mad Libs" games.""" diff --git a/src/crazylibs/__main__.py b/src/crazylibs/__main__.py new file mode 100644 index 0000000..ec54b81 --- /dev/null +++ b/src/crazylibs/__main__.py @@ -0,0 +1,6 @@ +"""Main entrypoint.""" + +from .cli import app + +if __name__ == "__main__": + app(prog_name="crazylibs") diff --git a/src/crazylibs/cli.py b/src/crazylibs/cli.py new file mode 100644 index 0000000..a5472b4 --- /dev/null +++ b/src/crazylibs/cli.py @@ -0,0 +1,34 @@ +"""CLI app definition.""" + +from random import choice + +import typer + +app = typer.Typer() + + +@app.command() +def run_story() -> None: + """Fun!""" # noqa: D400 + from crazylibs.stories import stories + + story = choice(stories) # noqa: S311 + + typer.echo(f'Welcome to "{story.title}"') + typer.echo("") + typer.echo("Please fill in the following words:") + + context = {} + for index, question in story.questions.items(): + context[index] = typer.prompt(question) + + text = story.template + for index, item in context.items(): + text = text.replace(f"({index})", item) + + typer.echo("") + typer.echo("Wow, what a good selection of words!") + typer.echo("Here is your story:") + typer.echo("") + + typer.echo(text) diff --git a/src/crazylibs/py.typed b/src/crazylibs/py.typed new file mode 100644 index 0000000..1242d43 --- /dev/null +++ b/src/crazylibs/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561. diff --git a/src/crazylibs/stories/__init__.py b/src/crazylibs/stories/__init__.py new file mode 100644 index 0000000..b06adc4 --- /dev/null +++ b/src/crazylibs/stories/__init__.py @@ -0,0 +1,5 @@ +"""The stories.""" + +from .a_day_at_the_zoo import a_day_at_the_zoo + +stories = [a_day_at_the_zoo] diff --git a/src/crazylibs/stories/_story.py b/src/crazylibs/stories/_story.py new file mode 100644 index 0000000..6b0c120 --- /dev/null +++ b/src/crazylibs/stories/_story.py @@ -0,0 +1,10 @@ +"""Story definition.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Story: + title: str + template: str + questions: dict[int, str] diff --git a/src/crazylibs/stories/a_day_at_the_zoo.py b/src/crazylibs/stories/a_day_at_the_zoo.py new file mode 100644 index 0000000..5987753 --- /dev/null +++ b/src/crazylibs/stories/a_day_at_the_zoo.py @@ -0,0 +1,41 @@ +"""A Day at the Zoo.""" + +from ._story import Story + +questions = { + 1: "adjective", + 2: "animal", + 3: "verb ending in -ing", + 4: "place", + 5: "adjective", + 6: "animal", + 7: "verb ending in -ing", + 8: "adjective", + 9: "food", + 10: "adjective", + 11: "animal", + 12: "adjective", + 13: "animal", + 14: "verb ending in -ing", + 15: "verb ending in -ing", + 16: "adjective", + 17: "adjective", + 18: "noun", + 19: "adjective", +} + +template = """ +One day, my friend and I decided to visit the zoo. We were very (1) to see all the animals. +First, we saw the (2), which was (3) in its (4). +Next, we went to the (5) exhibit where we saw a (6) that was (7). It was so (8)! + +After that, we decided to have some (9) for lunch. +While eating, we saw a (10) (11) that tried to steal our (9)! +We laughed and decided to go see the (12) (13) show. The (13) did tricks like (14) and (15). +It was the most (16) part of our day. + +Finally, we visited the gift shop and bought a (17) (18) as a souvenir. +It was a (19) day at the zoo, and we couldn't wait to come back again! +""" + +a_day_at_the_zoo = Story(title="A Day at the Zoo", template=template, questions=questions) diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..02435c1 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,12 @@ +"""Testing the app.""" + +from crazylibs.cli import app +from typer.testing import CliRunner + +runner = CliRunner() + + +def test_app() -> None: + """Test that the app can be invoked.""" + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0