diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json new file mode 100644 index 0000000..032ee6d --- /dev/null +++ b/.devcontainer/devcontainer-lock.json @@ -0,0 +1,14 @@ +{ + "features": { + "ghcr.io/devcontainers/features/powershell:1": { + "version": "1.3.4", + "resolved": "ghcr.io/devcontainers/features/powershell@sha256:3645db9e9598f04fc405c8c38aac0d60561b5697cc2268f801ff88f0afe0ca6c", + "integrity": "sha256:3645db9e9598f04fc405c8c38aac0d60561b5697cc2268f801ff88f0afe0ca6c" + }, + "ghcr.io/devcontainers/features/python:1": { + "version": "1.4.2", + "resolved": "ghcr.io/devcontainers/features/python@sha256:bf021f1800543f08bf029c449a3f25341be782b620802befa1f8e6ee51cf6cf6", + "integrity": "sha256:bf021f1800543f08bf029c449a3f25341be782b620802befa1f8e6ee51cf6cf6" + } + } +} \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..5ed637e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,10 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/python:1": { + "version": "3.12", + "installTools": false + }, + "ghcr.io/devcontainers/features/powershell:1": {} + } +} diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..611de21 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,33 @@ +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: "monthly" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + groups: + ci-dependencies: + patterns: + - "*" + + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + groups: + python-dependencies: + patterns: + - "*" + + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" + groups: + docker-dependencies: + patterns: + - "*" diff --git a/.github/workflows/container-build-push.yaml b/.github/workflows/container-build-push.yaml new file mode 100644 index 0000000..269e4d6 --- /dev/null +++ b/.github/workflows/container-build-push.yaml @@ -0,0 +1,22 @@ +name: Container Build and Push + +on: + push: + branches: + - main + tags: + - v* + pull_request: + +permissions: + contents: read + packages: write + # This is used to complete the identity challenge + # with sigstore/fulcio. + id-token: write + +jobs: + build-push: + uses: darbiadev/.github/.github/workflows/docker-build-push.yaml@ea97d99e1520c46080c4c9032a69552e491474ac # v13.0.0 + with: + file-name: Dockerfile diff --git a/.github/workflows/dependency-review.yaml b/.github/workflows/dependency-review.yaml new file mode 100644 index 0000000..1ae3889 --- /dev/null +++ b/.github/workflows/dependency-review.yaml @@ -0,0 +1,19 @@ +name: Dependency Review + +on: [ pull_request ] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 # v4.1.2 + + - name: Dependency Review + uses: actions/dependency-review-action@5bbc3ba658137598168acb2ab73b21c432dd411b # v4.2.5 + 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..f0458bd --- /dev/null +++ b/.github/workflows/python-ci.yaml @@ -0,0 +1,36 @@ +name: "Python CI" + +on: + push: + branches: + - main + pull_request: + +jobs: + pre-commit: + uses: darbiadev/.github/.github/workflows/generic-precommit.yaml@29197a38ef3741064f47b623ede0c1ad22402c57 # v13.0.3 + + lint: + needs: pre-commit + uses: darbiadev/.github/.github/workflows/python-lint.yaml@29197a38ef3741064f47b623ede0c1ad22402c57 # v13.0.3 + + test: + needs: lint + strategy: + matrix: + os: [ ubuntu-latest ] + python-version: [ "3.12" ] + + uses: darbiadev/.github/.github/workflows/python-test.yaml@29197a38ef3741064f47b623ede0c1ad22402c57 # v13.0.3 + with: + os: ${{ matrix.os }} + python-version: ${{ matrix.python-version }} + + 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@29197a38ef3741064f47b623ede0c1ad22402c57 # v13.0.3 diff --git a/.github/workflows/sentry-release.yaml b/.github/workflows/sentry-release.yaml new file mode 100644 index 0000000..f7c5e32 --- /dev/null +++ b/.github/workflows/sentry-release.yaml @@ -0,0 +1,23 @@ +name: "Sentry release" + +on: + push: + branches: + - main + +jobs: + sentry-release: + runs-on: ubuntu-latest + + steps: + - name: "Checkout repository" + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: "Create Sentry release" + uses: getsentry/action-release@e769183448303de84c5a06aaaddf9da7be26d6c7 # v1.7.0 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ vars.SENTRY_ORG }} + SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }} + with: + version_prefix: "${{ vars.SENTRY_PROJECT }}@" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..98c9b5c --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# JetBrains +.idea + +# Packaging +*.egg-info +dist + +# Cache +__pycache__ + +# Docs +docs/build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..61e3cb7 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.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 + exclude: .devcontainer/devcontainer-lock.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2dc4080 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12-slim@sha256:43a49c9cc2e614468e3d1a903aabe17a97a4c788c76cf5337b5cdc3535b07d4f + +# Define Git SHA build argument for sentry +ARG git_sha="development" +ENV GIT_SHA=$git_sha + +WORKDIR /home/zipalyzer + +COPY requirements/requirements.txt . +RUN python -m pip install --requirement requirements.txt + +COPY pyproject.toml pyproject.toml +COPY src/ src/ +RUN python -m pip install . + +RUN adduser --disabled-password zipalyzer +USER zipalyzer + +# HTTP +EXPOSE 8080 + +CMD ["uvicorn", "zipalyzer.server:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/README.md b/README.md index 254498b..db8f9e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # zipalyzer-api + An API for analyzing zip files 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..79d2680 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,5 @@ +Changelog +========= + +- :release:`0.1.0 <3rd April 2023>` +- :feature:`1` Initialize package diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..2f0a8b6 --- /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("zipalyzer") +project: str = project_metadata["Name"] +release: str = project_metadata["Version"] +REPO_LINK: str = project_metadata["Project-URL"].replace("repository, ", "") +copyright: str = "Vipyr Security" # noqa: A001 +author: str = "Vipyr Security Developers" + +# 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 | None: + """linkcode_resolve.""" + if domain != "py": + return None + if not info["module"]: + return None + + import importlib # noqa: PLC0415 + import inspect # noqa: PLC0415 + import types # noqa: PLC0415 + + 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..9f16909 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,25 @@ +zipalyzer +========= + +An API for analyzing zip files + +Module Index +------------ + +.. toctree:: + :maxdepth: 1 + + autoapi/index + +.. toctree:: + :caption: Other: + :hidden: + + changelog + +Extras +------ + +* :ref:`genindex` +* :ref:`search` +* :doc:`changelog` diff --git a/make.ps1 b/make.ps1 new file mode 100644 index 0000000..e79e4f1 --- /dev/null +++ b/make.ps1 @@ -0,0 +1,127 @@ +<# +.SYNOPSIS +Makefile + +.DESCRIPTION +USAGE + .\make.ps1 + +COMMANDS + init install Python build tools + install-dev install local package in editable mode + update-deps update the dependencies + upgrade-deps upgrade the dependencies + lint run `pre-commit` and `ruff` + test run `pytest` + build-dist run `python -m build` + clean delete generated content + help, -? show this help message +#> +param( + [Parameter(Position = 0)] + [ValidateSet("init", "install-dev", "update-deps", "upgrade-deps", "lint", "test", "build-dist", "clean", "help")] + [string]$Command +) + +function Invoke-Help +{ + Get-Help $PSCommandPath +} + +function Invoke-Init +{ + python -m pip install --upgrade pip wheel setuptools build +} + +function Invoke-Install-Dev +{ + python -m pip install --upgrade --editable ".[dev, tests, docs]" +} + +function Invoke-Update-Deps +{ + python -m pip install --upgrade pip-tools + pip-compile requirements/requirements.in + pip-compile requirements/requirements-dev.in + pip-compile requirements/requirements-tests.in + pip-compile requirements/requirements-docs.in +} + +function Invoke-Upgrade-Deps +{ + python -m pip install --upgrade pip-tools pre-commit + pre-commit autoupdate + pip-compile --upgrade requirements/requirements.in + pip-compile --upgrade requirements/requirements-dev.in + pip-compile --upgrade requirements/requirements-tests.in + pip-compile --upgrade requirements/requirements-docs.in +} + +function Invoke-Lint +{ + pre-commit run --all-files + python -m ruff check --fix . + python -m ruff format . + python -m mypy --strict src/ +} + +function Invoke-Test +{ + python -m pytest +} + +function Invoke-Build-Dist +{ + python -m pip install --upgrade build + python -m build +} + +function Invoke-Clean +{ + $folders = @("build", "dist") + foreach ($folder in $folders) + { + if (Test-Path $folder) + { + + Write-Verbose "Deleting $folder" + Remove-Item $folder -Recurse -Force + } + } +} + +switch ($Command) +{ + "init" { + Invoke-Init + } + "install-dev" { + Invoke-Install-Dev + } + "lint" { + Invoke-Lint + } + "update-deps" { + Invoke-Update-Deps + } + "upgrade-deps" { + Invoke-Upgrade-Deps + } + "test" { + Invoke-Test + } + "build-dist" { + Invoke-Build-Dist + } + "clean" { + Invoke-Clean + } + "help" { + Invoke-Help + } + default + { + Invoke-Init + Invoke-Install-Dev + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8182d4b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,60 @@ +[project] +name = "zipalyzer" +description = "An API for analyzing zip files" +authors = [ + { name = "Vipyr Security Developers", email = "support@vipyrsec.com" }, +] +license = { text = "MIT" } +readme = "README.md" +requires-python = ">=3.12,<3.13" +dynamic = ["version", "dependencies", "optional-dependencies"] + +[project.urls] +repository = "https://github.com/vipyrsec/zipalyzer-api/" +documentation = "https://docs.vipyrsec.com/zipalyzer-api/" + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.dynamic.version] +attr = "zipalyzer.__version__" + +[tool.setuptools.dynamic.dependencies] +file = ["requirements/requirements.txt"] + +[tool.setuptools.dynamic.optional-dependencies] +dev = { file = ["requirements/requirements-dev.txt"] } +tests = { file = ["requirements/requirements-tests.txt"] } +docs = { file = ["requirements/requirements-docs.txt"] } + +[tool.ruff] +preview = true +unsafe-fixes = true +target-version = "py312" +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 `tests/*.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 = [ + "zipalyzer", +] diff --git a/requirements/requirements-dev.in b/requirements/requirements-dev.in new file mode 100644 index 0000000..4cb96ee --- /dev/null +++ b/requirements/requirements-dev.in @@ -0,0 +1,7 @@ +# Constrain versions installed to be compatible with core dependencies +--constraint requirements.txt + +pip-tools +pre-commit +ruff +mypy diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt new file mode 100644 index 0000000..55776fa --- /dev/null +++ b/requirements/requirements-dev.txt @@ -0,0 +1,56 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements/requirements-dev.in +# +build==1.2.1 + # via pip-tools +cfgv==3.4.0 + # via pre-commit +click==8.1.7 + # via + # -c requirements/requirements.txt + # pip-tools +distlib==0.3.8 + # via virtualenv +filelock==3.13.3 + # via virtualenv +identify==2.5.35 + # via pre-commit +mypy==1.9.0 + # via -r requirements/requirements-dev.in +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.8.0 + # via pre-commit +packaging==24.0 + # via build +pip-tools==7.4.1 + # via -r requirements/requirements-dev.in +platformdirs==4.2.0 + # via virtualenv +pre-commit==3.7.0 + # via -r requirements/requirements-dev.in +pyproject-hooks==1.0.0 + # via + # build + # pip-tools +pyyaml==6.0.1 + # via + # -c requirements/requirements.txt + # pre-commit +ruff==0.3.5 + # via -r requirements/requirements-dev.in +typing-extensions==4.10.0 + # via + # -c requirements/requirements.txt + # mypy +virtualenv==20.25.1 + # via pre-commit +wheel==0.43.0 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/requirements-docs.in b/requirements/requirements-docs.in new file mode 100644 index 0000000..fc51bbd --- /dev/null +++ b/requirements/requirements-docs.in @@ -0,0 +1,7 @@ +# Constrain versions installed to be compatible with core dependencies +--constraint requirements.txt + +sphinx +furo +sphinx-autoapi +releases diff --git a/requirements/requirements-docs.txt b/requirements/requirements-docs.txt new file mode 100644 index 0000000..44d0a69 --- /dev/null +++ b/requirements/requirements-docs.txt @@ -0,0 +1,85 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements/requirements-docs.in +# +alabaster==0.7.16 + # via sphinx +anyascii==0.3.2 + # via sphinx-autoapi +astroid==3.1.0 + # via sphinx-autoapi +babel==2.14.0 + # via sphinx +beautifulsoup4==4.12.3 + # via furo +certifi==2024.2.2 + # via + # -c requirements/requirements.txt + # requests +charset-normalizer==3.3.2 + # via requests +docutils==0.20.1 + # via sphinx +furo==2024.1.29 + # via -r requirements/requirements-docs.in +idna==3.6 + # via + # -c requirements/requirements.txt + # requests +imagesize==1.4.1 + # via sphinx +jinja2==3.1.3 + # via + # sphinx + # sphinx-autoapi +markupsafe==2.1.5 + # via jinja2 +packaging==24.0 + # via sphinx +pygments==2.17.2 + # via + # furo + # sphinx +pyyaml==6.0.1 + # via + # -c requirements/requirements.txt + # sphinx-autoapi +releases==2.1.1 + # via -r requirements/requirements-docs.in +requests==2.31.0 + # via sphinx +semantic-version==2.6.0 + # via releases +snowballstemmer==2.2.0 + # via sphinx +soupsieve==2.5 + # via beautifulsoup4 +sphinx==7.2.6 + # via + # -r requirements/requirements-docs.in + # furo + # releases + # sphinx-autoapi + # sphinx-basic-ng +sphinx-autoapi==3.0.0 + # via -r requirements/requirements-docs.in +sphinx-basic-ng==1.0.0b2 + # via furo +sphinxcontrib-applehelp==1.0.8 + # via sphinx +sphinxcontrib-devhelp==1.0.6 + # via sphinx +sphinxcontrib-htmlhelp==2.0.5 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.7 + # via sphinx +sphinxcontrib-serializinghtml==1.1.10 + # via sphinx +urllib3==2.2.1 + # via + # -c requirements/requirements.txt + # requests diff --git a/requirements/requirements-tests.in b/requirements/requirements-tests.in new file mode 100644 index 0000000..83ef1be --- /dev/null +++ b/requirements/requirements-tests.in @@ -0,0 +1,7 @@ +# Constrain versions installed to be compatible with core dependencies +--constraint requirements.txt + +pytest +pytest-randomly + +httpx # for fastapi.testclient.TestClient diff --git a/requirements/requirements-tests.txt b/requirements/requirements-tests.txt new file mode 100644 index 0000000..2ad1cb1 --- /dev/null +++ b/requirements/requirements-tests.txt @@ -0,0 +1,45 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements/requirements-tests.in +# +anyio==4.3.0 + # via + # -c requirements/requirements.txt + # httpx +certifi==2024.2.2 + # via + # -c requirements/requirements.txt + # httpcore + # httpx +h11==0.14.0 + # via + # -c requirements/requirements.txt + # httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.0 + # via -r requirements/requirements-tests.in +idna==3.6 + # via + # -c requirements/requirements.txt + # anyio + # httpx +iniconfig==2.0.0 + # via pytest +packaging==24.0 + # via pytest +pluggy==1.4.0 + # via pytest +pytest==8.1.1 + # via + # -r requirements/requirements-tests.in + # pytest-randomly +pytest-randomly==3.15.0 + # via -r requirements/requirements-tests.in +sniffio==1.3.1 + # via + # -c requirements/requirements.txt + # anyio + # httpx diff --git a/requirements/requirements.in b/requirements/requirements.in new file mode 100644 index 0000000..0be69ca --- /dev/null +++ b/requirements/requirements.in @@ -0,0 +1,11 @@ +# Requirements +# --index-url=http://pypi.example.com/simple +# --no-cache-dir + +# Core +uvicorn[standard] +fastapi +pydantic-settings + +# Metrics +sentry-sdk[fastapi] diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..41ea5b3 --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,61 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile requirements/requirements.in +# +annotated-types==0.6.0 + # via pydantic +anyio==4.3.0 + # via + # starlette + # watchfiles +certifi==2024.2.2 + # via sentry-sdk +click==8.1.7 + # via uvicorn +fastapi==0.110.1 + # via + # -r requirements/requirements.in + # sentry-sdk +h11==0.14.0 + # via uvicorn +httptools==0.6.1 + # via uvicorn +idna==3.6 + # via anyio +pydantic==2.6.4 + # via + # fastapi + # pydantic-settings +pydantic-core==2.16.3 + # via pydantic +pydantic-settings==2.2.1 + # via -r requirements/requirements.in +python-dotenv==1.0.1 + # via + # pydantic-settings + # uvicorn +pyyaml==6.0.1 + # via uvicorn +sentry-sdk[fastapi]==1.44.1 + # via -r requirements/requirements.in +sniffio==1.3.1 + # via anyio +starlette==0.37.2 + # via fastapi +typing-extensions==4.10.0 + # via + # fastapi + # pydantic + # pydantic-core +urllib3==2.2.1 + # via sentry-sdk +uvicorn[standard]==0.29.0 + # via -r requirements/requirements.in +uvloop==0.19.0 + # via uvicorn +watchfiles==0.21.0 + # via uvicorn +websockets==12.0 + # via uvicorn diff --git a/src/zipalyzer/__init__.py b/src/zipalyzer/__init__.py new file mode 100644 index 0000000..0f14d68 --- /dev/null +++ b/src/zipalyzer/__init__.py @@ -0,0 +1,3 @@ +"""An API for analyzing zip files.""" + +__version__ = "0.1.0" diff --git a/src/zipalyzer/constants.py b/src/zipalyzer/constants.py new file mode 100644 index 0000000..29d75b8 --- /dev/null +++ b/src/zipalyzer/constants.py @@ -0,0 +1,34 @@ +""" +Loads configuration from environment variables and `.env` files. + +By default, the values defined in the classes are used, these can be overridden by an env var with the same name. + +An `.env` file is used to populate env vars, if present. +""" + +from os import getenv + +from pydantic_settings import BaseSettings, SettingsConfigDict + +# Git SHA for Sentry +GIT_SHA = getenv("GIT_SHA", "development") + + +class EnvConfig(BaseSettings): + """Our default configuration for models that should load from .env files.""" + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + env_nested_delimiter="__", + extra="ignore", + ) + + +class _Sentry(EnvConfig, env_prefix="sentry_"): + dsn: str = "" + environment: str = "production" + release_prefix: str = "zipalyzer" + + +Sentry = _Sentry() diff --git a/src/zipalyzer/metadata/__init__.py b/src/zipalyzer/metadata/__init__.py new file mode 100644 index 0000000..244e7c9 --- /dev/null +++ b/src/zipalyzer/metadata/__init__.py @@ -0,0 +1 @@ +"""Meta information routes.""" diff --git a/src/zipalyzer/metadata/routes.py b/src/zipalyzer/metadata/routes.py new file mode 100644 index 0000000..06d0f75 --- /dev/null +++ b/src/zipalyzer/metadata/routes.py @@ -0,0 +1,18 @@ +"""Core information routes.""" + +from fastapi import APIRouter + +from zipalyzer import __version__ +from zipalyzer.constants import GIT_SHA +from zipalyzer.models import ServerMetadata + +router = APIRouter(tags=["Metadata"]) + + +@router.get("/", summary="Get the server's metadata") +async def metadata() -> ServerMetadata: + """Get server metadata.""" + return ServerMetadata( + version=__version__, + server_commit=GIT_SHA, + ) diff --git a/src/zipalyzer/models/__init__.py b/src/zipalyzer/models/__init__.py new file mode 100644 index 0000000..68a73b7 --- /dev/null +++ b/src/zipalyzer/models/__init__.py @@ -0,0 +1,6 @@ +"""Models.""" + +from .generic import Message +from .metadata import ServerMetadata + +__all__ = ["Message", "ServerMetadata"] diff --git a/src/zipalyzer/models/generic.py b/src/zipalyzer/models/generic.py new file mode 100644 index 0000000..cb25f60 --- /dev/null +++ b/src/zipalyzer/models/generic.py @@ -0,0 +1,9 @@ +"""Model definitions.""" + +from pydantic import BaseModel + + +class Message(BaseModel): + """A message.""" + + detail: str diff --git a/src/zipalyzer/models/metadata.py b/src/zipalyzer/models/metadata.py new file mode 100644 index 0000000..e7df19a --- /dev/null +++ b/src/zipalyzer/models/metadata.py @@ -0,0 +1,10 @@ +"""Model definitions.""" + +from pydantic import BaseModel + + +class ServerMetadata(BaseModel): + """Metadata about the server.""" + + version: str + server_commit: str diff --git a/src/zipalyzer/server.py b/src/zipalyzer/server.py new file mode 100644 index 0000000..ed82b9d --- /dev/null +++ b/src/zipalyzer/server.py @@ -0,0 +1,34 @@ +"""API server definition.""" + +import sentry_sdk +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from zipalyzer.metadata.routes import router as router_metadata + +from . import __version__ +from .constants import GIT_SHA, Sentry + +sentry_sdk.init( + dsn=Sentry.dsn, + environment=Sentry.environment, + send_default_pii=True, + traces_sample_rate=0.05, + profiles_sample_rate=0.05, + release=f"{Sentry.release_prefix}@{GIT_SHA}", +) + +app = FastAPI( + title="Zipalyzer", + description="An API for analyzing zip files", + version=__version__, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(router_metadata) diff --git a/tests/test_server.py b/tests/test_server.py new file mode 100644 index 0000000..8f1375c --- /dev/null +++ b/tests/test_server.py @@ -0,0 +1,20 @@ +"""Test server.""" + +from http import HTTPStatus + +from fastapi.testclient import TestClient + +from zipalyzer import __version__ +from zipalyzer.server import app + +client = TestClient(app) + + +def test_read_main() -> None: + """Test getting the root route.""" + response = client.get("/") + assert response.status_code == HTTPStatus.OK + assert response.json() == { + "version": __version__, + "server_commit": "development", + }