diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..065f5eb --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,18 @@ +[bumpversion] +current_version = 0.0.1 +commit = True +tag = True + +[bumpversion:file:VERSION] + +[bumpversion:file:.github/workflows/deploy_python_package.yml] +search = PACKAGE_VERSION: {current_version} +replace = PACKAGE_VERSION: {new_version} + +[bumpversion:file:pyproject.toml] +search = version = "{current_version}" +replace = version = "{new_version}" + +[bumpversion:file:pylabrobot_sila_bridge/__init__.py] +search = __version__ = "{current_version}" +replace = __version__ = "{new_version}" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2ffc016 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,31 @@ +# http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +insert_final_newline = true +charset = utf-8 +end_of_line = lf +max_line_length = 120 + +[*.py] +indent_size = 4 + +[*.toml] +indent_size = 4 + +[*.bat] +indent_style = tab +end_of_line = crlf + +[LICENSE] +insert_final_newline = false + +[*.md] +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..e69de29 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..68a25f1 --- /dev/null +++ b/.flake8 @@ -0,0 +1,7 @@ +[flake8] +ignore = E203, W503 +select = B,B9,C,E,F,W +max-complexity = 10 +max-line-length = 120 +per-file-ignores = + test/*:S101 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..df99032 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,44 @@ +# this file is used by git large file support (git-lfs) to move +# large / binary files to a special location / database +*.jar filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.ZIP filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tar.bz2 filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.PNG filter=lfs diff=lfs merge=lfs -text +*.jpg filter=lfs diff=lfs merge=lfs -text +*.JPG filter=lfs diff=lfs merge=lfs -text +*.jpeg filter=lfs diff=lfs merge=lfs -text +*.JPEG filter=lfs diff=lfs merge=lfs -text +*.pdf filter=lfs diff=lfs merge=lfs -text +*.PDF filter=lfs diff=lfs merge=lfs -text +*.mp3 filter=lfs diff=lfs merge=lfs -text +*.MP3 filter=lfs diff=lfs merge=lfs -text +*.wav filter=lfs diff=lfs merge=lfs -text +*.WAV filter=lfs diff=lfs merge=lfs -text +*.mp4 filter=lfs diff=lfs merge=lfs -text +*.mov filter=lfs diff=lfs merge=lfs -text +*.ods filter=lfs diff=lfs merge=lfs -text +*.xls filter=lfs diff=lfs merge=lfs -text +*.xlsx filter=lfs diff=lfs merge=lfs -text +*.XLSX filter=lfs diff=lfs merge=lfs -text +*.odt filter=lfs diff=lfs merge=lfs -text +*.doc filter=lfs diff=lfs merge=lfs -text +*.DOC filter=lfs diff=lfs merge=lfs -text +*.docx filter=lfs diff=lfs merge=lfs -text +*.DOCX filter=lfs diff=lfs merge=lfs -text +*.sqlite filter=lfs diff=lfs merge=lfs -text +*.SQLITE filter=lfs diff=lfs merge=lfs -text +*.sqlite3 filter=lfs diff=lfs merge=lfs -text +*.SQLITE3 filter=lfs diff=lfs merge=lfs -text +*.exe filter=lfs diff=lfs merge=lfs -text +*.EXE filter=lfs diff=lfs merge=lfs -text +*.dll filter=lfs diff=lfs merge=lfs -text +*.DLL filter=lfs diff=lfs merge=lfs -text +*.so filter=lfs diff=lfs merge=lfs -text + + +# converting LF+CR to LF +* text=auto \ No newline at end of file diff --git a/.github/workflows/deploy_python_package.yml b/.github/workflows/deploy_python_package.yml new file mode 100644 index 0000000..3757fca --- /dev/null +++ b/.github/workflows/deploy_python_package.yml @@ -0,0 +1,58 @@ +# s. https://github.com/pallets/flask +name: PyLabRobot SiLA Bridge Python Package Push + +on: + push: + # Publish `master` as Docker `latest` image. + branches: + - main + - develop + #- seed + + # Publish `v1.2.3` tags as releases. + tags: + - v* + + # Run tests for any PRs. + pull_request: + +env: + PACKAGE_VERSION: 0.0.1 + PACKAGE_NAME: pylabrobot_sila_bridge-$PACKAGE_VERSION + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + - uses: actions/checkout@v3 + - name: Build package + run: | + cd SiLA + echo "Building SiLA python package ...." + ls -Al + python -m build + # Generate hashes used for provenance. + - name: generate hash + id: hash + run: | + echo "Generating hash ...." + cd SiLA + ls -Al + echo "dist: " + ls -Al dist + cd dist && echo "hash=$(sha256sum * | base64 -w0)" >> $GITHUB_OUTPUT + - uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce + with: + path: ./SiLA/dist + name: ${{ env.PACKAGE_NAME }} diff --git a/.github/workflows/pages_static_html.yml b/.github/workflows/pages_static_html.yml new file mode 100644 index 0000000..b8a27e4 --- /dev/null +++ b/.github/workflows/pages_static_html.yml @@ -0,0 +1,49 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Build Pages + run: | + cd docs + echo "Building documentation ...." + make html + ls -Al + # - name: Upload artifact + # uses: actions/upload-pages-artifact@v1 + # with: + # # Upload entire _built + # path: 'docs/_built' + # - name: Deploy to GitHub Pages + # id: deployment + # uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6861fb6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,116 @@ +# this file is used by git to ignore certain files or directories +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +# MANIFEST +*.spec + +# (virtual) environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# dotenv +.env + +# Cython debug symbols +cython_debug/ + +# SQLite database +*.sqlite +*.sqlite3 + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# PyCharm /IntelliJ Idea family of suites +.idea/ +*.iml + +# IDE settings - Visual Stuido code +.vscode/ + +# PyCharm +.idea + +# Mac stuff +.DS_Store + +# Generated files +bin/ +.build_* + +# Certificate files +*.pem +*.cer +*.crt +*.key diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..4c3093e --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,158 @@ +# Config file for GitLab CI pipeline + +image: python:latest + +# Change pip's cache directory to be inside the project directory since we can +# only cache local items. +variables: + PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" + SOURCE_DIR : "$CI_PROJECT_DIR/pylabrobot_sila_bridge" + DOCS_DIR : "docs" + DOCS_SOURCE_DIR : "docs/source" + DOCS_BUILD_DIR : "docs/_build" + +stages: + - build + - compliance + - test + - build_and_publish_docs + - publish_pypi + +# Configuration -------------------------------------------------------------------- + +# Pip's cache doesn't store the python packages +# https://pip.pypa.io/en/stable/reference/pip_install/#caching +# +# If you want to also cache the installed packages, you have to install +# them in a virtualenv and cache it as well. +cache: + paths: + - .cache/pip + - venv/ + +before_script: + - python -V # Print out python version for debugging + - pip install virtualenv + - virtualenv venv + - source venv/bin/activate + - pip install setuptools wheel + +.parallel_python_jobs: + parallel: + matrix: + - python_version: ["3.9", "3.10"] + +.run_on_all_branches: + only: + refs: + - develop + - merge_requests + - triggers + - master + - main + - tags + - /^v[0-9]+.[0-9]+.*/ + +.run_on_master: + only: + refs: + - triggers + - master + - main + +.run_on_release_tag: + only: + refs: + - tags + - /^v[0-9]+.[0-9]+.*/ + +.run_on_manual: + only: + refs: + - manual + +# Jobs: Build ----------------------------------------------------------------- + +build-package: + stage: build + image: python:latest + script: + - pip install twine + - python setup.py bdist_wheel + - TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/* + only: + - tags + +# build: +# stage: build +# script: +# - PYTHON_VERSION=${python_version} PYPI_URL=${pip_repository_index} make pull +# - PYTHON_VERSION=${python_version} PYPI_URL=${pip_repository_index} make build +# - PYTHON_VERSION=${python_version} PYPI_URL=${pip_repository_index} make push +# extends: +# - .docker_login +# - .run_on_all_branches +# - .parallel_python_jobs + +# Jobs: Compliance ------------------------------------------------------------ + +#compliance: +# stage: compliance +# script: +# - poetry run invoke format --check +# - poetry run invoke lint +# - poetry run invoke security-bandit +# extends: +# - .run_on_all_branches +# - .parallel_python_jobs +# - .use_generated_docker_image + +# Jobs: Test ------------------------------------------------------------------ + +# ============================= TEST ======================== +test: + script: + #- pip install tox flake8 + #- tox -e py39,flake8 + - pip install pytest + - pytest + extends: + - .run_on_all_branches + - .parallel_python_jobs + +# Jobs: Docs ----------------------------------------------------------------------- + +pages: + stage: build_and_publish_docs + script: + - pip install sphinx myst_parser python_docs_theme + - pip install . + #- cd docs ; make html + - sphinx-apidoc -e -P -o $DOCS_SOURCE_DIR $SOURCE_DIR + - sphinx-build -b html $DOCS_DIR $DOCS_BUILD_DIR + - mv $DOCS_BUILD_DIR public + artifacts: + paths: + - public + extends: + - .run_on_master + +# Jobs: Publish --------------------------------------------------------------- + + +release_pypi: + variables: + python_version: "3.9" + stage: publish_pypi + script: + - | + poetry run invoke release-twine \ + --tag_name="${tag_name}" \ + --pypi_user="${pypi_user}" \ + --pypi_pass="${pypi_pass}" \ + --pypi_publish_repository="${pypi_publish_repository}" \ + --pip_repository_index="${pip_repository_index}" + extends: + #- .run_on_release_tag + - .run_on_manual + #- .use_generated_docker_image diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..a053dc8 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,507 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + too-few-public-methods, + ungrouped-imports, + import-outside-toplevel, + fixme, + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=yes + +# Minimum lines number of a similarity. +min-similarity-lines=8 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _, + ui, + x, + y, + df, + ax, + pytestmark + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=6 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..40a5f5c --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,16 @@ + +# Acknowledgements and Credits + +The PyLabRobot SiLA Bridge project thanks + + +Contributors +------------ + +* Mickey Kim ! Thanks for the phantastic cookiecutter template ! + + +Development Lead +---------------- + +* mark doerr \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..8e9099e --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,130 @@ +Development +=========== + +Get Started! +------------ + +Ready to contribute? Here\'s how to set up [pylabrobot_sila_bridge]{.title-ref} for local development. + +1. Clone the [pylabrobot_sila_bridge]{.title-ref} repo from + GitLab: + + ``` {.shell} + $ git clone https://gitlab.com/opensourcelab/plr-sila-bridge.git + ``` + +3. Start your virtualenv and install dependencies: + + + # create a virtual environment and activate it then run to install development dependencies: + + pip install -e .[dev] + + # run unittests + + invoke test # use the invoke environment to manage development + + +4. Create a branch for local development: + + ``` {.shell} + $ git checkout -b feature/IssueNumber_name-of-your-bugfix-or-feature + + # please do not use the '#' in branch names ! + ``` + +Now you can make your changes locally. + +1. When you\'re done making changes, check that your changes pass the + tests, including testing other Python versions, with tox: + + ``` {.shell} + $ tox + ``` + +2. Commit your changes and push your branch to GitLab: + + ``` {.shell} + $ git add . + $ git commit -m "fix: Your detailed description of your changes." + $ git push origin feature/IssueNumber_name-of-your-bugfix-or-feature + ``` + +3. Submit a merge request through GitLab + +Merge Request Guidelines +------------------------ + +Before you submit a merge request, check that it meets these guidelines: + +1. The merge request should only include changes relating to one + ticket. +2. The merge request should include tests to cover any added changes + and check that all existing and new tests pass. +3. If the merge request adds functionality, the docs should be updated. + Put your new functionality into a function with a docstring, and add + the feature to the list in README.rst. +4. The team should be informed of any impactful changes. + +Documentation +------------- + +The Sphinx Documentation Sytem is used, + +markdown is supported via the mystparser ( + +) + +To build the documentation, run + +> \$ invoke docs + +Tips +---- + +1. To run a subset of tests: + + ``` {.shell} + $ pytest tests.test_pylabrobot_sila_bridge + ``` + +Deploying to Gitlab/Github/PyPI Package Registry +--------------------------------------------------- + +For every release: + +1. Update HISTORY.md + +2. Update version number (can also be patch or major): + + pre-commit hooks can be either installed with the provided script + or with the [pre-commit package](https://pre-commit.com) + + + ``` {.shell} + bumpversion --verbose patch + ``` + +3. Run the static analysis and tests: + + ``` {.shell} + tox + ``` + +4. Commit the changes: + + ``` {.shell} + git add HISTORY.md + git commit -m "Changelog for upcoming release <#.#.#>" + ``` + +5. Push the commit: + + ``` {.shell} + git push + ``` + +6. Add the release tag (version) on GitLab: +https://gitlab.com/opensourcelab/pylabrobot-sila-bridge/-/tags + +The GitLab CI pipeline will then deploy to PyPI if tests pass. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..948a55a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,54 @@ +ARG PYTHON_BASE + +FROM python:${PYTHON_BASE} as base +ARG PYPI_URL + +RUN mkdir /pylabrobot_sila_bridge +WORKDIR /pylabrobot_sila_bridge + +# copy build files +COPY pyproject.toml poetry.lock README.rst setup.cfg setup.py VERSION /pylabrobot_sila_bridge/ +COPY pylabrobot_sila_bridge /pylabrobot_sila_bridge/pylabrobot_sila_bridge + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + POETRY_NO_INTERACTION=1 \ + POETRY_VIRTUALENVS_CREATE=0 \ + PIP_INDEX_URL=$PYPI_URL + +#RUN sed -i 's|http://|https://artifactory.aws.gel.ac/artifactory/apt_|g' /etc/apt/sources.list + +# libcurl4-gnutls-dev is necessary for Pysam. See PCA-179 +RUN apt-get update -qq && apt-get install -qqy -f \ + build-essential \ + libbz2-dev \ + libffi-dev \ + liblzma-dev \ + libpq-dev \ + libsasl2-dev \ + libyaml-dev \ + libcurl4-gnutls-dev \ + nano \ + zlib1g-dev \ + && pip install -Iv --prefer-binary --index-url $PYPI_URL --upgrade \ + pip \ + setuptools \ + poetry==1.2.1 \ + poetry-plugin-export + +# Use poetry to resolve dependencies, output to requirements.txt and requirements_dev.txt, and pip to install +RUN poetry export --without dev --without-hashes -f requirements.txt -o requirements.txt \ + && poetry export --only dev --without-hashes -f requirements.txt -o requirements_dev.txt \ + && python -m pip install --prefer-binary --index-url $PYPI_URL -r requirements.txt \ + && python -m pip install --prefer-binary --index-url $PYPI_URL -e . + +FROM base as test +ARG PYPI_URL + +WORKDIR /pylabrobot_sila_bridge +COPY tests /pylabrobot_sila_bridge/tests + +# required to make sure pytest runs the right coverage checks +ENV PYTHONPATH . + +RUN python -m pip install --prefer-binary --index-url $PYPI_URL -r requirements_dev.txt .[tests] diff --git a/HISTORY.md b/HISTORY.md new file mode 100644 index 0000000..c6a06d1 --- /dev/null +++ b/HISTORY.md @@ -0,0 +1,8 @@ + +# History of pylabrobot_sila_bridge + + +* 0.0.1 (2024-08-14) + + +* First release .... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..04af712 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024, mark doerr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..fe47036 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +# add files to be included in package here +include VERSION +include *.txt \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dc72f31 --- /dev/null +++ b/Makefile @@ -0,0 +1,89 @@ +CODE_PATHS := pylabrobot_sila_bridge + +GITLAB_USERNAME ?= +GITLAB_TOKEN ?= + +PYTHON_VERSION ?= 3.9## default python build to 3.9 + +# Originally CI_PROJECT_NAME came from the CI, but it is safer if this comes directly from pyproject.toml +CI_PROJECT_NAME := $(shell grep -i name pyproject.toml | head -1 | awk -F'"' '{print $$2}' | tr "[A-Z]" "[a-z]" | tr "_" "-") +CI_COMMIT_REF_NAME ?= $(shell git symbolic-ref --short -q HEAD) +CI_COMMIT_REF_NAME := $(subst /,-,$(CI_COMMIT_REF_NAME)) +CI_COMMIT_BEFORE_SHA := $(shell git rev-parse HEAD^1) +CI_COMMIT_SHA ?= $(shell git rev-parse HEAD) +CI_COMMIT_SHORT_SHA ?= $(shell git rev-parse --short HEAD) +CURRENT_LOCATION ?= $(shell pwd) +CI_ENVIRONMENT_NAME ?= +CI_DOCKER_BUILD_ARGS ?= + +PYPI_URL ?= https://artifactory.aws.gel.ac/artifactory/api/pypi/pypi/simple + +CI_REGISTRY ?= registry.gitlab.com +CI_REGISTRY_IMAGE ?= ${CI_REGISTRY}/https://gitlab.com/opensourcelab/pylabrobot-sila-bridge +DOCKER_IMAGE_NAMESPACE := ${CI_REGISTRY_IMAGE}/${CI_PROJECT_NAME} +DOCKER_IMAGE_TAG ?= ${DOCKER_IMAGE_NAMESPACE}:py${PYTHON_VERSION}-${CI_COMMIT_REF_NAME} + +.PHONY: build test + +help: ## Prints this help/overview message + @awk 'BEGIN {FS = ":.*?## "} /^[a-z0-9_-]+:.*?## / {printf "\033[36m%-17s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST) + +_CMD_DOCKER_BUILD := \ + docker build \ + ${CI_DOCKER_BUILD_ARGS} \ + +## construct the docker image with the git branch inc. into the tag +.build_${PYTHON_VERSION}: Dockerfile pylabrobot_sila_bridge clean__py + ${_CMD_DOCKER_BUILD} \ + --platform linux/amd64 \ + --build-arg PYTHON_BASE=${PYTHON_VERSION} \ + --build-arg PYPI_URL=${PYPI_URL} \ + --cache-from ${DOCKER_IMAGE_TAG} \ + --tag ${DOCKER_IMAGE_TAG} \ + --target test \ + . + touch $@ + +build: .build_${PYTHON_VERSION} ## build local python3.9 image. Define PYTHON_VERSION to select other python version. + +push: build + docker push ${DOCKER_IMAGE_TAG} + +pull: + docker pull ${DOCKER_IMAGE_TAG} || echo "No pre-made image available" + +test: ## run pytest on mounted-in tests (change tests, no rebuild!) + mkdir -p $(shell pwd)/bin && \ + docker run \ + --platform linux/amd64 \ + --volume $(shell pwd)/bin/:/pylabrobot_sila_bridge/bin/ \ + --volume $(shell pwd):/pylabrobot_sila_bridge:rw \ + ${DOCKER_IMAGE_TAG} \ + pytest --cov-report xml:/pylabrobot_sila_bridge/bin/coverage.xml tests/ + +test_shell: ## enter test docker image Bash + mkdir -p $(shell pwd)/bin && \ + docker run -it \ + --platform linux/amd64 \ + --volume $(shell pwd)/bin/:/pylabrobot_sila_bridge/bin/ \ + --volume $(shell pwd):/pylabrobot_sila_bridge:rw \ + ${DOCKER_IMAGE_TAG} \ + /bin/bash + +# cleaning up -------------------------------------------------------- +clean: clean__docker clean__py clean__reports + +clean__py: ## clean python temp files + find . -iname *.pyc -delete + find . -iname __pycache__ -delete + find . -iname .cache -delete + +clean__docker: ## Clean up all docker images generated by this repo + rm -rf .*sentinel + for image in \ + $$(docker images --format "{\{.Repository}\}:{\{.Tag}\}\t{\{.ID}\}" | grep -e "${DOCKER_IMAGE_NAMESPACE}" | awk '{print $$2}'); do \ + docker rmi -f $$image; \ + done + +clean__reports: + rm -rf ${PATH_REPORTS} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7d1468 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# PyLabRobot SiLA Bridge + +PyLabRobot to SiLA bridge. + +## Features + +## Installation + + pip install pylabrobot_sila_bridge --index-url https://gitlab.com/api/v4/projects//packages/pypi/simple + +## Usage + + pylabrobot_sila_bridge --help + +## Development + + git clone gitlab.com/opensourcelab/plr-sila-bridge + + # create a virtual environment and activate it then run + + pip install -e .[dev] + + # run unittests + + invoke test # use the invoke environment to manage development + + +## Documentation + +The Documentation can be found here: [https://opensourcelab.gitlab.io/plr-sila-bridge](https://opensourcelab.gitlab.io/plr-sila-bridge) or [plr-sila-bridge.gitlab.io](pylabrobot_sila_bridge.gitlab.io/) + + +## Credits + +This package was created with [Cookiecutter](https://github.com/audreyr/cookiecutter) + and the [gitlab.com/opensourcelab/software-dev/cookiecutter-pypackage](https://gitlab.com/opensourcelab/software-dev/cookiecutter-pypackage) project template. + + + diff --git a/TODOs.md b/TODOs.md new file mode 100644 index 0000000..0cfd54f --- /dev/null +++ b/TODOs.md @@ -0,0 +1,3 @@ +# pylabrobot_sila_bridge TODOs + +* \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..8acdd82 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..185b6de --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,31 @@ +# docker-compose.yml for PyLabRobot SiLA Bridge +# to build the images: docker-compose build +# to build start everything: docker-compose up --build +# to build start everything after code change: docker-compose up --build + +version: '3.9' + +#networks: + #container_network + +services: + plr-sila-bridge: + build: + context: . + #image: registry.gitlab.com/ + #volumes: + # - : + #ports: + # - 8000:8000 + #expose: + # - 8000 + #networks: + # - container_network + env_file: + - ./.env.dev + command: + #sh -c "" + +#volumes: + + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..ea03c62 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,216 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sila_python.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sila_python.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/sila_python" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sila_python" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/_static/LARA_logo.svg b/docs/_static/LARA_logo.svg new file mode 100644 index 0000000..89acfad --- /dev/null +++ b/docs/_static/LARA_logo.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_static/custom.css-tpl b/docs/_static/custom.css-tpl new file mode 100644 index 0000000..83609f1 --- /dev/null +++ b/docs/_static/custom.css-tpl @@ -0,0 +1,28 @@ +dl { + margin: 1em 0; +} + +dl.class > dt, +dl.method > dt, +dl.exception > dt, +dl.function > dt { + padding: 6px; +} + +dl.class > dt, dl.exception > dt { + color: hsl(195, 60%, 52%); + background: hsl(195, 62%, 94%); + border-top: solid 3px hsl(195, 69%, 68%); + margin: 6px 0; +} + +dl.method > dt, dl.function > dt { + color: #555; + background: #f0f0f0; + border-left: solid 3px #ccc; + margin-bottom: 6px; +} + +code.sig-prename, code.sig-name { + font-weight: bold; +} diff --git a/docs/authors.rst b/docs/authors.rst new file mode 100644 index 0000000..2930f4a --- /dev/null +++ b/docs/authors.rst @@ -0,0 +1,4 @@ +Authors +======== + +.. include:: ../AUTHORS.md diff --git a/docs/conf.py b/docs/conf.py new file mode 100755 index 0000000..2afec95 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python +# +# pylabrobot_sila_bridge documentation build configuration file, created by +# sphinx-quickstart on Fri Jun 9 13:47:02 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another +# directory, add these directories to sys.path here. If the directory is +# relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + +import pylabrobot_sila_bridge + +# -- General configuration --------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# 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.viewcode', + 'sphinx.ext.napoleon', + 'sphinx.ext.todo', + 'myst_parser', + 'python_docs_theme', + #'sphinx.ext.mathjax', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] +#source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'PyLabRobot SiLA Bridge' +copyright = "2024, mark doerr" +author = "mark doerr" + +# The version info for the project you're documenting, acts as replacement +# for |version| and |release|, also used in various other places throughout +# the built documents. +# +# The short X.Y version. +version = pylabrobot_sila_bridge.__version__ +# The full version, including alpha/beta/rc tags. +release = pylabrobot_sila_bridge.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- 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 = 'alabaster' +html_theme = "python_docs_theme" + +# Theme options are theme-specific and customize the look and feel of a +# theme further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = { +# "logo": "LARA_logo.svg", +# "show_powered_by": False, +# "font_family": "sans-serif", +# "head_font_family": "Lato, sans-serif", +# "page_width": "1280px", +# "sidebar_width": "200px", +# "code_font_size": ".85em", +# "font_size": ".9em", +# "link": "hsl(195, 60%, 20%)", +# } + +# 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 = ['_static'] + +html_logo = "_static/LARA_logo.svg" + +# -- Options for HTMLHelp output --------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pylabrobot_sila_bridgedoc' + + +# -- Options for LaTeX output ------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', + + # Latex figure (float) alignment + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'pylabrobot_sila_bridge.tex', + 'PyLabRobot SiLA Bridge Documentation', + author, 'manual'), +] + + +# -- Options for manual page output ------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pylabrobot_sila_bridge', + 'PyLabRobot SiLA Bridge Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'pylabrobot_sila_bridge', + 'PyLabRobot SiLA Bridge Documentation', + author, + 'pylabrobot_sila_bridge', + 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/development.rst b/docs/development.rst new file mode 100644 index 0000000..339b948 --- /dev/null +++ b/docs/development.rst @@ -0,0 +1 @@ +.. include:: ../DEVELOPMENT.md diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 0000000..0473b9b --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,4 @@ +HISTORY +======== + +.. include:: ../HISTORY.md diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..1eee2d6 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,21 @@ +Welcome to PyLabRobot SiLA Bridge's documentation! +====================================================================== + +.. toctree:: + :glob: + :maxdepth: 2 + :caption: Contents: + + readme + installation + usage + source/modules + development + authors + history + +Indices and tables +================== +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..86d19c1 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,28 @@ +.. highlight:: shell + +============ +Installation +============ + + +Stable release +-------------- + +To install PyLabRobot SiLA Bridge, run this command in your terminal: + +.. code-block:: console + + $ pip install pylabrobot_sila_bridge + +This is the preferred method to install PyLabRobot SiLA Bridge, as it will always install the most recent stable release. + +If you don't have `pip`_ installed, this `Python installation guide`_ can guide +you through the process. + +.. _pip: https://pip.pypa.io +.. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ + + +From source +----------- + diff --git a/docs/readme.rst b/docs/readme.rst new file mode 100644 index 0000000..cbfbfbb --- /dev/null +++ b/docs/readme.rst @@ -0,0 +1,4 @@ +README +======= + +.. include:: ../README.md diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..3b72f62 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,7 @@ +===== +Usage +===== + +To use PyLabRobot SiLA Bridge in a project:: + + import pylabrobot_sila_bridge diff --git a/install-pre-commit-hook.py b/install-pre-commit-hook.py new file mode 100755 index 0000000..f4ca238 --- /dev/null +++ b/install-pre-commit-hook.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python + + +# shall be replaced by https://pre-commit.com/ + +import os +import stat +import sys +from os.path import dirname, join + +hook_file = join(dirname(__file__), ".git", "hooks", "pre-commit") + +# short-running tests +pytest_target_files = " ".join( + [ + "tests/test_*.py", + ] +) + +# write pre-commit hook script to .git/hooks/pre-commit +with open(hook_file, "w", encoding="utf-8") as fp: + fp.write( + f"""\ +#!/bin/sh +set -ex + +# ensure correct environment is used (workaround for PyCharm) +alias python={sys.executable} + +python -m isort --check-only . +python -m black --check . +python -m pflake8 . +python -m pytest {pytest_target_files} +""" + ) + +# make script executable +os.chmod(hook_file, os.stat(hook_file).st_mode | stat.S_IEXEC) diff --git a/jupyter/README.md b/jupyter/README.md new file mode 100644 index 0000000..a61c53b --- /dev/null +++ b/jupyter/README.md @@ -0,0 +1,2 @@ +# pylabrobot_sila_bridge jupyter Demo notebooks + diff --git a/pylabrobot_sila_bridge/__init__.py b/pylabrobot_sila_bridge/__init__.py new file mode 100644 index 0000000..5251307 --- /dev/null +++ b/pylabrobot_sila_bridge/__init__.py @@ -0,0 +1,5 @@ +"""Top-level package for PyLabRobot SiLA Bridge.""" + +__author__ = """mark doerr""" +__email__ = "mark.doerr@uni-greifswald.de" +__version__ = "0.0.1" diff --git a/pylabrobot_sila_bridge/__main__.py b/pylabrobot_sila_bridge/__main__.py new file mode 100644 index 0000000..abc827b --- /dev/null +++ b/pylabrobot_sila_bridge/__main__.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# vim:fileencoding=utf-8 +"""_____________________________________________________________________ + +:PROJECT: PyLabRobot SiLA Bridge + +* Main module command line interface * + +:details: Main module command line interface. + !!! Warning: it should have a diffent name than the package name. + +.. note:: - +.. todo:: - +________________________________________________________________________ +""" + +"""Main module implementation. !!! Warning: it should have a diffent name than the package name. """ +"""Console script for pylabrobot_sila_bridge.""" + +import argparse +import sys +import logging +from pylabrobot_sila_bridge import __version__ + +from pylabrobot_sila_bridge.pylabrobot_sila_bridge_impl import HelloWorld + +logging.basicConfig( + format="%(levelname)-4s| %(module)s.%(funcName)s: %(message)s", + level=logging.DEBUG, +) + +def parse_command_line(): + """ Looking for command line arguments""" + + description = "pylabrobot_sila_bridge" + parser = argparse.ArgumentParser(description=description) + + parser.add_argument("_", nargs="*") + + parser.add_argument( + "-n", "--name", action="store", default="yvain", help="name to greet" + ) + + parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + __version__) + + # add more arguments here + + return parser.parse_args() + +def main(): + """Console script for pylabrobot_sila_bridge.""" + # or use logging.INFO (=20) or logging.ERROR (=30) for less output + logging.basicConfig( + format='%(levelname)-4s| %(module)s.%(funcName)s: %(message)s', level=logging.DEBUG) + + + args = parse_command_line() + + if len(sys.argv) <= 2: + logging.debug("no arguments provided !") + + + print("Arguments: " + str(args._)) + print("Replace this message by putting your code into pylabrobot_sila_bridge.__main__") + + hw = HelloWorld() + greeting = hw.greet_the_world(args.name) + logging.debug(greeting) + + return 0 + + +if __name__ == "__main__": + + sys.exit(main()) # pragma: no cover diff --git a/pylabrobot_sila_bridge/pylabrobot_sila_bridge_impl.py b/pylabrobot_sila_bridge/pylabrobot_sila_bridge_impl.py new file mode 100644 index 0000000..6fc7c3a --- /dev/null +++ b/pylabrobot_sila_bridge/pylabrobot_sila_bridge_impl.py @@ -0,0 +1,32 @@ +"""_____________________________________________________________________ + +:PROJECT: PyLabRobot SiLA Bridge + +* Main module implementation * + +:details: Main module implementation. + +.. note:: - +.. todo:: - +________________________________________________________________________ +""" + + +import logging + +from pylabrobot_sila_bridge.pylabrobot_sila_bridge_interface import GreeterInterface + +class HelloWorld(GreeterInterface): + def __init__(self) -> None: + """Implementation of the GreeterInterface + """ + + def greet_the_world(self, name: str) -> str: + """greeting module - adds a name to a greeting + + :param name: person to greet + :type name: str + """ + logging.debug(f"Greeting: {name}") + return f"Hello world, {name} !" + diff --git a/pylabrobot_sila_bridge/pylabrobot_sila_bridge_interface.py b/pylabrobot_sila_bridge/pylabrobot_sila_bridge_interface.py new file mode 100644 index 0000000..3e0fc80 --- /dev/null +++ b/pylabrobot_sila_bridge/pylabrobot_sila_bridge_interface.py @@ -0,0 +1,42 @@ +"""_____________________________________________________________________ + +:PROJECT: PyLabRobot SiLA Bridge + +* Main module formal interface. * + +:details: In larger projects, formal interfaces are helping to define a trustable contract. + Currently there are two commonly used approaches: + [ABCMetadata](https://docs.python.org/3/library/abc.html) or [Python Protocols](https://peps.python.org/pep-0544/) + + see also: + ABC metaclass + - https://realpython.com/python-interface/ + - https://dev.to/meseta/factories-abstract-base-classes-and-python-s-new-protocols-structural-subtyping-20bm + +.. note:: - +.. todo:: - +________________________________________________________________________ +""" + + +# here is a +from abc import ABCMeta, abstractclassmethod + +class GreeterInterface(metaclass=ABCMeta): + """ Greeter formal Interface + TODO: test, if ABC baseclass is wor + """ + @classmethod + def __subclasshook__(cls, subclass): + return (hasattr(subclass, 'greet_the_world') and + callable(subclass.greet_the_world) or + NotImplemented) + + @abstractclassmethod + def greet_the_world(self, name: str) -> str: + """greeting module - adds a name to a greeting + + :param name: person to greet + :type name: str + """ + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..25d2b8e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,148 @@ +#--------------------------- setuptools ----------------------------- + +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "pylabrobot_sila_bridge" +version = "0.0.1" +readme = "README.md" +authors =[{name = "mark doerr", email = "mark.doerr@uni-greifswald.de"}] +description="PyLabRobot to SiLA bridge." +license = {text = "MIT"} +classifiers=[ + 'Development Status :: 2 - Pre-Alpha', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'Intended Audience :: Education', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Natural Language :: English', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Scientific/Engineering', + 'Topic :: Scientific/Engineering :: Information Analysis', + 'Topic :: Scientific/Engineering :: Visualization', + 'Topic :: Scientific/Engineering :: Bio-Informatics', + 'Topic :: Scientific/Engineering :: Chemistry' +] + +dependencies = [] + +# use MANIFEST.in to include non-python files ! +#packages = [ +# { include = "pylabrobot_sila_bridge" }, +#] + +# uncomment to enable commandline access of the module via its name from anywhere +#[project.scripts] +#pylabrobot_sila_bridge = "pylabrobot_sila_bridge.__main__:main" + +[project.optional-dependencies] +dev = [ + "pytest>=7.3", + "pytest-cov>=2.12", + "pytest-xdist>=2.0", + "coverage>=7.2", + "tox>=4.5", + "safety>=1.0", + "bandit>=1.0", + #"flake8>=3.0", + "pytest-cov", + "pyproject-flake8", + #"flake8-bugbear>=20.0", + "black>=20.0", + "isort>=5.0", + "mypy>=0.0", + "pylint>=2.0", + "invoke>=2.1", + "bumpversion>=0.6", + #"types-requests>=0.0", + #"pre-commit>=2.0", # https://pre-commit.com/ +] + +test = [ + "pytest>=7.3", + "pytest-cov>=2.12", + "pytest-xdist>=2.0", + "coverage>=7.2", + "tox>=4.5", + "safety>=1.0", + "bandit>=1.0", + "flake8>=3.0", + #"flake8-bugbear>=20.0", + "black>=20.0", + "isort>=5.0", + "mypy>=0.0", + "pylint>=2.0", + #"types-requests>=0.0", +] + +docs = [ + "sphinx>=7.0", + "python-docs-theme>=2023.3", + "myst-parser>=1.0", + #"types-requests>=0.0", +] + +[project.urls] +"Homepage" = "https://gitlab.com/opensourcelab/pylabrobot-sila-bridge" + +[tool.pytest.ini_options] +minversion = "6.0" + +addopts = [ + "-v", + #"-n=auto", +] +filterwarnings = [ + # https://github.com/pypa/pip/issues/11975 + "ignore:.*pkg_resources.*:DeprecationWarning", +] + +[tool.black] +line-length = 120 + +[tool.isort] +line_length = 120 +profile = "black" +skip = [ + "venv", +] + +[tool.flake8] +max-line-length = 120 +extend-ignore = "E203,E501,W293" +exclude = [ + "venv", +] + +[tool.bandit] +exclude_dirs = ["tests"] +skips = ["B101"] + +[tool.coverage.report] +exclude_lines = [ + "if __name__ == .__main__.:", + "@abstractmethod", + "@abc.abstractmethod", + "def __repr__(self):", + # re-enable the standard pragma + "pragma: no cover", + "raise NotImplementedError", + "if TYPE_CHECKING", +] +skip_empty = true +omit = [ + "setup.py", + # auto-generated by grpcio_tools.protoc + "*_pb2.py", + # will be copied to the server, so this source code is never executed +] +precision = 2 +# end setuptools + + + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..609c6b4 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[run] +omit = *experiment.py,*process_*.py,*old*.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d6c73aa --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +import os +import re + +from setuptools import setup, find_packages + + +REGEX_COMMENT = re.compile(r"[\s^]#(.*)") + +# allow setup.py to be run from any path +os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) + +dir_path = os.path.dirname(os.path.realpath(__file__)) + +with open(os.path.join(dir_path, "VERSION"), "r") as version_file: + version = str(version_file.readline()).strip() + + +def parse_requirements(filename): + filename = os.path.join(os.path.dirname(os.path.abspath(__file__)), filename) + with open(filename, "rt") as filehandle: + requirements = filehandle.readlines()[2:] + return tuple(filter(None, (REGEX_COMMENT.sub("", line).strip() for line in requirements))) + + +setup( + name="pylabrobot_sila_bridge", + version=version, + packages=find_packages(), + include_package_data=True, +) diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..ece077b --- /dev/null +++ b/tasks.py @@ -0,0 +1,482 @@ +""" +Tasks for maintaining the project. + +Execute 'invoke --list' for guidance on using Invoke +""" +import os +import sys +import platform +import shutil +import webbrowser +from pathlib import Path +from distutils.util import strtobool +import venv + +import pytest +from invoke import task, exceptions # type: ignore + +OS_PLATFORM = platform.system() +HOME_DIR = str(Path.home()) +ROOT_DIR = Path(__file__).parent +BIN_DIR = ROOT_DIR.joinpath("bin") +SETUP_FILE = ROOT_DIR.joinpath("setup.py") +TEST_DIR = ROOT_DIR.joinpath("tests") +SOURCE_DIR = ROOT_DIR.joinpath("pylabrobot_sila_bridge") +TOX_DIR = ROOT_DIR.joinpath(".tox") +JUNIT_XML_FILE = BIN_DIR.joinpath("report.xml") +COVERAGE_XML_FILE = BIN_DIR.joinpath("coverage.xml") +COVERAGE_HTML_DIR = BIN_DIR.joinpath("coverage_html") +COVERAGE_HTML_FILE = COVERAGE_HTML_DIR.joinpath("index.html") +DOCS_DIR = ROOT_DIR.joinpath("docs") +DOCS_SOURCE_DIR = DOCS_DIR.joinpath("source") +DOCS_BUILD_DIR = DOCS_DIR.joinpath("_build") +DOCS_INDEX = DOCS_BUILD_DIR.joinpath("index.html") +PYTHON_DIRS = [str(d) for d in [SOURCE_DIR, TEST_DIR]] +SAFETY_REQUIREMENTS_FILE = BIN_DIR.joinpath("safety_requirements.txt") +PYPI_URL = "https://pypi.python.org/api/pypi/pypi/simple" +PYTHON_VERSION = 3.9 +CI_PROJECT_NAME = "pylabrobot-sila-bridge" +CI_REGISTRY_IMAGE = "registry.gitlab.com/https://gitlab.com/opensourcelab/pylabrobot-sila-bridge" +DOCKERFILE = "Dockerfile" +DOCKER_BUILD_PLATFORM = "--platform linux/amd64" +VENV_MODULE_NAME = "venv" + + + +def _delete_file(file): + """ + If the file exists, delete it + + :param file: The file to delete + """ + try: + file.unlink(missing_ok=True) + except TypeError: + # missing_ok argument added in 3.8 + try: + file.unlink() + except FileNotFoundError: + pass + + +def _run(_c, command): + """ + It runs a command + + :param _c: The context object that is passed to invoke tasks + :param command: The command to run + """ + return _c.run(command, pty=platform.system() != 'Windows') + + +def _get_registry_path_str(python_version): + """ + It takes a build tag and a Python version, and returns a string that is the path to the image in the registry + + :param python_version: The version of Python to use + :return: The registry path for the image. + """ + ci_commit_ref_name = os.popen("git symbolic-ref --short -q HEAD").read().strip() + build_tag = ci_commit_ref_name if ci_commit_ref_name else "latest" + image_name = f"{CI_PROJECT_NAME}:py{python_version}-{build_tag}" + registry_path = f"{CI_REGISTRY_IMAGE}/{image_name}" + return registry_path + + +@task(help={'check': "Checks if source is formatted without applying changes"}) +def format(_c, check=False): + """ + It runs the `black` and `isort` tools on the Python code in the `PYTHON_DIRS` directories + + :param _c: The context object that is passed to invoke tasks + :param check: If True, the code will be checked for formatting, but not changed, defaults to False (optional) + """ + python_dirs_string = " ".join(PYTHON_DIRS) + # Run black + black_options = "--check" if check else "" + _run(_c, f"black {black_options} {python_dirs_string}") + # Run isort + isort_options = "--check-only --diff" if check else "" + _run(_c, f"isort {isort_options} {python_dirs_string}") + + +@task +def lint_flake8(_c): + """ + It runs the flake8 linter on all Python files in the project + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, f"flake8 {' '.join(PYTHON_DIRS)}") + + +@task +def lint_pylint(_c): + """ + It runs pylint on all Python files in the project + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, "pylint {}".format(" ".join(PYTHON_DIRS))) + + +@task +def lint_mypy(_c): + """ + It runs mypy on all Python files in the project + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, "mypy {}".format(" ".join(PYTHON_DIRS))) + + +@task(lint_flake8, lint_pylint, lint_mypy) +def lint(_): + """ + It runs all linting tools on all Python files in the project + """ + + +@task +def security_bandit(_c): + """ + It runs bandit security checks on the source directory + + :param _c: The command to run + """ + _run(_c, f"bandit -c pyproject.toml -r {SOURCE_DIR}") + + +@task +def security_safety(_c): + """ + It runs security checks on package dependencies + + :param _c: The context object that is passed to the task + """ + Path(BIN_DIR).mkdir(parents=True, exist_ok=True) + _run(_c, f"poetry export --dev --format=requirements.txt --without-hashes --output={SAFETY_REQUIREMENTS_FILE}") + _run(_c, f"safety check --file={SAFETY_REQUIREMENTS_FILE} --full-report") + + +@task(security_bandit, security_safety) +def security(_): + """ + It runs all security checks + """ + + +@task( + optional=["coverage"], + help={ + "coverage": 'Add coverage, ="html" for html output or ="xml" for xml output', + "junit": "Output a junit xml report", + }, +) +def test(_, coverage=None, junit=False): + """ + It runs the tests in the current directory + + :param _: The context object that is passed to invoke tasks + :param coverage: Generates coverage report, "html" for html output or "xml" for xml output (optional) + :param junit: If True, the test results will be written to a JUnit XML file, defaults to False (optional) + """ + pytest_args = ["-v"] + + if junit: + pytest_args.append(f"--junitxml={JUNIT_XML_FILE}") + + if coverage is not None: + pytest_args.append(f"--cov={SOURCE_DIR}") + + if coverage == "html": + pytest_args.append(f"--cov-report=html:{COVERAGE_HTML_DIR}") + elif coverage == "xml": + pytest_args.append(f"--cov-report=xml:{COVERAGE_XML_FILE}") + + pytest_args.append(str(TEST_DIR)) + return_code = pytest.main(pytest_args) + + if coverage == "html": + webbrowser.open(COVERAGE_HTML_FILE.as_uri()) + + if return_code: + raise exceptions.Exit("Tests failed", code=return_code) + + +@task +def clean_docs(_c): + """ + It takes a list of strings and returns a list of strings + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, f"rm -fr {DOCS_BUILD_DIR}") + _run(_c, f"rm -fr {DOCS_SOURCE_DIR}") + + +@task(pre=[clean_docs], help={"launch": "Launch documentation in the web browser"}) +def docs(_c, launch=True): + """ + It generates and opens the documentation for the project + + :param _c: The context object that is passed to invoke tasks + :param launch: If True, the docs will be opened in a browser. defaults to True (optional) + """ + # Generate autodoc stub files + _run(_c, f"sphinx-apidoc -e -P -o {DOCS_SOURCE_DIR} {SOURCE_DIR}") + # Generate docs + _run(_c, f"sphinx-build -b html {DOCS_DIR} {DOCS_BUILD_DIR}") + if launch: + webbrowser.open(DOCS_INDEX.as_uri()) + + +@task +def clean_build(_c): + """ + It cleans all the Python build and distribution artifacts + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, "rm -fr build/") + _run(_c, "rm -fr dist/") + _run(_c, "rm -fr .eggs/") + _run(_c, "find . -name '*.egg-info' -exec rm -fr {} +") + _run(_c, "find . -name '*.egg' -exec rm -f {} +") + + +@task +def clean_python(_c): + """ + It removes all the Python artifacts + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, "find . -name '*.pyc' -exec rm -f {} +") + _run(_c, "find . -name '*.pyo' -exec rm -f {} +") + _run(_c, "find . -name '*~' -exec rm -f {} +") + _run(_c, "find . -name '__pycache__' -exec rm -fr {} +") + + +@task +def clean_tests(_): + """ + It deletes all the test artifacts + + :param _: The context object that is passed to invoke tasks + """ + _delete_file(JUNIT_XML_FILE) + _delete_file(COVERAGE_XML_FILE) + shutil.rmtree(COVERAGE_HTML_DIR, ignore_errors=True) + shutil.rmtree(BIN_DIR, ignore_errors=True) + shutil.rmtree(TOX_DIR, ignore_errors=True) + + +@task(pre=[clean_build, clean_python, clean_tests, clean_docs]) +def clean(_): + """ + It runs all clean sub-tasks + + :param _: The context object that is passed to invoke tasks + """ + pass + + +@task( + pre=[clean_python], + optional=["python_version"], + help={ + "python_version": 'Python version to use, e.g. "3.9"', + }, +) +def docker_build(_c, python_version=PYTHON_VERSION, target="test"): + """ + It builds a Docker image with the given tag using the given Python version + + :param _c: The context object that is passed to invoke tasks + :param python_version: The base python version to use + :param target: The target to build ("test", "regression"), defaults to "test" (optional) + """ + build_args = f"--build-arg PYTHON_BASE={python_version} --build-arg PYPI_URL={PYPI_URL}" + registry_path = _get_registry_path_str(python_version) + cache = f"--cache-from {registry_path}" + target_tag = f"--target {target}" + _run( + _c, + f"docker build {build_args} {DOCKER_BUILD_PLATFORM} {cache} -f {DOCKERFILE} -t {registry_path} {target_tag} .", + ) + + +@task +def docker_pull(_c, python_version=PYTHON_VERSION): + """ + It pulls the image from the local registry, or if it doesn't exist, it prints a message + + :param _c: The context object that is passed to invoke tasks + :param python_version: The base python version to use + """ + registry_path = _get_registry_path_str(python_version) + _run(_c, f'docker pull {registry_path} || echo "No pre-made image available"') + + +@task +def docker_push(_c, python_version=PYTHON_VERSION): + """ + It pushes the image to the registry + + :param _c: The context object that is passed to invoke tasks + :param python_version: The base python version to use + """ + registry_path = _get_registry_path_str(python_version) + _run(_c, f"docker push {registry_path}") + + +@task +def docker_test(_c, python_version=PYTHON_VERSION): + """ + It runs the tests in a docker container + + :param _c: The context object that is passed to invoke tasks + :param python_version: The base python version to use + :param target: The target to test ("test", "regression"), defaults to "test" (optional) + """ + volume_mount = ( + f"--volume {BIN_DIR}:/pylabrobot_sila_bridge/bin/ --volume {ROOT_DIR}:/pylabrobot_sila_bridge:rw" + ) + registry_path = _get_registry_path_str(python_version) + pytest_arg = f"pytest -v --cov-report xml:/pylabrobot_sila_bridge/bin/coverage.xml {TEST_DIR}" + Path(BIN_DIR).mkdir(parents=True, exist_ok=True) + _run(_c, f"docker run {DOCKER_BUILD_PLATFORM} {volume_mount} {registry_path} {pytest_arg}") + + +@task +def docker_shell(_c, python_version=PYTHON_VERSION): + """ + It opens shell in the docker container + + :param _c: The context object that is passed to invoke tasks + :param python_version: The base python version to use + """ + volume_mount = ( + f"--volume {BIN_DIR}:/pylabrobot_sila_bridge/bin/ --volume {ROOT_DIR}:/pylabrobot_sila_bridge:rw" + ) + registry_path = _get_registry_path_str(python_version) + bash_path = "/bin/bash" + _run(_c, f"docker run -it {DOCKER_BUILD_PLATFORM} {volume_mount} {registry_path} {bash_path}") + + +@task +def init_repo(_c): + """Initialise the repository with git-LFS and git flow + + :param _c: The context object that is passed to invoke tasks + :type _c: context object + """ + # check, if it is already a git repo + # otherwise run git init + _run(_c, "git-lfs install") + _run(_c, "git flow init") + +@task(pre=[clean]) +def release_twine( + _c, + tag_name, + pypi_user, + pypi_pass, + pypi_publish_repository="https://artifactory.aws.gel.ac/artifactory/api/pypi/pypi_genomics_dev", + pip_repository_index="https://artifactory.aws.gel.ac/artifactory/api/pypi/pypi/simple", +): + """ + It makes a release of the Python package and publishes to the GEL PyPI Artifactory using setup.py and twine + + :param _c: The context object that is passed to invoke tasks + :param tag_name: The name of the tag that triggered the workflow + :param pypi_user: The username of the account that has access to the repository + :param pypi_pass: The password for the pypi user + :param pypi_publish_repository: The URL of the repository to publish to (optional) + :param pip_repository_index: The URL of the pip repository to use for installing twine (optional) + """ + version_str = tag_name.replace("v", "") + _run(_c, f'echo "Build tag - {version_str}."') + _run(_c, f"echo {version_str} > VERSION") + pypirc_str = ( + "[distutils]\n" + "index-servers = gel_pypi\n" + "\n" + "[gel_pypi]\n" + f"repository: {pypi_publish_repository}\n" + f"username: {pypi_user}\n" + f"password: {pypi_pass}\n)" + ) + _run(_c, f'printf "{pypirc_str}" > ~/.pypirc') + _run(_c, f"pip install -i {pip_repository_index} twine") + _run(_c, 'mkdir -p dist && rm -rf dist/* || echo "Nothing found in dist/"; python setup.py sdist;') + _run(_c, f'twine upload --repository-url {pypi_publish_repository} -u "{pypi_user}" -p "{pypi_pass}" dist/*') + + +@task +def generate_reqs(_c): + """ + It generates requirements.txt and requirements_dev.txt using poetry (dependencies from pyproject.toml). + + :param _c: The context object that is passed to invoke tasks + """ + _run(_c, f"poetry export --without dev --without-hashes -f requirements.txt -o {ROOT_DIR}/requirements.txt") + _run(_c, f"poetry export --only dev --without-hashes -f requirements.txt -o {ROOT_DIR}/requirements_dev.txt") +# --------------- installation helper functions, please do not modify ----------------------------- + +def query_yes_no(question, default_answer="yes", help=""): + """Ask user at stdin a yes or no question + + :param question: question text to user + :param default_answer: should be "yes" or "no" + :param help: help text string + :return: :type: bool + """ + if default_answer == "yes": + prompt_txt = "{question} [Y/n] ".format(question=question) + elif default_answer == "no": # explicit no + prompt_txt = "{question} [y/N] ".format(question=question) + else: + raise ValueError("default_answer must be 'yes' or 'no'!") + + while True: + try: + answer = input(prompt_txt) + if answer: + if answer == "?": + print(help) + continue + else: + return strtobool(answer) + else: + return strtobool(default_answer) + except ValueError: + sys.stderr.write("Please respond with 'yes' or 'no' " + "(or 'y' or 'n').\n") + except KeyboardInterrupt: + sys.stderr.write("Query interrupted by user, exiting now ...") + exit(0) + + +def query(question, default_answer="", help=""): + """Ask user a question + + :param question: question text to user + :param default_answer: any default answering text string + :param help: help text string + :return: stripped answer string + """ + prompt_txt = "{question} [{default_answer}] ".format(question=question, default_answer=default_answer) + + while True: + answer = input(prompt_txt).strip() + + if answer: + if answer == "?": + print(help) + continue + else: + return answer + else: + return default_answer \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e57e116 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Unit test package for pylabrobot_sila_bridge.""" diff --git a/tests/test_pylabrobot_sila_bridge.py b/tests/test_pylabrobot_sila_bridge.py new file mode 100644 index 0000000..98c3c2d --- /dev/null +++ b/tests/test_pylabrobot_sila_bridge.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +"""Tests for `pylabrobot_sila_bridge` package.""" +# pylint: disable=redefined-outer-name +from pylabrobot_sila_bridge import __version__ +from pylabrobot_sila_bridge.pylabrobot_sila_bridge_interface import GreeterInterface +from pylabrobot_sila_bridge.pylabrobot_sila_bridge_impl import HelloWorld + +def test_version(): + """Sample pytest test function.""" + assert __version__ == "0.0.1" + +def test_GreeterInterface(): + """ testing the formal interface (GreeterInterface) + """ + assert issubclass(HelloWorld, GreeterInterface) + +def test_HelloWorld(): + """ Testing HelloWorld class + """ + hw = HelloWorld() + name = 'yvain' + assert hw.greet_the_world(name) == f"Hello world, {name} !" +