Skip to content

Commit 4369ceb

Browse files
authored
Merge branch 'master' into task-logging-test
2 parents 85d7e40 + e34794f commit 4369ceb

File tree

96 files changed

+5800
-787
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+5800
-787
lines changed

.coveragerc

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[run]
2+
branch = True
3+
source = ./labthings
4+
omit = .venv/*, labthings/server/wsgi/*, , labthings/server/monkey.py
5+
concurrency = greenlet
6+
7+
[report]
8+
# Regexes for lines to exclude from consideration
9+
exclude_lines =
10+
# Have to re-enable the standard pragma
11+
pragma: no cover
12+
13+
# Don't complain about missing debug-only code:
14+
def __repr__
15+
if self\.debug
16+
17+
# Don't complain if tests don't hit defensive assertion code:
18+
raise AssertionError
19+
raise NotImplementedError
20+
21+
# Don't complain if non-runnable code isn't run:
22+
if 0:
23+
if __name__ == .__main__.:
24+
25+
ignore_errors = True
26+
27+
[html]
28+
directory = coverage_html_report

.flake8

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[flake8]
2+
max-line-length = 88
3+
exclude = tests/*

.github/workflows/ci.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: CI
2+
3+
on: [push]
4+
5+
jobs:
6+
test:
7+
runs-on: ubuntu-latest
8+
9+
steps:
10+
- uses: actions/checkout@v1
11+
with:
12+
fetch-depth: 1
13+
14+
- name: Set up Python 3.7
15+
uses: actions/setup-python@v1
16+
with:
17+
python-version: 3.7
18+
19+
- name: Install Poetry
20+
uses: dschep/install-poetry-action@v1.3
21+
22+
- name: Cache Poetry virtualenv
23+
uses: actions/cache@v1
24+
id: cache
25+
with:
26+
path: ~/.virtualenvs
27+
key: poetry-${{ hashFiles('**/poetry.lock') }}
28+
restore-keys: |
29+
poetry-${{ hashFiles('**/poetry.lock') }}
30+
31+
- name: Set Poetry config
32+
run: |
33+
poetry config virtualenvs.in-project false
34+
poetry config virtualenvs.path ~/.virtualenvs
35+
36+
- name: Install Dependencies
37+
run: poetry install
38+
if: steps.cache.outputs.cache-hit != 'true'
39+
40+
- name: Code Quality
41+
run: poetry run black . --check
42+
43+
- name: Test with pytest
44+
run: poetry run pytest --cov-report term-missing --cov-report=xml --cov=labthings ./tests
45+
46+
- name: Upload coverage to Codecov
47+
uses: codecov/codecov-action@v1
48+
with:
49+
file: ./coverage.xml
50+
flags: unittests
51+
name: codecov-umbrella
52+
fail_ci_if_error: true

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pip-delete-this-directory.txt
3939

4040
# Unit test / coverage reports
4141
htmlcov/
42+
coverage_html_report/
4243
.tox/
4344
.nox/
4445
.coverage
@@ -50,6 +51,8 @@ coverage.xml
5051
*.py,cover
5152
.hypothesis/
5253
.pytest_cache/
54+
coverage_html_report/
55+
prof/
5356

5457
# Translations
5558
*.mo

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
[![LabThings](https://img.shields.io/badge/-LabThings-8E00FF?style=flat&logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4NCjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz4NCjxzdmcgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIyIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNjMgMTYzIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Im0xMjIuMjQgMTYyLjk5aDQwLjc0OHYtMTYyLjk5aC0xMDEuODd2NDAuNzQ4aDYxLjEyMnYxMjIuMjR6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0ibTAgMTIuMjI0di0xMi4yMjRoNDAuNzQ4djEyMi4yNGg2MS4xMjJ2NDAuNzQ4aC0xMDEuODd2LTEyLjIyNGgyMC4zNzR2LTguMTVoLTIwLjM3NHYtOC4xNDloOC4wMTl2LTguMTVoLTguMDE5di04LjE1aDIwLjM3NHYtOC4xNDloLTIwLjM3NHYtOC4xNWg4LjAxOXYtOC4xNWgtOC4wMTl2LTguMTQ5aDIwLjM3NHYtOC4xNWgtMjAuMzc0di04LjE0OWg4LjAxOXYtOC4xNWgtOC4wMTl2LTguMTVoMjAuMzc0di04LjE0OWgtMjAuMzc0di04LjE1aDguMDE5di04LjE0OWgtOC4wMTl2LTguMTVoMjAuMzc0di04LjE1aC0yMC4zNzR6IiBmaWxsPSIjZmZmIi8+PC9zdmc+DQo=)](https://github.com/labthings/)
44
[![PyPI](https://img.shields.io/pypi/v/labthings)](https://pypi.org/project/labthings/)
55
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
6-
[![Gitter](https://badges.gitter.im/labthings/community.svg)](https://gitter.im/labthings/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
6+
[![codecov](https://codecov.io/gh/labthings/python-labthings/branch/master/graph/badge.svg)](https://codecov.io/gh/labthings/python-labthings)
7+
[![Riot.im](https://img.shields.io/badge/chat-on%20riot.im-368BD6)](https://riot.im/app/#/room/#labthings:matrix.org)
78

89
A Python implementation of the LabThings API structure, based on the Flask microframework.
910

examples/builder.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import uuid
2-
import types
3-
import functools
4-
import atexit
1+
# Monkey patch for easy concurrency
2+
from labthings.server.monkey import patch_all
3+
4+
patch_all()
5+
6+
# Import requirements
57
import logging
68

79
from labthings.server.quick import create_app
8-
from labthings.server.view.builder import property_of
10+
from labthings.server.view.builder import property_of, action_from
911

1012
from components.pdf_component import PdfComponent
1113

1214

13-
def cleanup():
14-
logging.info("Exiting. Running any cleanup code here...")
15-
16-
1715
# Create LabThings Flask app
1816
app, labthing = create_app(
1917
__name__,
@@ -42,8 +40,17 @@ def cleanup():
4240
),
4341
"/dictionary",
4442
)
43+
labthing.add_view(
44+
action_from(
45+
my_component.average_data,
46+
description="Take an averaged measurement",
47+
task=True, # Is the action a long-running task?
48+
safe=True, # Is the state of the Thing unchanged by calling the action?
49+
idempotent=True, # Can the action be called repeatedly with the same result?
50+
),
51+
"/average",
52+
)
4553

46-
atexit.register(cleanup)
4754

4855
# Start the app
4956
if __name__ == "__main__":

examples/components/pdf_component.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import math
33
import time
44

5+
from typing import List
6+
57
"""
68
Class for our lab component functionality. This could include serial communication,
79
equipment API calls, network requests, or a "virtual" device as seen here.
@@ -38,7 +40,7 @@ def data(self):
3840
"""Return a 1D data trace."""
3941
return [self.noisy_pdf(x) for x in self.x_range]
4042

41-
def average_data(self, n: int):
43+
def average_data(self, n: int = 10, optlist: List[int] = [1, 2, 3]):
4244
"""Average n-sets of data. Emulates a measurement that may take a while."""
4345
summed_data = self.data
4446

examples/simple_extensions.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ def ext_on_my_component(component):
4040

4141

4242
static_folder = path_relative_to(__file__, "static")
43-
print(static_folder)
4443

4544
example_extension = BaseExtension(
4645
"org.labthings.examples.extension", static_folder=static_folder

examples/simple_thing.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1+
#!/usr/bin/env python
2+
from gevent import monkey
3+
4+
# Patch most system modules. Leave threads untouched so we can still use them normally if needed.
5+
print("Monkey patching with Gevenet")
6+
monkey.patch_all(thread=False)
7+
print("Monkey patching successful")
8+
19
import random
210
import math
311
import time
12+
import logging
13+
import atexit
414

515
from labthings.server.quick import create_app
616
from labthings.server.decorators import (
@@ -13,7 +23,7 @@
1323
from labthings.server.view import View
1424
from labthings.server.find import find_component
1525
from labthings.server import fields
16-
from labthings.core.tasks import taskify
26+
from labthings.core.tasks import taskify, update_task_data
1727

1828

1929
"""
@@ -22,6 +32,14 @@
2232
"""
2333

2434

35+
from gevent.monkey import get_original
36+
37+
get_ident = get_original("_thread", "get_ident")
38+
39+
print(f"ROOT IDENT")
40+
print(get_ident())
41+
42+
2543
class MyComponent:
2644
def __init__(self):
2745
self.x_range = range(-100, 100)
@@ -48,8 +66,10 @@ def average_data(self, n: int):
4866
"""Average n-sets of data. Emulates a measurement that may take a while."""
4967
summed_data = self.data
5068

69+
logging.warning("Starting an averaged measurement. This may take a while...")
5170
for i in range(n):
5271
summed_data = [summed_data[i] + el for i, el in enumerate(self.data)]
72+
update_task_data({"data": summed_data})
5373
time.sleep(0.25)
5474

5575
summed_data = [i / n for i in summed_data]
@@ -150,6 +170,13 @@ def post(self, args):
150170
return task
151171

152172

173+
# Handle exit cleanup
174+
def cleanup():
175+
logging.info("Exiting. Running any cleanup code here...")
176+
177+
178+
atexit.register(cleanup)
179+
153180
# Create LabThings Flask app
154181
app, labthing = create_app(
155182
__name__,
@@ -173,4 +200,4 @@ def post(self, args):
173200
from labthings.server.wsgi import Server
174201

175202
server = Server(app)
176-
server.run(host="0.0.0.0", port=5000, debug=False)
203+
server.run(host="0.0.0.0", port=5000, debug=False, zeroconf=False)

labthings/core/event.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from gevent.hub import getcurrent
2+
import gevent
3+
import time
4+
import logging
5+
6+
from gevent.monkey import get_original
7+
8+
# Guarantee that Task threads will always be proper system threads, regardless of Gevent patches
9+
Event = get_original("threading", "Event")
10+
11+
12+
class ClientEvent(object):
13+
"""
14+
An event-signaller object with per-client setting and waiting.
15+
16+
A client can be any Greenlet or native Thread. This can be used, for example,
17+
to signal to clients that new data is available
18+
"""
19+
20+
def __init__(self):
21+
self.events = {}
22+
23+
def wait(self, timeout: int = 5):
24+
"""Wait for the next data frame (invoked from each client's thread)."""
25+
ident = id(getcurrent())
26+
if ident not in self.events:
27+
# this is a new client
28+
# add an entry for it in the self.events dict
29+
# each entry has two elements, a threading.Event() and a timestamp
30+
self.events[ident] = [Event(), time.time()]
31+
32+
# We have to reimplement event waiting here as we need native thread events to allow gevent context switching
33+
wait_start = time.time()
34+
while not self.events[ident][0].is_set():
35+
now = time.time()
36+
if now - wait_start > timeout:
37+
return False
38+
gevent.sleep(0)
39+
return True
40+
41+
def set(self, timeout=5):
42+
"""Signal that a new frame is available."""
43+
now = time.time()
44+
remove = None
45+
for ident, event in self.events.items():
46+
if not event[0].is_set():
47+
# if this client's event is not set, then set it
48+
# also update the last set timestamp to now
49+
event[0].set()
50+
event[1] = now
51+
else:
52+
# if the client's event is already set, it means the client
53+
# did not process a previous frame
54+
# if the event stays set for more than `timeout` seconds, then assume
55+
# the client is gone and remove it
56+
if now - event[1] >= timeout:
57+
remove = ident
58+
if remove:
59+
del self.events[remove]
60+
61+
def clear(self):
62+
"""Clear frame event, once processed."""
63+
ident = id(getcurrent())
64+
if ident not in self.events:
65+
logging.error(f"Mismatched ident. Current: {ident}, available:")
66+
logging.error(self.events.keys())
67+
return False
68+
self.events[id(getcurrent())][0].clear()
69+
return True

0 commit comments

Comments
 (0)