diff --git a/.copier-answers.yml b/.copier-answers.yml index d30483b..bf831f1 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -1,16 +1,19 @@ # Changes here will be overwritten by Copier -_commit: 2.1.0 +_commit: 3.0.0 _src_path: gh:DiamondLightSource/python-copier-template -author_email: david.perl@diamond.ac.uk -author_name: David Perl -component_owner: group:default/sscc -description: A service to put and get your config values from +author_email: oliver.silvester@diamond.ac.uk +author_name: Oliver Silvester +component_lifecycle: experimental +component_owner: group:default/data-acquisition +component_type: service +description: A service to read files on dls_sw from a blueapi container distribution_name: daq-config-server -docker: false -docs_type: README +docker: true +docs_type: sphinx git_platform: github.com github_org: DiamondLightSource package_name: daq_config_server pypi: true repo_name: daq-config-server +strict_typing: true type_checker: pyright diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 79b85ff..fce9dd5 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -28,11 +28,11 @@ } }, "features": { - // Some default things like git config - "ghcr.io/devcontainers/features/common-utils:2": { - "upgradePackages": false - } + // add in eternal history and other bash features + "ghcr.io/diamondlightsource/devcontainer-features/bash-config:1": {} }, + // Create the config folder for the bash-config feature + "initializeCommand": "mkdir -p ${localEnv:HOME}/.config/bash-config", "runArgs": [ // Allow the container to access the host X11 display and EPICS CA "--net=host", @@ -43,4 +43,4 @@ "workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind", // After the container is created, install the python project in editable form "postCreateCommand": "pip install $([ -f dev-requirements.txt ] && echo '-c dev-requirements.txt') -e '.[dev]' && pre-commit install" -} \ No newline at end of file +} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index b72a22f..edf2dcb 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -24,4 +24,4 @@ It is recommended that developers use a [vscode devcontainer](https://code.visua This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects. -For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/2.1.0/how-to.html). +For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/3.0.0/how-to.html). diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..92087f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug Report +about: The template to use for reporting bugs and usability issues +title: " " +labels: 'bug' +assignees: '' + +--- + +Describe the bug, including a clear and concise description of the expected behaviour, the actual behavior and the context in which you encountered it (ideally include details of your environment). + +## Steps To Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + + +## Acceptance Criteria +- Specific criteria that will be used to judge if the issue is fixed diff --git a/.github/ISSUE_TEMPLATE/issue.md b/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 0000000..52c84dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,13 @@ +--- +name: Issue +about: The standard template to use for feature requests, design discussions and tasks +title: " " +labels: '' +assignees: '' + +--- + +A brief description of the issue, including specific stakeholders and the business case where appropriate + +## Acceptance Criteria +- Specific criteria that will be used to judge if the issue is fixed diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..8200afe --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,8 @@ +Fixes #ISSUE + +### Instructions to reviewer on how to test: +1. Do thing x +2. Confirm thing y happens + +### Checks for reviewer +- [ ] Would the PR title make sense to a user on a set of release notes diff --git a/.github/pages/index.html b/.github/pages/index.html index 80f0a00..c495f39 100644 --- a/.github/pages/index.html +++ b/.github/pages/index.html @@ -8,4 +8,4 @@ - \ No newline at end of file + diff --git a/.github/pages/make_switcher.py b/.github/pages/make_switcher.py index 29f646c..c06813a 100755 --- a/.github/pages/make_switcher.py +++ b/.github/pages/make_switcher.py @@ -1,3 +1,5 @@ +"""Make switcher.json to allow docs to switch between different versions.""" + import json import logging from argparse import ArgumentParser @@ -6,6 +8,7 @@ def report_output(stdout: bytes, label: str) -> list[str]: + """Print and return something received frm stdout.""" ret = stdout.decode().strip().split("\n") print(f"{label}: {ret}") return ret @@ -52,14 +55,12 @@ def get_versions(ref: str, add: str | None) -> list[str]: return versions -def write_json(path: Path, repository: str, versions: str): +def write_json(path: Path, repository: str, versions: list[str]): + """Write the JSON switcher to path.""" org, repo_name = repository.split("/") - pages_url = f"https://{org}.github.io" - if repo_name != f"{org}.github.io": - # Only add the repo name if it isn't the source for the org pages site - pages_url += f"/{repo_name}" struct = [ - {"version": version, "url": f"{pages_url}/{version}/"} for version in versions + {"version": version, "url": f"https://{org}.github.io/{repo_name}/{version}/"} + for version in versions ] text = json.dumps(struct, indent=2) print(f"JSON switcher:\n{text}") @@ -67,6 +68,7 @@ def write_json(path: Path, repository: str, versions: str): def main(args=None): + """Parse args and write switcher.""" parser = ArgumentParser( description="Make a versions.json file from gh-pages directories" ) diff --git a/.github/workflows/_container.yml b/.github/workflows/_container.yml new file mode 100644 index 0000000..da5e493 --- /dev/null +++ b/.github/workflows/_container.yml @@ -0,0 +1,60 @@ +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Docker Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and export to Docker local cache + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + with: + context: . + # Need load and tags so we can test it below + load: true + tags: tag_for_testing + + - name: Test cli works in cached runtime image + run: docker run --rm tag_for_testing --version + + - name: Create tags for publishing image + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=tag + type=raw,value=latest + + - name: Push cached image to container registry + if: github.ref_type == 'tag' + uses: docker/build-push-action@v6 + env: + DOCKER_BUILD_RECORD_UPLOAD: false + # This does not build the image again, it will find the image in the + # Docker cache and publish it + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/_docs.yml b/.github/workflows/_docs.yml new file mode 100644 index 0000000..a1cafca --- /dev/null +++ b/.github/workflows/_docs.yml @@ -0,0 +1,54 @@ +on: + workflow_call: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Avoid git conflicts when tag and branch pushed at same time + if: github.ref_type == 'tag' + run: sleep 60 + + - name: Checkout + uses: actions/checkout@v4 + with: + # Need this to get version number from last tag + fetch-depth: 0 + + - name: Install system packages + run: sudo apt-get install graphviz + + - name: Install python packages + uses: ./.github/actions/install_requirements + + - name: Build docs + run: tox -e docs + + - name: Remove environment.pickle + run: rm build/html/.doctrees/environment.pickle + + - name: Upload built docs artifact + uses: actions/upload-artifact@v4 + with: + name: docs + path: build + + - name: Sanitize ref name for docs version + run: echo "DOCS_VERSION=${GITHUB_REF_NAME//[^A-Za-z0-9._-]/_}" >> $GITHUB_ENV + + - name: Move to versioned directory + run: mv build/html .github/pages/$DOCS_VERSION + + - name: Write switcher.json + run: python .github/pages/make_switcher.py --add $DOCS_VERSION ${{ github.repository }} .github/pages/switcher.json + + - name: Publish Docs to gh-pages + if: github.ref_type == 'tag' || github.ref_name == 'main' + # We pin to the SHA, not the tag, for security reasons. + # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions + uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: .github/pages + keep_files: true diff --git a/.github/workflows/_pypi.yml b/.github/workflows/_pypi.yml index 0c5258d..8032bba 100644 --- a/.github/workflows/_pypi.yml +++ b/.github/workflows/_pypi.yml @@ -15,3 +15,5 @@ jobs: - name: Publish to PyPI using trusted publishing uses: pypa/gh-action-pypi-publish@release/v1 + with: + attestations: false diff --git a/.github/workflows/_release.yml b/.github/workflows/_release.yml index 10d8ed8..c771682 100644 --- a/.github/workflows/_release.yml +++ b/.github/workflows/_release.yml @@ -23,7 +23,7 @@ jobs: - name: Create GitHub Release # We pin to the SHA, not the tag, for security reasons. # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions - uses: softprops/action-gh-release@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 with: prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }} files: "*" diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index d02b93d..f88f506 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -54,7 +54,7 @@ jobs: run: tox -e unit_tests - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: name: ${{ inputs.python-version }}/${{ inputs.runs-on }} files: cov.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da07f69..1784710 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: runs-on: ["ubuntu-latest"] # can add windows-latest, macos-latest - python-version: ["3.11", "3.12"] + python-version: ["3.11", "3.12", "3.13"] include: # Include one that runs in the dev environment - runs-on: "ubuntu-latest" @@ -34,6 +34,19 @@ jobs: secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + container: + needs: check + if: needs.check.outputs.branch-pr == '' + uses: ./.github/workflows/_container.yml + permissions: + contents: read + packages: write + + docs: + needs: check + if: needs.check.outputs.branch-pr == '' + uses: ./.github/workflows/_docs.yml + dist: needs: check if: needs.check.outputs.branch-pr == '' @@ -48,7 +61,7 @@ jobs: release: if: github.ref_type == 'tag' - needs: [dist] + needs: [dist, docs] uses: ./.github/workflows/_release.yml permissions: contents: write diff --git a/.github/workflows/periodic.yml b/.github/workflows/periodic.yml new file mode 100644 index 0000000..e2a0fd1 --- /dev/null +++ b/.github/workflows/periodic.yml @@ -0,0 +1,13 @@ +name: Periodic + +on: + workflow_dispatch: + schedule: + # Run weekly to check URL links still resolve + - cron: "0 8 * * WED" + +jobs: + linkcheck: + uses: ./.github/workflows/_tox.yml + with: + tox: docs build -- -b linkcheck diff --git a/.gitignore b/.gitignore index 2db76ff..0626c9d 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ cov.xml # Sphinx documentation docs/_build/ +docs/_api # PyBuilder target/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d7942d..737c1e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,13 +7,14 @@ repos: args: [--allow-multiple-documents] exclude: ^helmchart/ - id: check-merge-conflict + - id: end-of-file-fixer - repo: local hooks: - id: ruff name: lint with ruff language: system - entry: ruff check --force-exclude + entry: ruff check --force-exclude --fix types: [python] exclude: ^.github/pages require_serial: true @@ -38,4 +39,4 @@ repos: language: system entry: npx eslint gui/config-server-gui/src types: [javascript,jsx,ts,tsx] - require_serial: true \ No newline at end of file + require_serial: true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 66ad632..933c580 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,4 +2,4 @@ "recommendations": [ "ms-vscode-remote.remote-containers", ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index e9d83ff..6f73f55 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,7 @@ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, + "files.insertFinalNewline": true, "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", }, @@ -25,4 +26,4 @@ "[yaml]": { "editor.defaultFormatter": "ms-kubernetes-tools.vscode-kubernetes-tools", } -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 946e69d..c999e86 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,4 +13,4 @@ "problemMatcher": [], } ] -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile index ebebe42..3ae59ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # The devcontainer should use the developer target and run as root with podman # or docker with user namespaces. ARG PYTHON_VERSION=3.11 -FROM python:${PYTHON_VERSION} as developer +FROM python:${PYTHON_VERSION} AS developer # Add any system dependencies for the developer/build environment here RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -28,5 +28,4 @@ COPY --from=build /venv/ /venv/ COPY tests/test_data/beamline_parameters.txt tests/test_data/beamline_parameters.txt ENV PATH=/venv/bin:$PATH -# change this entrypoint if it is not the same as the repo -CMD daq-config-server +ENTRYPOINT ["daq-config-server"] diff --git a/README.md b/README.md index 38bee1e..e814f5b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Backend CI](https://github.com/DiamondLightSource/daq-config-server/actions/workflows/ci.yml/badge.svg)](https://github.com/DiamondLightSource/daq-config-server/actions/workflows/backend_ci.yml) [![Coverage](https://codecov.io/gh/DiamondLightSource/daq-config-server/branch/main/graph/badge.svg)](https://codecov.io/gh/DiamondLightSource/daq-config-server) [![PyPI](https://img.shields.io/pypi/v/daq-config-server.svg)](https://pypi.org/project/daq-config-server) -[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) # DAQ Config Server @@ -62,3 +62,5 @@ kubectl port-forward service/daq-config-server-svc 8555 ``` after which you should be able to access the API on `http://localhost:8555/docs` + + diff --git a/catalog-info.yaml b/catalog-info.yaml index 1c1036f..bea3448 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -3,8 +3,8 @@ kind: Component metadata: name: daq-config-server title: daq-config-server - description: A service to put and get your config values from + description: A service to read files on dls_sw from a blueapi container spec: - type: documentation + type: service lifecycle: experimental - owner: group:default/sscc \ No newline at end of file + owner: group:default/data-acquisition diff --git a/deployment/build_and_push.sh b/deployment/build_and_push.sh index 7653996..e64654b 100755 --- a/deployment/build_and_push.sh +++ b/deployment/build_and_push.sh @@ -59,4 +59,4 @@ podman build --build-arg -t $MAIN_CONTAINER_NAME . if [ $PUSH -gt 0 ]; then podman tag $MAIN_CONTAINER_NAME $MAIN_CONTAINER_TAG podman push $MAIN_CONTAINER_NAME $MAIN_CONTAINER_TAG -fi \ No newline at end of file +fi diff --git a/dev-values.yaml b/dev-values.yaml index c5b8f65..d1ac267 100644 --- a/dev-values.yaml +++ b/dev-values.yaml @@ -7,4 +7,4 @@ imageNames: db: "gcr.io/diamond-privreg/daq-config-server/daq-config-server-db-dev" ingress: - enabled: false \ No newline at end of file + enabled: false diff --git a/docs/_api.rst b/docs/_api.rst new file mode 100644 index 0000000..67a4f0b --- /dev/null +++ b/docs/_api.rst @@ -0,0 +1,16 @@ +:orphan: + +.. + This page is not included in the TOC tree, but must exist so that the + autosummary pages are generated for daq_config_server and all its + subpackages + +API +=== + +.. autosummary:: + :toctree: _api + :template: custom-module-template.rst + :recursive: + + daq_config_server diff --git a/docs/_templates/custom-module-template.rst b/docs/_templates/custom-module-template.rst new file mode 100644 index 0000000..9aeca54 --- /dev/null +++ b/docs/_templates/custom-module-template.rst @@ -0,0 +1,37 @@ +{{ ('``' + fullname + '``') | underline }} + +{%- set filtered_members = [] %} +{%- for item in members %} + {%- if item in functions + classes + exceptions + attributes %} + {% set _ = filtered_members.append(item) %} + {%- endif %} +{%- endfor %} + +.. automodule:: {{ fullname }} + :members: + + {% block modules %} + {% if modules %} + .. rubric:: Submodules + + .. autosummary:: + :toctree: + :template: custom-module-template.rst + :recursive: + {% for item in modules %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} + + {% block members %} + {% if filtered_members %} + .. rubric:: Members + + .. autosummary:: + :nosignatures: + {% for item in filtered_members %} + {{ item }} + {%- endfor %} + {% endif %} + {% endblock %} diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..93a7616 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,191 @@ +"""Configuration file for the Sphinx documentation builder. + +This file only contains a selection of the most common options. For a full +list see the documentation: +https://www.sphinx-doc.org/en/master/usage/configuration.html +""" + +import sys +from pathlib import Path +from subprocess import check_output + +import requests + +import daq_config_server + +# -- General configuration ------------------------------------------------ + +# General information about the project. +project = "daq-config-server" + +# The full version, including alpha/beta/rc tags. +release = daq_config_server.__version__ + +# The short X.Y version. +if "+" in release: + # Not on a tag, use branch name + root = Path(__file__).absolute().parent.parent + git_branch = check_output("git branch --show-current".split(), cwd=root) + version = git_branch.decode().strip() +else: + version = release + +extensions = [ + # Use this for generating API docs + "sphinx.ext.autodoc", + # and making summary tables at the top of API docs + "sphinx.ext.autosummary", + # This can parse google style docstrings + "sphinx.ext.napoleon", + # For linking to external sphinx documentation + "sphinx.ext.intersphinx", + # Add links to source code in API docs + "sphinx.ext.viewcode", + # Adds the inheritance-diagram generation directive + "sphinx.ext.inheritance_diagram", + # Add a copy button to each code block + "sphinx_copybutton", + # For the card element + "sphinx_design", + # So we can write markdown files + "myst_parser", +] + +# So we can use the ::: syntax +myst_enable_extensions = ["colon_fence"] + +# If true, Sphinx will warn about all references where the target cannot +# be found. +nitpicky = True + +# A list of (type, target) tuples (by default empty) that should be ignored when +# generating warnings in "nitpicky mode". Note that type should include the +# domain name if present. Example entries would be ('py:func', 'int') or +# ('envvar', 'LD_LIBRARY_PATH'). +nitpick_ignore = [ + ("py:class", "NoneType"), + ("py:class", "'str'"), + ("py:class", "'float'"), + ("py:class", "'int'"), + ("py:class", "'bool'"), + ("py:class", "'object'"), + ("py:class", "'id'"), + ("py:class", "typing_extensions.Literal"), +] + +# Both the class’ and the __init__ method’s docstring are concatenated and +# inserted into the main body of the autoclass directive +autoclass_content = "both" + +# Order the members by the order they appear in the source code +autodoc_member_order = "bysource" + +# Don't inherit docstrings from baseclasses +autodoc_inherit_docstrings = False + +# Document only what is in __all__ +autosummary_ignore_module_all = False + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# Output graphviz directive produced images in a scalable format +graphviz_output_format = "svg" + +# The name of a reST role (builtin or Sphinx extension) to use as the default +# role, that is, for text marked up `like this` +default_role = "any" + +# The master toctree document. +master_doc = "index" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# These patterns also affect html_static_path and html_extra_path +exclude_patterns = ["_build"] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# This means you can link things like `str` and `asyncio` to the relevant +# docs in the python documentation. +intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} + +# A dictionary of graphviz graph attributes for inheritance diagrams. +inheritance_graph_attrs = {"rankdir": "TB"} + +# Ignore localhost links for periodic check that links in docs are valid +linkcheck_ignore = [r"http://localhost:\d+/"] + +# Set copy-button to ignore python and bash prompts +# https://sphinx-copybutton.readthedocs.io/en/latest/use.html#using-regexp-prompt-identifiers +copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " +copybutton_prompt_is_regexp = True + +# -- 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 = "pydata_sphinx_theme" +github_repo = "daq-config-server" +github_user = "DiamondLightSource" +switcher_json = f"https://{github_user}.github.io/{github_repo}/switcher.json" +switcher_exists = requests.get(switcher_json).ok +if not switcher_exists: + print( + "*** Can't read version switcher, is GitHub pages enabled? \n" + " Once Docs CI job has successfully run once, set the " + "Github pages source branch to be 'gh-pages' at:\n" + f" https://github.com/{github_user}/{github_repo}/settings/pages", + file=sys.stderr, + ) + +# Theme options for pydata_sphinx_theme +# We don't check switcher because there are 3 possible states for a repo: +# 1. New project, docs are not published so there is no switcher +# 2. Existing project with latest copier template, switcher exists and works +# 3. Existing project with old copier template that makes broken switcher, +# switcher exists but is broken +# Point 3 makes checking switcher difficult, because the updated copier template +# will fix the switcher at the end of the docs workflow, but never gets a chance +# to complete as the docs build warns and fails. +html_theme_options = { + "logo": { + "text": project, + }, + "use_edit_page_button": True, + "github_url": f"https://github.com/{github_user}/{github_repo}", + "icon_links": [ + { + "name": "PyPI", + "url": f"https://pypi.org/project/{project}", + "icon": "fas fa-cube", + } + ], + "switcher": { + "json_url": switcher_json, + "version_match": version, + }, + "check_switcher": False, + "navbar_end": ["theme-switcher", "icon-links", "version-switcher"], + "navigation_with_keys": False, +} + +# A dictionary of values to pass into the template engine’s context for all pages +html_context = { + "github_user": github_user, + "github_repo": github_repo, + "github_version": version, + "doc_path": "docs", +} + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False + +# Logo +html_logo = "images/dls-logo.svg" +html_favicon = html_logo diff --git a/docs/explanations.md b/docs/explanations.md new file mode 100644 index 0000000..73ab289 --- /dev/null +++ b/docs/explanations.md @@ -0,0 +1,10 @@ +# Explanations + +Explanations of how it works and why it works that way. + +```{toctree} +:maxdepth: 1 +:glob: + +explanations/* +``` diff --git a/docs/explanations/decisions.md b/docs/explanations/decisions.md new file mode 100644 index 0000000..0533b98 --- /dev/null +++ b/docs/explanations/decisions.md @@ -0,0 +1,12 @@ +# Architectural Decision Records + +Architectural decisions are made throughout a project's lifetime. As a way of keeping track of these decisions, we record these decisions in Architecture Decision Records (ADRs) listed below. + +```{toctree} +:glob: true +:maxdepth: 1 + +decisions/* +``` + +For more information on ADRs see this [blog by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). diff --git a/docs/explanations/decisions/0001-record-architecture-decisions.md b/docs/explanations/decisions/0001-record-architecture-decisions.md new file mode 100644 index 0000000..44d234e --- /dev/null +++ b/docs/explanations/decisions/0001-record-architecture-decisions.md @@ -0,0 +1,18 @@ +# 1. Record architecture decisions + +## Status + +Accepted + +## Context + +We need to record the architectural decisions made on this project. + +## Decision + +We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions). + +## Consequences + +See Michael Nygard's article, linked above. To create new ADRs we will copy and +paste from existing ones. diff --git a/docs/explanations/decisions/0002-switched-to-python-copier-template.md b/docs/explanations/decisions/0002-switched-to-python-copier-template.md new file mode 100644 index 0000000..66fe5d8 --- /dev/null +++ b/docs/explanations/decisions/0002-switched-to-python-copier-template.md @@ -0,0 +1,28 @@ +# 2. Adopt python-copier-template for project structure + +## Status + +Accepted + +## Context + +We should use the following [python-copier-template](https://github.com/DiamondLightSource/python-copier-template). +The template will ensure consistency in developer +environments and package management. + +## Decision + +We have switched to using the template. + +## Consequences + +This module will use a fixed set of tools as developed in `python-copier-template` +and can pull from this template to update the packaging to the latest techniques. + +As such, the developer environment may have changed, the following could be +different: + +- linting +- formatting +- pip venv setup +- CI/CD diff --git a/docs/explanations/decisions/COPYME b/docs/explanations/decisions/COPYME new file mode 100644 index 0000000..b466c79 --- /dev/null +++ b/docs/explanations/decisions/COPYME @@ -0,0 +1,19 @@ +# 3. Short descriptive title + +Date: Today's date + +## Status + +Accepted + +## Context + +Background to allow us to make the decision, to show how we arrived at our conclusions. + +## Decision + +What decision we made. + +## Consequences + +What we will do as a result of this decision. diff --git a/docs/genindex.md b/docs/genindex.md new file mode 100644 index 0000000..73f1191 --- /dev/null +++ b/docs/genindex.md @@ -0,0 +1,3 @@ +# Index + + diff --git a/docs/how-to.md b/docs/how-to.md new file mode 100644 index 0000000..6b16141 --- /dev/null +++ b/docs/how-to.md @@ -0,0 +1,10 @@ +# How-to Guides + +Practical step-by-step guides for the more experienced user. + +```{toctree} +:maxdepth: 1 +:glob: + +how-to/* +``` diff --git a/docs/how-to/contribute.md b/docs/how-to/contribute.md new file mode 100644 index 0000000..6e41979 --- /dev/null +++ b/docs/how-to/contribute.md @@ -0,0 +1,2 @@ +```{include} ../../.github/CONTRIBUTING.md +``` diff --git a/docs/how-to/run-container.md b/docs/how-to/run-container.md new file mode 100644 index 0000000..14f065e --- /dev/null +++ b/docs/how-to/run-container.md @@ -0,0 +1,14 @@ +# Run in a container + +Pre-built containers with daq-config-server and its dependencies already +installed are available on [Github Container Registry](https://ghcr.io/DiamondLightSource/daq-config-server). + +## Starting the container + +To pull the container from github container registry and run: + +``` +$ docker run ghcr.io/diamondlightsource/daq-config-server:latest --version +``` + +To get a released version, use a numbered release instead of `latest`. diff --git a/docs/images/dls-logo.svg b/docs/images/dls-logo.svg new file mode 100644 index 0000000..4fcaa86 --- /dev/null +++ b/docs/images/dls-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..cc2fe33 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,55 @@ +--- +html_theme.sidebar_secondary.remove: true +--- + +```{include} ../README.md +:end-before: + +::::{grid} 2 +:gutter: 4 + +:::{grid-item-card} {material-regular}`directions_walk;2em` +```{toctree} +:maxdepth: 2 +tutorials +``` ++++ +Tutorials for installation and typical usage. New users start here. +::: + +:::{grid-item-card} {material-regular}`directions;2em` +```{toctree} +:maxdepth: 2 +how-to +``` ++++ +Practical step-by-step guides for the more experienced user. +::: + +:::{grid-item-card} {material-regular}`info;2em` +```{toctree} +:maxdepth: 2 +explanations +``` ++++ +Explanations of how it works and why it works that way. +::: + +:::{grid-item-card} {material-regular}`menu_book;2em` +```{toctree} +:maxdepth: 2 +reference +``` ++++ +Technical reference material including APIs and release notes. +::: + +:::: diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..90c367c --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,12 @@ +# Reference + +Technical reference material including APIs and release notes. + +```{toctree} +:maxdepth: 1 +:glob: + +API <_api/daq_config_server> +genindex +Release Notes +``` diff --git a/docs/tutorials.md b/docs/tutorials.md new file mode 100644 index 0000000..1fe66c5 --- /dev/null +++ b/docs/tutorials.md @@ -0,0 +1,10 @@ +# Tutorials + +Tutorials for installation and typical usage. New users start here. + +```{toctree} +:maxdepth: 1 +:glob: + +tutorials/* +``` diff --git a/docs/tutorials/installation.md b/docs/tutorials/installation.md new file mode 100644 index 0000000..d6424a9 --- /dev/null +++ b/docs/tutorials/installation.md @@ -0,0 +1,42 @@ +# Installation + +## Check your version of python + +You will need python 3.11 or later. You can check your version of python by +typing into a terminal: + +``` +$ python3 --version +``` + +## Create a virtual environment + +It is recommended that you install into a “virtual environment” so this +installation will not interfere with any existing Python software: + +``` +$ python3 -m venv /path/to/venv +$ source /path/to/venv/bin/activate +``` + +## Installing the library + +You can now use `pip` to install the library and its dependencies: + +``` +$ python3 -m pip install daq-config-server +``` + +If you require a feature that is not currently released you can also install +from github: + +``` +$ python3 -m pip install git+https://github.com/DiamondLightSource/daq-config-server.git +``` + +The library should now be installed and the commandline interface on your path. +You can check the version that has been installed by typing: + +``` +$ daq-config-server --version +``` diff --git a/pyproject.toml b/pyproject.toml index 2b2b2aa..1180cfd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2"] +requires = ["setuptools>=64", "setuptools_scm[toml]>=8"] build-backend = "setuptools.build_meta" [project] @@ -8,8 +8,10 @@ classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: Apache Software License", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] -description = "A service to put and get your config values from" +description = "A service to read files on dls_sw from a blueapi container" dependencies = ["urllib3", "requests"] dynamic = ["version"] license.file = "LICENSE" @@ -22,13 +24,18 @@ dev = [ "copier", "httpx", "mockito", + "myst-parser", "pipdeptree", "pre-commit", + "pydata-sphinx-theme>=0.12", "pyright", "pytest", "pytest-cov", "pytest-asyncio", "ruff", + "sphinx-autobuild", + "sphinx-copybutton", + "sphinx-design", "tox-direct", "types-mock", "cachetools", @@ -43,14 +50,15 @@ daq-config-server = "daq_config_server.__main__:main" GitHub = "https://github.com/DiamondLightSource/daq-config-server" [[project.authors]] # Further authors may be added by duplicating this section -email = "david.perl@diamond.ac.uk" -name = "David Perl" +email = "oliver.silvester@diamond.ac.uk" +name = "Oliver Silvester" [tool.setuptools_scm] -write_to = "src/daq_config_server/_version.py" +version_file = "src/daq_config_server/_version.py" [tool.pyright] +typeCheckingMode = "strict" reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] @@ -58,7 +66,7 @@ asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" # Run pytest with all our checkers, and don't spam us with massive tracebacks on error addopts = """ - --tb=native -vv + --tb=native -vv --doctest-modules --doctest-glob="*.rst" """ markers = """ requires_local_server: mark a test which requires locally hosting the config server @@ -83,7 +91,7 @@ legacy_tox_ini = """ [tox] skipsdist=True -[testenv] +[testenv:{pre-commit,type-checking,unit_tests,docs}] # Don't create a virtualenv for the command, requires tox-direct plugin direct = True passenv = * @@ -93,11 +101,11 @@ allowlist_externals = pyright sphinx-build sphinx-autobuild -[testenv:{pre-commit,type-checking,unit_tests,system_tests}] commands = pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs} type-checking: pyright src tests {posargs} unit_tests: pytest --cov=daq_config_server --cov-report term --cov-report xml:cov.xml {posargs} tests/unit_tests + docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html system_tests: pytest tests/system_tests """ @@ -105,11 +113,18 @@ commands = src = ["src", "tests"] line-length = 88 lint.select = [ - "B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b - "C4", # flake8-comprehensions - https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - "E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e - "F", # pyflakes rules - https://docs.astral.sh/ruff/rules/#pyflakes-f - "W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w - "I", # isort - https://docs.astral.sh/ruff/rules/#isort-i - "UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up + "B", # flake8-bugbear - https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "C4", # flake8-comprehensions - https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "E", # pycodestyle errors - https://docs.astral.sh/ruff/rules/#error-e + "F", # pyflakes rules - https://docs.astral.sh/ruff/rules/#pyflakes-f + "W", # pycodestyle warnings - https://docs.astral.sh/ruff/rules/#warning-w + "I", # isort - https://docs.astral.sh/ruff/rules/#isort-i + "UP", # pyupgrade - https://docs.astral.sh/ruff/rules/#pyupgrade-up + "SLF", # self - https://docs.astral.sh/ruff/settings/#lintflake8-self ] + +[tool.ruff.lint.per-file-ignores] +# By default, private member access is allowed in tests +# See https://github.com/DiamondLightSource/python-copier-template/issues/154 +# Remove this line to forbid private member access in tests +"tests/**/*" = ["SLF001"] diff --git a/src/daq_config_server/__init__.py b/src/daq_config_server/__init__.py index 26d23ba..a2ffbf3 100644 --- a/src/daq_config_server/__init__.py +++ b/src/daq_config_server/__init__.py @@ -1,3 +1,11 @@ +"""Top level API. + +.. data:: __version__ + :type: str + + Version number as calculated by https://github.com/pypa/setuptools_scm +""" + from ._version import __version__ __all__ = ["__version__"] diff --git a/src/daq_config_server/__main__.py b/src/daq_config_server/__main__.py index 0ee1ca3..86796fe 100644 --- a/src/daq_config_server/__main__.py +++ b/src/daq_config_server/__main__.py @@ -1,3 +1,5 @@ +"""Interface for ``python -m daq_config_server``.""" + from argparse import ArgumentParser from . import __version__ @@ -11,8 +13,8 @@ def check_server_dependencies(): try: - import uvicorn # noqa - from fastapi import FastAPI # noqa + import uvicorn # type: ignore # noqa: F401 + from fastapi import FastAPI # type: ignore # noqa return True @@ -33,6 +35,5 @@ def main(): main() -# test with: python -m daq_config_server if __name__ == "__main__": main() diff --git a/src/daq_config_server/client.py b/src/daq_config_server/client.py index 4f72379..e8ad56d 100644 --- a/src/daq_config_server/client.py +++ b/src/daq_config_server/client.py @@ -27,7 +27,9 @@ def __init__( """ self._url = url.rstrip("/") self._log = log if log else getLogger("daq_config_server.client") - self._cache = TTLCache(maxsize=cache_size, ttl=cache_lifetime_s) + self._cache: TTLCache[tuple[str, str | None], str] = TTLCache( + maxsize=cache_size, ttl=cache_lifetime_s + ) def _get( self, diff --git a/tests/test_data/beamline_parameters.txt b/tests/test_data/beamline_parameters.txt index 83243cb..5247b15 100644 --- a/tests/test_data/beamline_parameters.txt +++ b/tests/test_data/beamline_parameters.txt @@ -295,4 +295,4 @@ fluorescence_mca_sca_offset = 400 attenuation_optimisation_multiplier = 2 attenuation_optimisation_target_count = 2000 attenuation_optimisation_upper_limit = 50000 -attenuation_optimisation_lower_limit = 20000 \ No newline at end of file +attenuation_optimisation_lower_limit = 20000 diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index b8827ee..b750bb7 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -19,7 +19,7 @@ def test_cli_version(): @patch("daq_config_server.__main__.ArgumentParser.parse_args") @patch.dict("sys.modules", {"uvicorn": None, "fastapi": None}) def test_print_and_exit_if_incorrect_dependencies( - mock_parse_args, mock_print: MagicMock, mock_main: MagicMock + mock_parse_args: MagicMock, mock_print: MagicMock, mock_main: MagicMock ): main() mock_print.assert_called_once_with(INSUFFICIENT_DEPENDENCIES_MESSAGE) @@ -28,6 +28,8 @@ def test_print_and_exit_if_incorrect_dependencies( @patch("daq_config_server.app.main") @patch("daq_config_server.__main__.ArgumentParser.parse_args") -def test_main_runs_with_correct_dependencies(mock_parse_args, mock_main: MagicMock): +def test_main_runs_with_correct_dependencies( + mock_parse_args: MagicMock, mock_main: MagicMock +): main() mock_main.assert_called_once() diff --git a/tests/unit_tests/test_client.py b/tests/unit_tests/test_client.py index c8a3b09..464600b 100644 --- a/tests/unit_tests/test_client.py +++ b/tests/unit_tests/test_client.py @@ -4,12 +4,17 @@ import pytest import requests from fastapi import status +from requests import RequestException from daq_config_server.client import ConfigServer from daq_config_server.constants import ENDPOINTS -def make_mock_response(json_value, status_code=200, raise_exc=None): +def make_mock_response( + json_value: str | dict[str, str], + status_code: int = 200, + raise_exc: type[RequestException] | None = None, +): mock_response = MagicMock() mock_response.json.return_value = json_value mock_response.status_code = status_code