Merge branch 'sangaboard-v4' into 'master'
Support latest sangaboard firmware See merge request openflexure/openflexure-microscope-server!154
This commit is contained in:
commit
ac45423739
38 changed files with 1572 additions and 967 deletions
111
.gitlab-ci.yml
111
.gitlab-ci.yml
|
|
@ -6,8 +6,12 @@ stages:
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
# Re-usable block to install (and cache) Poetry and openflexure-microscope-server
|
# Re-usable block to install (and cache) Poetry and openflexure-microscope-server
|
||||||
.python-install-template: &python-install
|
.python:
|
||||||
|
image: balenalib/raspberry-pi:buster
|
||||||
|
retry: 1
|
||||||
before_script:
|
before_script:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install apt-utils curl wget python3 python3-pip python3-venv raspi-gpio libgfortran3 libatlas3-base libffi-dev vim git-lfs libtiff5 libpango-1.0-0 libavcodec58 libgdk-pixbuf2.0-0 libjasper1 libswscale5 libpangocairo-1.0-0 libilmbase23 libatk1.0-0 libgtk-3-0 libwebp6 libcairo2 libavutil56 libcairo-gobject2 libopenexr23 libavformat58 libxslt1.1
|
||||||
- python3 -m venv .venv # NB this does not overwrite an existing cached venv
|
- python3 -m venv .venv # NB this does not overwrite an existing cached venv
|
||||||
- source .venv/bin/activate
|
- source .venv/bin/activate
|
||||||
- pip install pipenv # Should be cached after first run
|
- pip install pipenv # Should be cached after first run
|
||||||
|
|
@ -17,9 +21,14 @@ stages:
|
||||||
paths:
|
paths:
|
||||||
- .cache/pip
|
- .cache/pip
|
||||||
- .venv
|
- .venv
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
- tags
|
||||||
|
- web
|
||||||
|
|
||||||
# Re-usable block to install (and cache) openflexure-microscope-server static app
|
.javascript:
|
||||||
.node-install-template: &node-install
|
image: node:15
|
||||||
before_script:
|
before_script:
|
||||||
- cd webapp
|
- cd webapp
|
||||||
- npm install
|
- npm install
|
||||||
|
|
@ -27,93 +36,54 @@ stages:
|
||||||
key: "${CI_COMMIT_REF_SLUG}"
|
key: "${CI_COMMIT_REF_SLUG}"
|
||||||
paths:
|
paths:
|
||||||
- webapp/node_modules
|
- webapp/node_modules
|
||||||
|
only:
|
||||||
|
- branches
|
||||||
|
- merge_requests
|
||||||
|
- tags
|
||||||
|
- web
|
||||||
|
|
||||||
# Python static analysis with PyLint
|
# Python static analysis with PyLint
|
||||||
pylint:
|
pylint:
|
||||||
stage: analysis
|
stage: analysis
|
||||||
image: python:3.7
|
extends: .python
|
||||||
retry: 1
|
|
||||||
|
|
||||||
<<: *python-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- poe pylint
|
- poe pylint
|
||||||
|
|
||||||
only:
|
|
||||||
- branches
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Python type checking with Mypy
|
# Python type checking with Mypy
|
||||||
mypy:
|
mypy:
|
||||||
stage: analysis
|
stage: analysis
|
||||||
image: python:3.7
|
extends: .python
|
||||||
retry: 1
|
|
||||||
|
|
||||||
<<: *python-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- poe mypy
|
- poe mypy
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
reports:
|
reports:
|
||||||
cobertura: openflexure_microscope/cobertura.xml
|
coverage_report:
|
||||||
|
coverage_format: cobertura
|
||||||
only:
|
path: openflexure_microscope/cobertura.xml
|
||||||
- branches
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Python style analysis with Black
|
# Python style analysis with Black
|
||||||
black:
|
black:
|
||||||
stage: analysis
|
stage: analysis
|
||||||
image: python:3.7
|
|
||||||
retry: 1
|
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
|
extends: .python
|
||||||
<<: *python-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
# Run static build script
|
|
||||||
- poe black_check
|
- poe black_check
|
||||||
|
|
||||||
only:
|
|
||||||
- branches
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Python unit tests with PyTest
|
# Python unit tests with PyTest
|
||||||
pytest:
|
pytest:
|
||||||
stage: testing
|
stage: testing
|
||||||
image: python:3.7
|
extends: .python
|
||||||
retry: 1
|
|
||||||
|
|
||||||
<<: *python-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- poe test
|
- poe test
|
||||||
|
|
||||||
only:
|
|
||||||
- branches
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
when: always
|
when: always
|
||||||
reports:
|
reports:
|
||||||
junit: pytest_report.xml
|
junit: pytest_report.xml
|
||||||
|
|
||||||
|
# Generate and validate OpenAPI description
|
||||||
openapi:
|
openapi:
|
||||||
stage: testing
|
stage: testing
|
||||||
image: python:3.7
|
extends: .python
|
||||||
retry: 1
|
|
||||||
|
|
||||||
<<: *python-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- mkdir -p docs/build/
|
- mkdir -p docs/build/
|
||||||
- ofm-generate-openapi -o docs/build/swagger.yaml --validate
|
- ofm-generate-openapi -o docs/build/swagger.yaml --validate
|
||||||
|
|
@ -123,53 +93,28 @@ openapi:
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
paths:
|
paths:
|
||||||
- "./docs/build/swagger.yaml"
|
- "./docs/build/swagger.yaml"
|
||||||
|
|
||||||
only:
|
|
||||||
- branches
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
# JavaScript linting with ESLint (via Vue CLI)
|
# JavaScript linting with ESLint (via Vue CLI)
|
||||||
eslint:
|
eslint:
|
||||||
stage: analysis
|
stage: analysis
|
||||||
image: node:15
|
extends: .javascript
|
||||||
|
|
||||||
<<: *node-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
# Build JS application for production
|
# Build JS application for production
|
||||||
- npm run lint
|
- npm run lint
|
||||||
|
|
||||||
only:
|
|
||||||
- branches
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Build JS app
|
# Build JS app
|
||||||
build:
|
build:
|
||||||
stage: build
|
stage: build
|
||||||
image: node:15
|
extends: .javascript
|
||||||
|
|
||||||
<<: *node-install
|
|
||||||
|
|
||||||
script:
|
script:
|
||||||
# Build JS application for production
|
# Build JS application for production
|
||||||
- npm run build
|
- npm run build
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
name: "dist"
|
name: "dist"
|
||||||
expire_in: 1 week
|
expire_in: 1 week
|
||||||
paths:
|
paths:
|
||||||
- "openflexure_microscope/api/static/dist/"
|
- "openflexure_microscope/api/static/dist/"
|
||||||
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
- merge_requests
|
|
||||||
- tags
|
|
||||||
- web
|
|
||||||
|
|
||||||
# Package application into distribution tarball
|
# Package application into distribution tarball
|
||||||
package:
|
package:
|
||||||
stage: package
|
stage: package
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
# [v2.11.0](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.10.1...v2.11.0) (2022-08-08)
|
||||||
|
## New features
|
||||||
|
* Background detection can now be used in scans, if you have the background-detect extension. ([!153](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/153))
|
||||||
|
* Support for Sangaboard firmware v1, via updated `sangaboard` dependency ([!154](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/154))
|
||||||
|
* Fixed links in the OpenAPI documentation describing actions etc. ([!155](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/154))
|
||||||
|
|
||||||
# [v2.10.1](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.10.0...v2.10.1) (2022-01-17)
|
# [v2.10.1](https://gitlab.com/openflexure/openflexure-microscope-server/compare/v2.10.0...v2.10.1) (2022-01-17)
|
||||||
## Bug fixes
|
## Bug fixes
|
||||||
* Fixed a typo in the serialisation of numpy arrays (#243) ([!148](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/148))
|
* Fixed a typo in the serialisation of numpy arrays (#243) ([!148](https://gitlab.com/openflexure/openflexure-microscope-server/-/merge_requests/148))
|
||||||
|
|
|
||||||
5
Pipfile
5
Pipfile
|
|
@ -1,8 +1,3 @@
|
||||||
[[source]]
|
|
||||||
url = "https://pypi.python.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
name = "pypi"
|
|
||||||
|
|
||||||
[[source]]
|
[[source]]
|
||||||
url = "https://www.piwheels.org/simple"
|
url = "https://www.piwheels.org/simple"
|
||||||
verify_ssl = true
|
verify_ssl = true
|
||||||
|
|
|
||||||
2007
Pipfile.lock
generated
2007
Pipfile.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -69,7 +69,7 @@ To set up a development version of the software (most likely using emulated came
|
||||||
* Create a virtual environment and activate it:
|
* Create a virtual environment and activate it:
|
||||||
* `python -m venv .venv`
|
* `python -m venv .venv`
|
||||||
* `source .venv/bin/activate` (on Linux) or `.venv/Scripts/activate` (on Windows)
|
* `source .venv/bin/activate` (on Linux) or `.venv/Scripts/activate` (on Windows)
|
||||||
* `pip install pipenv`
|
* `pip install --upgrade pip wheel pipenv`
|
||||||
* `pipenv install --dev` (This will install development dependencies. If you don't need these, `pipenv install` will get you just the dependencies needed to run the server, which takes about half the time.)
|
* `pipenv install --dev` (This will install development dependencies. If you don't need these, `pipenv install` will get you just the dependencies needed to run the server, which takes about half the time.)
|
||||||
* Finally, run the server:
|
* Finally, run the server:
|
||||||
* You can use `ofm serve` or `ofm restart` on the Raspberry Pi to manage the server.
|
* You can use `ofm serve` or `ofm restart` on the Raspberry Pi to manage the server.
|
||||||
|
|
@ -146,6 +146,10 @@ As of `2.10.0b0` we have switched to using `pipenv` for managing dependencies, a
|
||||||
* `pyproject.toml` is retained, but *no longer includes package metadata or dependencies* and does not have a `[tool.poetry]` table. It does define the build system as per PEP517, which is `setuptools`, and it also contains settings for `black`, `poe` and `isort`.
|
* `pyproject.toml` is retained, but *no longer includes package metadata or dependencies* and does not have a `[tool.poetry]` table. It does define the build system as per PEP517, which is `setuptools`, and it also contains settings for `black`, `poe` and `isort`.
|
||||||
* `Pipfile` is very minimal, and only declares dependencies on the current module, i.e. the dependencies declared in `setup.py`. It also specifies the Python version. This allows us to single-source dependency information from `setup.py` but use the dependency resolution/locking functionality of `pipenv`
|
* `Pipfile` is very minimal, and only declares dependencies on the current module, i.e. the dependencies declared in `setup.py`. It also specifies the Python version. This allows us to single-source dependency information from `setup.py` but use the dependency resolution/locking functionality of `pipenv`
|
||||||
* `Pipfile.lock` locks the dependency versions in the same way as `poetry.lock` used to, i.e. it specifies exact versions of everything, derived from the looser specifications in `setup.py`.
|
* `Pipfile.lock` locks the dependency versions in the same way as `poetry.lock` used to, i.e. it specifies exact versions of everything, derived from the looser specifications in `setup.py`.
|
||||||
|
* Dependency resolution is a pain, in part because compiling packages like scipy and numpy from source on a Pi is slow and unreliable. To avoid this, we usually lock the dependencies on a Raspberry Pi, using:
|
||||||
|
`PIP_ONLY_BINARY="numpy, scipy, matplotlib" pipenv lock --dev`
|
||||||
|
This forces the use of wheels only, and thus restricts us to things that built OK on Piwheels. The resulting Pipfile.lock should then work nicely on other Raspberry Pis.
|
||||||
|
* **The Pipfile we use is now Raspberry Pi-specific** so if you attempt to use it on other platforms it will probably fail. This is because we've removed `pypi` to avoid hash conflicts. The CI now runs on a Raspberry Pi image, so that the tests use a similar environment to our deployment. Currently, changing the `[source]` entry back to PyPi will allow you to re-lock the dependencies on other platforms. In the future we may need to maintain two Pipfiles and two Pipfile.lock files.
|
||||||
|
|
||||||
## Creating releases
|
## Creating releases
|
||||||
|
|
||||||
|
|
@ -166,3 +170,6 @@ As of `2.10.0b0` we have switched to using `pipenv` for managing dependencies, a
|
||||||
|
|
||||||
The Microscope module, and Flask app, both support plugins for extending lower-level functionality not well suited to web API calls. The current documentation can be found [here](https://openflexure-microscope-software.readthedocs.io/en/latest/plugins.html).
|
The Microscope module, and Flask app, both support plugins for extending lower-level functionality not well suited to web API calls. The current documentation can be found [here](https://openflexure-microscope-software.readthedocs.io/en/latest/plugins.html).
|
||||||
If you want to add functions to the microscope software, this is probably the best mechanism to use if it works for you.
|
If you want to add functions to the microscope software, this is probably the best mechanism to use if it works for you.
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ Like properties, we use a special view class to identify a view as an action: ``
|
||||||
Take an image capture and return it without saving
|
Take an image capture and return it without saving
|
||||||
"""
|
"""
|
||||||
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
||||||
args = {"use_video_port": fields.Boolean(missing=True)}
|
args = {"use_video_port": fields.Boolean(load_default=True)}
|
||||||
|
|
||||||
# Our success response (200) returns an image (image/jpeg mimetype)
|
# Our success response (200) returns an image (image/jpeg mimetype)
|
||||||
responses = {
|
responses = {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class ExampleIdentifyView(View):
|
||||||
class ExampleRenameView(View):
|
class ExampleRenameView(View):
|
||||||
# Expect a request parameter called "name", which is a string.
|
# Expect a request parameter called "name", which is a string.
|
||||||
# Passed to the argument "args".
|
# Passed to the argument "args".
|
||||||
args = fields.String(required=True, example="My Example Microscope")
|
args = fields.String(required=True, metadata={"example": "My Example Microscope"})
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Look for our new name in the request body
|
# Look for our new name in the request body
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,11 @@ class ExampleRenameView(View):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
schema = MicroscopeIdentifySchema()
|
schema = MicroscopeIdentifySchema()
|
||||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||||
args = {"name": fields.String(required=True, example="My Example Microscope")}
|
args = {
|
||||||
|
"name": fields.String(
|
||||||
|
required=True, metadata={"example": "My Example Microscope"}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Look for our "name" parameter in the request arguments
|
# Look for our "name" parameter in the request arguments
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,11 @@ class ExampleIdentifyView(PropertyView):
|
||||||
# We can use a single schema as the input and output will be formatted identically
|
# We can use a single schema as the input and output will be formatted identically
|
||||||
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||||
class ExampleRenameView(PropertyView):
|
class ExampleRenameView(PropertyView):
|
||||||
schema = {"name": fields.String(required=True, example="My Example Microscope")}
|
schema = {
|
||||||
|
"name": fields.String(
|
||||||
|
required=True, metadata={"example": "My Example Microscope"}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,11 @@ class ExampleIdentifyView(PropertyView):
|
||||||
# We can use a single schema as the input and output will be formatted identically
|
# We can use a single schema as the input and output will be formatted identically
|
||||||
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||||
class ExampleRenameView(PropertyView):
|
class ExampleRenameView(PropertyView):
|
||||||
schema = {"name": fields.String(required=True, example="My Example Microscope")}
|
schema = {
|
||||||
|
"name": fields.String(
|
||||||
|
required=True, metadata={"example": "My Example Microscope"}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -91,7 +95,7 @@ class QuickCaptureAPI(ActionView):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
||||||
args = {"use_video_port": fields.Boolean(missing=True)}
|
args = {"use_video_port": fields.Boolean(load_default=True)}
|
||||||
|
|
||||||
# Our success response (200) returns an image (image/jpeg mimetype)
|
# Our success response (200) returns an image (image/jpeg mimetype)
|
||||||
responses = {200: {"content": {"image/jpeg": {}}}}
|
responses = {200: {"content": {"image/jpeg": {}}}}
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,11 @@ class TimelapseAPIView(ActionView):
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"n_images": fields.Integer(
|
"n_images": fields.Integer(
|
||||||
required=True, example=5, description="Number of images"
|
required=True, metadata={"example": 5, "description": "Number of images"}
|
||||||
),
|
),
|
||||||
"t_between": fields.Number(
|
"t_between": fields.Number(
|
||||||
missing=1, example=1, description="Time (seconds) between images"
|
load_default=1,
|
||||||
|
metadata={"example": 1, "description": "Time (seconds) between images"},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,11 @@ class TimelapseAPIView(ActionView):
|
||||||
|
|
||||||
args = {
|
args = {
|
||||||
"n_images": fields.Integer(
|
"n_images": fields.Integer(
|
||||||
required=True, example=5, description="Number of images"
|
required=True, metadata={"example": 5, "description": "Number of images"}
|
||||||
),
|
),
|
||||||
"t_between": fields.Number(
|
"t_between": fields.Number(
|
||||||
missing=1, example=1, description="Time (seconds) between images"
|
load_default=1,
|
||||||
|
metadata={"example": 1, "description": "Time (seconds) between images"},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ A **field** describes the data type of a single parameter, as well as any other
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
fields.String(required=False, missing="Default value", example="Example value")
|
fields.String(required=False, load_default="Default value", metadata={"example: "Example value"})
|
||||||
|
|
||||||
A **schema** is a collection of keys and fields describing how an object should be serialized/deserialized. Schemas can be created in several ways, either by creating a ``Schema`` class, or by passing a dictionary of key-field pairs. Both methods will be discussed in the following examples.
|
A **schema** is a collection of keys and fields describing how an object should be serialized/deserialized. Schemas can be created in several ways, either by creating a ``Schema`` class, or by passing a dictionary of key-field pairs. Both methods will be discussed in the following examples.
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ For example, if you are creating an API route, in which you expect parameters ``
|
||||||
class UserSchema(Schema):
|
class UserSchema(Schema):
|
||||||
name = fields.String(required=True)
|
name = fields.String(required=True)
|
||||||
age = fields.Integer(required=True)
|
age = fields.Integer(required=True)
|
||||||
job = fields.String(required=False, missing="Unknown")
|
job = fields.String(required=False, load_default="Unknown")
|
||||||
|
|
||||||
To inform your POST method to expect these arguments, use the ``args`` class attribute:
|
To inform your POST method to expect these arguments, use the ``args`` class attribute:
|
||||||
|
|
||||||
|
|
@ -63,7 +63,7 @@ Alternatively, if your schema is only used in a single location, it may be simpl
|
||||||
args = {
|
args = {
|
||||||
"name": fields.String(required=True),
|
"name": fields.String(required=True),
|
||||||
"age": fields.Integer(required=True),
|
"age": fields.Integer(required=True),
|
||||||
"job": fields.String(required=False, missing="Unknown")
|
"job": fields.String(required=False, load_default="Unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
|
|
@ -88,7 +88,7 @@ This JSON data is the parsed, converted into a Python dictionary, and passed as
|
||||||
args = {
|
args = {
|
||||||
"name": fields.String(required=True),
|
"name": fields.String(required=True),
|
||||||
"age": fields.Integer(required=True),
|
"age": fields.Integer(required=True),
|
||||||
"job": fields.String(required=False, missing="Unknown")
|
"job": fields.String(required=False, load_default="Unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
|
|
@ -146,7 +146,7 @@ Our ``rename`` view class may now look like:
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
schema = MicroscopeIdentifySchema()
|
schema = MicroscopeIdentifySchema()
|
||||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||||
args = {"name": fields.String(required=True, example="My Example Microscope")}
|
args = {"name": fields.String(required=True, metadata={"example": "My Example Microscope"})}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Look for our "name" parameter in the request arguments
|
# Look for our "name" parameter in the request arguments
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ We will implement the ``schema`` attribute in our ``ExampleRenameView`` view fro
|
||||||
# We can use a single schema as the input and output will be formatted identically
|
# We can use a single schema as the input and output will be formatted identically
|
||||||
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||||
class ExampleRenameView(PropertyView):
|
class ExampleRenameView(PropertyView):
|
||||||
schema = {"name": fields.String(required=True, example="My Example Microscope")}
|
schema = {"name": fields.String(required=True, metadata={"example": "My Example Microscope"})}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -261,10 +261,10 @@ def generate_openapi():
|
||||||
if fname.endswith(".json"):
|
if fname.endswith(".json"):
|
||||||
import json
|
import json
|
||||||
|
|
||||||
with open(fname, "w") as fd:
|
with open(fname, "w", encoding="utf-8") as fd:
|
||||||
json.dump(labthing.spec.to_dict(), fd)
|
json.dump(labthing.spec.to_dict(), fd)
|
||||||
else:
|
else:
|
||||||
with open(fname, "w") as fd:
|
with open(fname, "w", encoding="utf-8") as fd:
|
||||||
fd.write(labthing.spec.to_yaml())
|
fd.write(labthing.spec.to_yaml())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,9 @@ def extension_action(args=None):
|
||||||
# Run the action
|
# Run the action
|
||||||
return func(self.extension, **arguments)
|
return func(self.extension, **arguments)
|
||||||
|
|
||||||
def get(self, *args, **kwargs): # pylint: disable=useless-super-delegation
|
def get(
|
||||||
|
self, *args, **kwargs
|
||||||
|
): # pylint: disable=useless-super-delegation,arguments-differ
|
||||||
# Explicitly wrap the `get` method to allow us to add a docstring
|
# Explicitly wrap the `get` method to allow us to add a docstring
|
||||||
return super().get(*args, **kwargs)
|
return super().get(*args, **kwargs)
|
||||||
|
|
||||||
|
|
@ -347,8 +349,10 @@ class AutofocusExtension(BaseExtension):
|
||||||
args={
|
args={
|
||||||
"dz": fields.List(
|
"dz": fields.List(
|
||||||
fields.Int(),
|
fields.Int(),
|
||||||
description="An ascending list of relative z positions",
|
metadata={
|
||||||
example=[int(x) for x in np.linspace(-300, 300, 7)],
|
"description": "An ascending list of relative z positions",
|
||||||
|
"example": [int(x) for x in np.linspace(-300, 300, 7)],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -408,7 +412,9 @@ class AutofocusExtension(BaseExtension):
|
||||||
|
|
||||||
@extension_action(
|
@extension_action(
|
||||||
args={
|
args={
|
||||||
"dz": fields.Int(required=True, description="The relative Z move to make")
|
"dz": fields.Int(
|
||||||
|
required=True, metadata={"description": "The relative Z move to make"}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
def move_and_measure(
|
def move_and_measure(
|
||||||
|
|
@ -430,9 +436,11 @@ class AutofocusExtension(BaseExtension):
|
||||||
@extension_action(
|
@extension_action(
|
||||||
args={
|
args={
|
||||||
"dz": fields.Int(
|
"dz": fields.Int(
|
||||||
missing=2000,
|
load_default=2000,
|
||||||
example=2000,
|
metadata={
|
||||||
description="Total Z range to search over (in stage steps)",
|
"description": "Total Z range to search over (in stage steps)",
|
||||||
|
"example": 2000,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -526,14 +534,18 @@ class AutofocusExtension(BaseExtension):
|
||||||
@extension_action(
|
@extension_action(
|
||||||
args={
|
args={
|
||||||
"dz": fields.Int(
|
"dz": fields.Int(
|
||||||
missing=500,
|
load_default=500,
|
||||||
example=500,
|
metadata={
|
||||||
description="Total Z range to move down, then up (in stage steps)",
|
"description": "Total Z range to move down, then up (in stage steps)",
|
||||||
|
"example": 500,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"delay": fields.Int(
|
"delay": fields.Int(
|
||||||
missing=5,
|
load_default=5,
|
||||||
example=5,
|
metadata={
|
||||||
description="How long to measure sharpness for after the move",
|
"description": "How long to measure sharpness for after the move, in seconds",
|
||||||
|
"example": 5,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -554,23 +566,31 @@ class AutofocusExtension(BaseExtension):
|
||||||
@extension_action(
|
@extension_action(
|
||||||
args={
|
args={
|
||||||
"dz": fields.Int(
|
"dz": fields.Int(
|
||||||
missing=2000,
|
load_default=2000,
|
||||||
example=2000,
|
metadata={
|
||||||
description="Total Z range to search over (in stage steps)",
|
"description": "Total Z range to search over (in stage steps)",
|
||||||
|
"example": 2000,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"target_z": fields.Int(
|
"target_z": fields.Int(
|
||||||
missing=0,
|
load_default=0,
|
||||||
example=-100,
|
metadata={
|
||||||
description="Target finishing position, relative to the focus.",
|
"description": "Target finishing position, relative to the focus.",
|
||||||
|
"example": -100,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"initial_move_up": fields.Bool(
|
"initial_move_up": fields.Bool(
|
||||||
missing=True,
|
load_default=True,
|
||||||
description="Set to Flase to disable the initial move upwards",
|
metadata={
|
||||||
|
"description": "Set to False to disable the initial move upwards"
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"backlash": fields.Int(
|
"backlash": fields.Int(
|
||||||
missing=25,
|
load_default=25,
|
||||||
minimum=0,
|
metadata={
|
||||||
description="Distance to undershoot, before correction move.",
|
"description": "Distance to undershoot, before correction move.",
|
||||||
|
"minimum": 0,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ class GetLocationsView(PropertyView):
|
||||||
|
|
||||||
|
|
||||||
class PreferredLocationView(PropertyView):
|
class PreferredLocationView(PropertyView):
|
||||||
schema = fields.String(required=True, example="Default")
|
schema = fields.String(required=True, metadata={"example": "Default"})
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
self.extension.check_location()
|
self.extension.check_location()
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class CSMExtension(BaseExtension):
|
||||||
"linear_calibration_y": cal_y,
|
"linear_calibration_y": cal_y,
|
||||||
}
|
}
|
||||||
|
|
||||||
with open(CSM_DATAFILE_PATH, "w") as f:
|
with open(CSM_DATAFILE_PATH, "w", encoding="utf-8") as f:
|
||||||
json.dump(data, f, cls=JSONEncoder)
|
json.dump(data, f, cls=JSONEncoder)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
@ -279,7 +279,11 @@ class CSMExtension(BaseExtension):
|
||||||
|
|
||||||
|
|
||||||
class Calibrate1DView(ActionView):
|
class Calibrate1DView(ActionView):
|
||||||
args = {"direction": fields.List(fields.Float(), required=True, example=[1, 0, 0])}
|
args = {
|
||||||
|
"direction": fields.List(
|
||||||
|
fields.Float(), required=True, metadata={"example": [1, 0, 0]}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""Calibrate one axis of the microscope stage against the camera."""
|
"""Calibrate one axis of the microscope stage against the camera."""
|
||||||
|
|
@ -298,10 +302,18 @@ class CalibrateXYView(ActionView):
|
||||||
class MoveInImageCoordinatesView(ActionView):
|
class MoveInImageCoordinatesView(ActionView):
|
||||||
args = {
|
args = {
|
||||||
"x": fields.Float(
|
"x": fields.Float(
|
||||||
description="The number of pixels to move in X", required=True, example=100
|
metadata={
|
||||||
|
"description": "The number of pixels to move in X",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
),
|
),
|
||||||
"y": fields.Float(
|
"y": fields.Float(
|
||||||
description="The number of pixels to move in Y", required=True, example=100
|
metadata={
|
||||||
|
"description": "The number of pixels to move in Y",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -316,10 +328,18 @@ class MoveInImageCoordinatesView(ActionView):
|
||||||
class ClosedLoopMoveInImageCoordinatesView(ActionView):
|
class ClosedLoopMoveInImageCoordinatesView(ActionView):
|
||||||
args = {
|
args = {
|
||||||
"x": fields.Float(
|
"x": fields.Float(
|
||||||
description="The number of pixels to move in X", required=True, example=100
|
metadata={
|
||||||
|
"description": "The number of pixels to move in X",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
),
|
),
|
||||||
"y": fields.Float(
|
"y": fields.Float(
|
||||||
description="The number of pixels to move in Y", required=True, example=100
|
metadata={
|
||||||
|
"description": "The number of pixels to move in Y",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -336,15 +356,25 @@ class ClosedLoopMoveInImageCoordinatesView(ActionView):
|
||||||
class TestClosedLoopSpiralScanView(ActionView):
|
class TestClosedLoopSpiralScanView(ActionView):
|
||||||
args = {
|
args = {
|
||||||
"x_step": fields.Float(
|
"x_step": fields.Float(
|
||||||
description="The number of pixels to move in X", required=True, example=100
|
metadata={
|
||||||
|
"description": "The number of pixels to move in X",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
),
|
),
|
||||||
"y_step": fields.Float(
|
"y_step": fields.Float(
|
||||||
description="The number of pixels to move in Y", required=True, example=100
|
metadata={
|
||||||
|
"description": "The number of pixels to move in Y",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
|
required=True,
|
||||||
),
|
),
|
||||||
"N": fields.Int(
|
"N": fields.Int(
|
||||||
description="The number of rings in the spiral scan",
|
metadata={
|
||||||
|
"description": "The number of rings in the spiral scan",
|
||||||
|
"example": 100,
|
||||||
|
},
|
||||||
required=True,
|
required=True,
|
||||||
example=3,
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -169,39 +169,50 @@ class DeleteLSTView(ActionView):
|
||||||
microscope.save_settings()
|
microscope.save_settings()
|
||||||
|
|
||||||
|
|
||||||
|
percentile_field = fields.Float(
|
||||||
|
load_default=99.9,
|
||||||
|
metadata={
|
||||||
|
"example": 99.9,
|
||||||
|
"description": (
|
||||||
|
"A float between 0 and 100 setting the centile to use "
|
||||||
|
"to measure the white point of the image. A value "
|
||||||
|
"of 99.9 allows 0.1% of the pixels to be erroneously "
|
||||||
|
"bright - this helps stability in low light."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AutoExposureFromRawView(ActionView):
|
class AutoExposureFromRawView(ActionView):
|
||||||
args = {
|
args = {
|
||||||
"target_white_level": fields.Int(
|
"target_white_level": fields.Int(
|
||||||
missing=700,
|
load_default=700,
|
||||||
example=700,
|
metadata={
|
||||||
description=(
|
"example": 700,
|
||||||
"The pixel value (10-bit format) that we aim for when adjusting shutter/gain."
|
"description": (
|
||||||
),
|
"The pixel value (10-bit format) that we aim for when adjusting shutter/gain."
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"max_iterations": fields.Int(
|
"max_iterations": fields.Int(
|
||||||
missing=20,
|
load_default=20,
|
||||||
description=(
|
metadata={
|
||||||
"The number of adjustments to the camera's settings to make before giving up."
|
"description": (
|
||||||
),
|
"The number of adjustments to the camera's settings to make before giving up."
|
||||||
|
)
|
||||||
|
},
|
||||||
),
|
),
|
||||||
"tolerance": fields.Float(
|
"tolerance": fields.Float(
|
||||||
missing=0.05,
|
load_default=0.05,
|
||||||
example=0.05,
|
metadata={
|
||||||
description=(
|
"example": 0.05,
|
||||||
"We stop adjusting when we get within this fraction of the target "
|
"description": (
|
||||||
"value. It is a number between 0 and 1, usually 0.01--0.1."
|
"We stop adjusting when we get within this fraction of the target "
|
||||||
),
|
"value. It is a number between 0 and 1, usually 0.01--0.1."
|
||||||
),
|
),
|
||||||
"percentile": fields.Float(
|
},
|
||||||
missing=99.9,
|
|
||||||
example=99.9,
|
|
||||||
description=(
|
|
||||||
"A float between 0 and 100 setting the centile to use "
|
|
||||||
"to measure the white point of the image. A value "
|
|
||||||
"of 99.9 allows 0.1% of the pixels to be erroneously "
|
|
||||||
"bright - this helps stability in low light."
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
"percentile": percentile_field,
|
||||||
}
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
|
|
@ -210,18 +221,7 @@ class AutoExposureFromRawView(ActionView):
|
||||||
|
|
||||||
|
|
||||||
class AutoWhiteBalanceFromRawView(ActionView):
|
class AutoWhiteBalanceFromRawView(ActionView):
|
||||||
args = {
|
args = {"percentile": percentile_field}
|
||||||
"percentile": fields.Float(
|
|
||||||
missing=99.9,
|
|
||||||
example=99.9,
|
|
||||||
description=(
|
|
||||||
"A float between 0 and 100 setting the centile to use "
|
|
||||||
"to measure the white point of the image. A value "
|
|
||||||
"of 99.9 allows 0.1% of the pixels to be erroneously "
|
|
||||||
"bright - this helps stability in low light."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
with find_picamera() as (picamera, _, _):
|
with find_picamera() as (picamera, _, _):
|
||||||
|
|
@ -231,8 +231,10 @@ class AutoWhiteBalanceFromRawView(ActionView):
|
||||||
class GetRawChannelPercentilesView(ActionView):
|
class GetRawChannelPercentilesView(ActionView):
|
||||||
args = {
|
args = {
|
||||||
"percentile": fields.Float(
|
"percentile": fields.Float(
|
||||||
example=99.9,
|
metadata={
|
||||||
description="A float between 0 and 100 setting the centile to calculate",
|
"description": "A float between 0 and 100 setting the centile to calculate",
|
||||||
|
"example": 99.9,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
schema = fields.List(fields.Integer)
|
schema = fields.List(fields.Integer)
|
||||||
|
|
|
||||||
|
|
@ -578,19 +578,23 @@ class ScanExtension(BaseExtension):
|
||||||
|
|
||||||
|
|
||||||
class TileScanArgs(FullCaptureArgs):
|
class TileScanArgs(FullCaptureArgs):
|
||||||
namemode = fields.String(missing="coordinates", example="coordinates")
|
namemode = fields.String(
|
||||||
|
load_default="coordinates", metadata={"example": "coordinates"}
|
||||||
|
)
|
||||||
grid = fields.List(
|
grid = fields.List(
|
||||||
fields.Integer(validate=marshmallow.validate.Range(min=1)),
|
fields.Integer(validate=marshmallow.validate.Range(min=1)),
|
||||||
missing=[3, 3, 3],
|
load_default=[3, 3, 3],
|
||||||
example=[3, 3, 3],
|
metadata={"example": [3, 3, 3]},
|
||||||
)
|
)
|
||||||
style = fields.String(missing="raster")
|
style = fields.String(load_default="raster")
|
||||||
autofocus_dz = fields.Integer(missing=50)
|
autofocus_dz = fields.Integer(load_default=50)
|
||||||
fast_autofocus = fields.Boolean(missing=False)
|
fast_autofocus = fields.Boolean(load_default=False)
|
||||||
stride_size = fields.List(
|
stride_size = fields.List(
|
||||||
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
|
fields.Integer,
|
||||||
|
load_default=[2000, 1500, 100],
|
||||||
|
metadata={"example": [2000, 1500, 100]},
|
||||||
)
|
)
|
||||||
detect_empty_fields_and_skip_autofocus = fields.Boolean(missing=False)
|
detect_empty_fields_and_skip_autofocus = fields.Boolean(load_default=False)
|
||||||
|
|
||||||
|
|
||||||
class TileScanAPI(ActionView):
|
class TileScanAPI(ActionView):
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,11 @@ class RaiseException(ActionView):
|
||||||
|
|
||||||
class SleepFor(ActionView):
|
class SleepFor(ActionView):
|
||||||
schema = {"TimeAsleep": fields.Float()}
|
schema = {"TimeAsleep": fields.Float()}
|
||||||
args = {"time": fields.Float(description="Time to sleep, in seconds", example=0.5)}
|
args = {
|
||||||
|
"time": fields.Float(
|
||||||
|
metadata={"description": "Time to sleep, in seconds", "example": 0.5}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
sleep_time: int = args.get("time", 0)
|
sleep_time: int = args.get("time", 0)
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,8 @@ API_TAGS = [
|
||||||
"embedded in the JSON action description."
|
"embedded in the JSON action description."
|
||||||
),
|
),
|
||||||
"externalDocs": {
|
"externalDocs": {
|
||||||
"url": "https://iot.mozilla.org/wot/#action-resource",
|
"url": "https://www.w3.org/TR/wot-thing-description/#actionaffordance",
|
||||||
"description": "Mozilla's description of Web of Things 'Action' resources.",
|
"description": "W3C's description of Web of Things 'Action' resources.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -19,8 +19,8 @@ API_TAGS = [
|
||||||
"state of the microscope."
|
"state of the microscope."
|
||||||
),
|
),
|
||||||
"externalDocs": {
|
"externalDocs": {
|
||||||
"url": "https://iot.mozilla.org/wot/#property-resource",
|
"url": "https://www.w3.org/TR/wot-thing-description/#propertyaffordance",
|
||||||
"description": "Mozilla's description of Web of Things 'Property' resources.",
|
"description": "W3C's description of Web of Things 'Property' resources.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{"name": "captures", "description": ""},
|
{"name": "captures", "description": ""},
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ def init_default_extensions(extension_dir: str):
|
||||||
create_file(default_ext_path)
|
create_file(default_ext_path)
|
||||||
|
|
||||||
logging.info("Populating %s...", (default_ext_path))
|
logging.info("Populating %s...", (default_ext_path))
|
||||||
with open(default_ext_path, "w") as outfile:
|
with open(default_ext_path, "w", encoding="utf-8") as outfile:
|
||||||
outfile.write(_DEFAULT_EXTENSION_INIT)
|
outfile.write(_DEFAULT_EXTENSION_INIT)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,28 @@ from openflexure_microscope.api.v2.views.captures import CaptureSchema
|
||||||
|
|
||||||
|
|
||||||
class CaptureResizeSchema(Schema):
|
class CaptureResizeSchema(Schema):
|
||||||
width = fields.Integer(example=640, required=True)
|
width = fields.Integer(metadata={"example": 640}, required=True)
|
||||||
height = fields.Integer(example=480, required=True)
|
height = fields.Integer(metadata={"example": 480}, required=True)
|
||||||
|
|
||||||
|
|
||||||
class BasicCaptureArgs(Schema):
|
class BasicCaptureArgs(Schema):
|
||||||
use_video_port = fields.Boolean(missing=False)
|
use_video_port = fields.Boolean(load_default=False)
|
||||||
bayer = fields.Boolean(
|
bayer = fields.Boolean(
|
||||||
missing=False, description="Include raw bayer data in capture"
|
load_default=False,
|
||||||
|
metadata={"description": "Include raw bayer data in capture"},
|
||||||
)
|
)
|
||||||
resize = fields.Nested(CaptureResizeSchema(), required=False)
|
resize = fields.Nested(CaptureResizeSchema(), required=False)
|
||||||
|
|
||||||
|
|
||||||
class FullCaptureArgs(BasicCaptureArgs):
|
class FullCaptureArgs(BasicCaptureArgs):
|
||||||
filename = fields.String(example="MyFileName")
|
filename = fields.String(metadata={"example": "MyFileName"})
|
||||||
temporary = fields.Boolean(missing=False, description="Delete capture on shutdown")
|
temporary = fields.Boolean(
|
||||||
annotations = fields.Dict(missing={}, example={"Client": "SwaggerUI"})
|
load_default=False, metadata={"description": "Delete capture on shutdown"}
|
||||||
tags = fields.List(fields.String, missing=[], example=["docs"])
|
)
|
||||||
|
annotations = fields.Dict(
|
||||||
|
load_default={}, metadata={"example": {"Client": "SwaggerUI"}}
|
||||||
|
)
|
||||||
|
tags = fields.List(fields.String, load_default=[], metadata={"example": ["docs"]})
|
||||||
|
|
||||||
|
|
||||||
class CaptureAPI(ActionView):
|
class CaptureAPI(ActionView):
|
||||||
|
|
@ -113,7 +118,11 @@ class GPUPreviewStartAPI(ActionView):
|
||||||
in the format ``[x, y, width, height]``.
|
in the format ``[x, y, width, height]``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
args = {"window": fields.List(fields.Integer, missing=[], example=[0, 0, 640, 480])}
|
args = {
|
||||||
|
"window": fields.List(
|
||||||
|
fields.Integer, load_default=[], metadata={"example": [0, 0, 640, 480]}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,12 @@ from labthings.views import ActionView
|
||||||
class MoveStageAPI(ActionView):
|
class MoveStageAPI(ActionView):
|
||||||
args = {
|
args = {
|
||||||
"absolute": fields.Boolean(
|
"absolute": fields.Boolean(
|
||||||
missing=False, example=False, description="Move to an absolute position"
|
load_default=False,
|
||||||
|
metadata={"description": "Move to an absolute position", "example": False},
|
||||||
),
|
),
|
||||||
"x": fields.Int(missing=None, example=100, allow_none=False),
|
"x": fields.Int(load_default=None, metadata={"example": 100}, allow_none=True),
|
||||||
"y": fields.Int(missing=None, example=100, allow_none=False),
|
"y": fields.Int(load_default=None, metadata={"example": 100}, allow_none=True),
|
||||||
"z": fields.Int(missing=None, example=20, allow_none=False),
|
"z": fields.Int(load_default=None, metadata={"example": 20}, allow_none=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ class InstrumentSchema(Schema):
|
||||||
|
|
||||||
class ImageSchema(Schema):
|
class ImageSchema(Schema):
|
||||||
id = fields.UUID()
|
id = fields.UUID()
|
||||||
time = fields.String(format="date")
|
time = fields.String(metadata={"format": "date"})
|
||||||
format = fields.String()
|
format = fields.String()
|
||||||
name = fields.String()
|
name = fields.String()
|
||||||
tags = fields.List(fields.String())
|
tags = fields.List(fields.String())
|
||||||
|
|
@ -59,7 +59,7 @@ class CaptureSchema(ImageSchema):
|
||||||
# without the server having to do a tonne of file IO
|
# without the server having to do a tonne of file IO
|
||||||
dataset = fields.Nested(BasicDatasetSchema())
|
dataset = fields.Nested(BasicDatasetSchema())
|
||||||
file = fields.String(
|
file = fields.String(
|
||||||
data_key="path", description="Path of file on microscope device"
|
data_key="path", metadata={"description": "Path of file on microscope device"}
|
||||||
)
|
)
|
||||||
# No need to make a schema for links as we only ever
|
# No need to make a schema for links as we only ever
|
||||||
# create the dictionary right here in `generate_links`
|
# create the dictionary right here in `generate_links`
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ class StageTypeProperty(PropertyView):
|
||||||
"""The type of the stage"""
|
"""The type of the stage"""
|
||||||
|
|
||||||
schema = fields.String(
|
schema = fields.String(
|
||||||
missing=None,
|
load_default="SangaStage",
|
||||||
example="SangaStage",
|
|
||||||
validate=validate.OneOf(["SangaStage", "SangaDeltaStage"]),
|
validate=validate.OneOf(["SangaStage", "SangaDeltaStage"]),
|
||||||
description="The translation stage geometry",
|
metadata={
|
||||||
|
"description": "The translation stage geometry",
|
||||||
|
"example": "SangaStage",
|
||||||
|
},
|
||||||
allow_none=False,
|
allow_none=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ class FrameStream(io.BytesIO):
|
||||||
t: Optional[Type[BaseException]],
|
t: Optional[Type[BaseException]],
|
||||||
value: Optional[BaseException],
|
value: Optional[BaseException],
|
||||||
traceback: Optional[TracebackType],
|
traceback: Optional[TracebackType],
|
||||||
) -> Optional[bool]:
|
) -> None:
|
||||||
self.stop_tracking()
|
self.stop_tracking()
|
||||||
return super().__exit__(t, value, traceback)
|
return super().__exit__(t, value, traceback)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
# Type hinting
|
# Type hinting
|
||||||
from typing import BinaryIO, Tuple, Union
|
from typing import BinaryIO, Optional, Tuple, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
@ -307,7 +307,9 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.picamera.zoom = new_fov
|
self.picamera.zoom = new_fov
|
||||||
|
|
||||||
def start_preview(
|
def start_preview(
|
||||||
self, fullscreen: bool = True, window: Tuple[int, int, int, int] = None
|
self,
|
||||||
|
fullscreen: bool = True,
|
||||||
|
window: Optional[Tuple[int, int, int, int]] = None,
|
||||||
):
|
):
|
||||||
"""Start the on board GPU camera preview."""
|
"""Start the on board GPU camera preview."""
|
||||||
with self.lock(timeout=1):
|
with self.lock(timeout=1):
|
||||||
|
|
@ -462,9 +464,9 @@ class PiCameraStreamer(BaseCamera):
|
||||||
output: Union[str, BinaryIO],
|
output: Union[str, BinaryIO],
|
||||||
fmt: str = "jpeg",
|
fmt: str = "jpeg",
|
||||||
use_video_port: bool = False,
|
use_video_port: bool = False,
|
||||||
resize: Tuple[int, int] = None,
|
resize: Optional[Tuple[int, int]] = None,
|
||||||
bayer: bool = True,
|
bayer: bool = True,
|
||||||
thumbnail: Tuple[int, int, int] = None,
|
thumbnail: Optional[Tuple[int, int, int]] = None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Capture a still image to a StreamObject.
|
Capture a still image to a StreamObject.
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,8 @@ class CaptureObject(object):
|
||||||
logging.debug("Finished writing to disk %s", self.file)
|
logging.debug("Finished writing to disk %s", self.file)
|
||||||
|
|
||||||
def open(self, mode):
|
def open(self, mode):
|
||||||
return open(self.file, mode)
|
# We don't specify an encoding because this will be a binary file.
|
||||||
|
return open(self.file, mode) # pylint: disable=unspecified-encoding
|
||||||
|
|
||||||
def split_file_path(self, filepath: str):
|
def split_file_path(self, filepath: str):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .json import JSONEncoder
|
from .json import JSONEncoder
|
||||||
from .paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
|
from .paths import CONFIGURATION_FILE_PATH, SETTINGS_FILE_PATH
|
||||||
|
|
@ -17,7 +18,7 @@ class OpenflexureSettingsFile:
|
||||||
expand (bool): Expand paths to valid auxillary config files.
|
expand (bool): Expand paths to valid auxillary config files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, path: str, defaults: dict = None):
|
def __init__(self, path: str, defaults: Optional[dict] = None):
|
||||||
defaults = defaults or {}
|
defaults = defaults or {}
|
||||||
|
|
||||||
# Set arguments
|
# Set arguments
|
||||||
|
|
@ -88,7 +89,7 @@ def load_json_file(config_path) -> dict:
|
||||||
|
|
||||||
logging.info("Loading %s...", config_path)
|
logging.info("Loading %s...", config_path)
|
||||||
|
|
||||||
with open(config_path) as config_file:
|
with open(config_path, encoding="utf-8") as config_file:
|
||||||
try:
|
try:
|
||||||
config_data = json.load(config_file)
|
config_data = json.load(config_file)
|
||||||
except json.decoder.JSONDecodeError as e:
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
|
@ -112,7 +113,7 @@ def save_json_file(config_path: str, config_dict: dict):
|
||||||
logging.info("Saving %s...", config_path)
|
logging.info("Saving %s...", config_path)
|
||||||
logging.debug(config_dict)
|
logging.debug(config_dict)
|
||||||
|
|
||||||
with open(config_path, "w") as outfile:
|
with open(config_path, "w", encoding="utf-8") as outfile:
|
||||||
json.dump(config_dict, outfile, cls=JSONEncoder, indent=2, sort_keys=True)
|
json.dump(config_dict, outfile, cls=JSONEncoder, indent=2, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -150,7 +151,7 @@ def initialise_file(config_path, populate: str = "{}\n"):
|
||||||
create_file(config_path)
|
create_file(config_path)
|
||||||
|
|
||||||
logging.info("Populating %s...", (config_path))
|
logging.info("Populating %s...", (config_path))
|
||||||
with open(config_path, "w") as outfile:
|
with open(config_path, "w", encoding="utf-8") as outfile:
|
||||||
outfile.write(populate)
|
outfile.write(populate)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,7 +370,7 @@ class Microscope:
|
||||||
folder: str = "",
|
folder: str = "",
|
||||||
temporary: bool = False,
|
temporary: bool = False,
|
||||||
use_video_port: bool = False,
|
use_video_port: bool = False,
|
||||||
resize: Tuple[int, int] = None,
|
resize: Optional[Tuple[int, int]] = None,
|
||||||
bayer: bool = True,
|
bayer: bool = True,
|
||||||
fmt: str = "jpeg",
|
fmt: str = "jpeg",
|
||||||
annotations: Optional[Dict[str, str]] = None,
|
annotations: Optional[Dict[str, str]] = None,
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ def main():
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import picamerax
|
import picamerax
|
||||||
except Exception as e: # pylint: disable=W0703
|
except Exception as _e: # pylint: disable=W0703
|
||||||
error_sources.append(PICAMERA_IMPORT_ERROR)
|
error_sources.append(PICAMERA_IMPORT_ERROR)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
_ = picamerax.PiCamera()
|
_cam = picamerax.PiCamera()
|
||||||
except picamerax.PiCameraError as e:
|
except picamerax.PiCameraError as e:
|
||||||
msg = e.args[0]
|
msg = e.args[0]
|
||||||
if msg in PICAMERA_ERROR_MAP:
|
if msg in PICAMERA_ERROR_MAP:
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ def main():
|
||||||
# Try connecting on the specified port
|
# Try connecting on the specified port
|
||||||
try:
|
try:
|
||||||
stage = Sangaboard(stage_port)
|
stage = Sangaboard(stage_port)
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as _:
|
||||||
if stage_port:
|
if stage_port:
|
||||||
error_sources.append(
|
error_sources.append(
|
||||||
ErrorSource(
|
ErrorSource(
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ def script_directory(path):
|
||||||
|
|
||||||
|
|
||||||
def get_meta():
|
def get_meta():
|
||||||
with open(script_directory("metadata.yaml")) as f:
|
with open(script_directory("metadata.yaml"), encoding="utf-8") as f:
|
||||||
metadata = f.read()
|
metadata = f.read()
|
||||||
|
|
||||||
return yaml.safe_load(metadata)
|
return yaml.safe_load(metadata)
|
||||||
|
|
@ -51,7 +51,7 @@ def main():
|
||||||
zenodo.upload_file(deposit["id"], path)
|
zenodo.upload_file(deposit["id"], path)
|
||||||
|
|
||||||
link = deposit["links"]["latest_draft_html"]
|
link = deposit["links"]["latest_draft_html"]
|
||||||
with open("zenodo-link.html", "w") as f:
|
with open("zenodo-link.html", "w", encoding="utf-8") as f:
|
||||||
f.write(f'<a href="{link}">{link}</a>')
|
f.write(f'<a href="{link}">{link}</a>')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
25
setup.py
25
setup.py
|
|
@ -22,7 +22,7 @@ with open(path.join(here, "README.md"), encoding="utf-8") as f:
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="openflexure-microscope-server",
|
name="openflexure-microscope-server",
|
||||||
version="2.10.1",
|
version="2.11.0",
|
||||||
description="Python module, and Flask-based web API, to run the OpenFlexure Microscope.",
|
description="Python module, and Flask-based web API, to run the OpenFlexure Microscope.",
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
long_description_content_type="text/markdown",
|
long_description_content_type="text/markdown",
|
||||||
|
|
@ -51,8 +51,9 @@ setup(
|
||||||
"scipy ~= 1.6.1",
|
"scipy ~= 1.6.1",
|
||||||
"python-dateutil ~= 2.8",
|
"python-dateutil ~= 2.8",
|
||||||
"psutil ~= 5.6.7", # Autostorage extension
|
"psutil ~= 5.6.7", # Autostorage extension
|
||||||
|
"markupsafe ~= 2.0.0", # 2.1.x breaks jinja2
|
||||||
"opencv-python-headless ~= 4.5.1",
|
"opencv-python-headless ~= 4.5.1",
|
||||||
"sangaboard ~= 0.2",
|
"sangaboard ~= 0.3.3",
|
||||||
"expiringdict ~= 1.2.1",
|
"expiringdict ~= 1.2.1",
|
||||||
"camera-stage-mapping == 0.1.4",
|
"camera-stage-mapping == 0.1.4",
|
||||||
"picamerax ~= 20.9.1",
|
"picamerax ~= 20.9.1",
|
||||||
|
|
@ -60,7 +61,7 @@ setup(
|
||||||
"pytest-cov ~= 2.10.1",
|
"pytest-cov ~= 2.10.1",
|
||||||
"piexif ~= 1.1.3",
|
"piexif ~= 1.1.3",
|
||||||
"labthings ~= 1.3.0",
|
"labthings ~= 1.3.0",
|
||||||
"typing-extensions ~= 3.7.4", # Needed for some type-hints in Python < 3.8 (e.g. Literal)
|
"typing-extensions ~= 4.3.0", # Needed for some type-hints in Python < 3.8 (e.g. Literal)
|
||||||
"RPi.GPIO ~= 0.7.0; platform_machine == 'armv7l'",
|
"RPi.GPIO ~= 0.7.0; platform_machine == 'armv7l'",
|
||||||
],
|
],
|
||||||
# "dev" specifies extra packages used for development (linting, testing, etc.)
|
# "dev" specifies extra packages used for development (linting, testing, etc.)
|
||||||
|
|
@ -71,19 +72,15 @@ setup(
|
||||||
"sphinx < 4.0", # Currently httpdomain isn't ready for 4.0
|
"sphinx < 4.0", # Currently httpdomain isn't ready for 4.0
|
||||||
"sphinxcontrib-openapi ~= 0.7",
|
"sphinxcontrib-openapi ~= 0.7",
|
||||||
"sphinx_rtd_theme ~=0.5.2",
|
"sphinx_rtd_theme ~=0.5.2",
|
||||||
"rope ~= 0.14.0",
|
"pylint ~= 2.14.0", # 2.9.2 crashes and I've not yet figured out why.
|
||||||
"pylint ~= 2.8.0", # 2.9.2 crashes and I've not yet figured out why.
|
"pytest ~= 7.1.2",
|
||||||
"pytest ~= 6.1.2",
|
"mypy ~= 0.971",
|
||||||
"mypy ~= 0.790",
|
|
||||||
"types-python-dateutil",
|
"types-python-dateutil",
|
||||||
"types-setuptools",
|
"types-setuptools",
|
||||||
"poethepoet ~= 0.10.0",
|
"poethepoet ~= 0.16.0",
|
||||||
"freezegun ~= 1.0.0",
|
"freezegun ~= 1.2.1",
|
||||||
"lxml ~= 4.6",
|
"lxml ~= 4.9",
|
||||||
"black == 18.9b0", # Because black is in prerelease, giving a flexible version
|
"black == 18.9b0",
|
||||||
# specifier causes all manner of problems. A fixed version
|
|
||||||
# avoids issues with prereleases (which are normally not
|
|
||||||
# allowed by pipenv) but can be a bit of a pain sometimes!
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
dependency_links=[],
|
dependency_links=[],
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ def test_thing_description_valid():
|
||||||
# TODO: it would be nice to put this into LabThings
|
# TODO: it would be nice to put this into LabThings
|
||||||
# First load the schema from file and check it validates
|
# First load the schema from file and check it validates
|
||||||
schema_fname = os.path.join(os.path.dirname(__file__), "w3c_td_schema.json")
|
schema_fname = os.path.join(os.path.dirname(__file__), "w3c_td_schema.json")
|
||||||
schema = json.load(open(schema_fname, "r"))
|
schema = json.load(open(schema_fname, "r"), encoding="utf-8")
|
||||||
jsonschema.Draft7Validator.check_schema(schema)
|
jsonschema.Draft7Validator.check_schema(schema)
|
||||||
|
|
||||||
# Build a TD dictionary
|
# Build a TD dictionary
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,10 @@
|
||||||
"thing-context": {
|
"thing-context": {
|
||||||
"oneOf": [{
|
"oneOf": [{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": [{
|
"prefixItems": [{
|
||||||
"$ref": "#/definitions/thing-context-w3c-uri"
|
"$ref": "#/definitions/thing-context-w3c-uri"
|
||||||
}],
|
}],
|
||||||
"additionalItems": {
|
"items": {
|
||||||
"anyOf": [{
|
"anyOf": [{
|
||||||
"$ref": "#/definitions/anyUri"
|
"$ref": "#/definitions/anyUri"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue