From ace877b1eaea3ae9755d3028cdc6fa331c9a937b Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 5 Mar 2025 22:22:16 +0000 Subject: [PATCH 1/6] Update quickstart page to match current version The quickstart page was badly out of date. I've updated it to reflect changes in the package, and in how the labthings server is started. --- docs/source/index.rst | 2 +- docs/source/quickstart.rst | 57 ---------------------- docs/source/quickstart/counter.py | 42 ++++++++++++++++ docs/source/quickstart/counter_client.py | 11 +++++ docs/source/quickstart/example_config.json | 5 ++ docs/source/quickstart/quickstart.rst | 41 ++++++++++++++++ 6 files changed, 100 insertions(+), 58 deletions(-) delete mode 100644 docs/source/quickstart.rst create mode 100644 docs/source/quickstart/counter.py create mode 100644 docs/source/quickstart/counter_client.py create mode 100644 docs/source/quickstart/example_config.json create mode 100644 docs/source/quickstart/quickstart.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 67b3c90b..f66e6fe4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,7 +11,7 @@ Welcome to labthings-fastapi's documentation! :caption: Contents: core_concepts.rst - quickstart.rst + quickstart/quickstart.rst dependencies.rst apidocs/index diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst deleted file mode 100644 index 41714401..00000000 --- a/docs/source/quickstart.rst +++ /dev/null @@ -1,57 +0,0 @@ -Quick start -=========== - -The fastest way to get started with `labthings-fastapi` is to try out one of the examples. - -You can install `labthings-fastapi` using `pip`: - -.. code-block:: bash - - pip install labthings-fastapi - -Then, paste the following into a python file, ``counter.py``: - -.. code-block:: python - - import time - from labthings_fastapi.thing import Thing - from labthings_fastapi.decorators import thing_action - from labthings_fastapi.descriptors import PropertyDescriptor - from labthings_fastapi.thing_server import ThingServer - - - class TestThing(Thing): - """A test thing with a counter property and a couple of actions""" - - @thing_action - def increment_counter(self) -> None: - """Increment the counter property - - This action doesn't do very much - all it does, in fact, - is increment the counter (which may be read using the - `counter` property). - """ - self.counter += 1 - - @thing_action - def slowly_increase_counter(self) -> None: - """Increment the counter slowly over a minute""" - for i in range(60): - time.sleep(1) - self.increment_counter() - - counter = PropertyDescriptor( - model=int, initial_value=0, readonly=True, description="A pointless counter" - ) - - - server = ThingServer() - server.add_thing(TestThing(), "/test") - -You can then run this file with `uvicorn`: - -.. code-block:: bash - - uvicorn counter:app --reload - -This will start a server on `http://localhost:8000` that serves the `TestThing` thing. Visiting `http://localhost:8000/test/` will show the thing description, and you can interact with the actions and properties using the Swagger UI at `http://localhost:8000/docs/`. diff --git a/docs/source/quickstart/counter.py b/docs/source/quickstart/counter.py new file mode 100644 index 00000000..12a27f2d --- /dev/null +++ b/docs/source/quickstart/counter.py @@ -0,0 +1,42 @@ +import time +from labthings_fastapi.thing import Thing +from labthings_fastapi.decorators import thing_action +from labthings_fastapi.descriptors import PropertyDescriptor + + +class TestThing(Thing): + """A test thing with a counter property and a couple of actions""" + + @thing_action + def increment_counter(self) -> None: + """Increment the counter property + + This action doesn't do very much - all it does, in fact, + is increment the counter (which may be read using the + `counter` property). + """ + self.counter += 1 + + @thing_action + def slowly_increase_counter(self) -> None: + """Increment the counter slowly over a minute""" + for i in range(60): + time.sleep(1) + self.increment_counter() + + counter = PropertyDescriptor( + model=int, initial_value=0, readonly=True, description="A pointless counter" + ) + +if __name__ == "__main__": + from labthings_fastapi.server import ThingServer + import uvicorn + + server = ThingServer() + + # The line below creates a TestThing instance and adds it to the server + server.add_thing(TestThing(), "/counter/") + + # We run the server using `uvicorn`: + uvicorn.run(server.app, port=5000) + diff --git a/docs/source/quickstart/counter_client.py b/docs/source/quickstart/counter_client.py new file mode 100644 index 00000000..346ac3c7 --- /dev/null +++ b/docs/source/quickstart/counter_client.py @@ -0,0 +1,11 @@ +from labthings_fastapi.client import ThingClient + +counter = ThingClient.from_url("http://localhost:5000/counter/") + +v = counter.counter +print(f"The counter value was {v}") + +counter.increment_counter() + +v = counter.counter +print(f"After incrementing, the counter value was {v}") diff --git a/docs/source/quickstart/example_config.json b/docs/source/quickstart/example_config.json new file mode 100644 index 00000000..5c51a3a0 --- /dev/null +++ b/docs/source/quickstart/example_config.json @@ -0,0 +1,5 @@ +{ + "things": { + "example": "labthings_fastapi.example_things:MyThing" + } +} \ No newline at end of file diff --git a/docs/source/quickstart/quickstart.rst b/docs/source/quickstart/quickstart.rst new file mode 100644 index 00000000..02927c1f --- /dev/null +++ b/docs/source/quickstart/quickstart.rst @@ -0,0 +1,41 @@ +Quick start +=========== + +You can install `labthings-fastapi` using `pip`. Create a virtual environment for you project, then install labthings with: + +.. code-block:: bash + + pip install labthings-fastapi[server] + +To define a simple example ``Thing``, paste the following into a python file, ``counter.py``: + +.. literalinclude:: counter.py + :language: python + +``counter.py`` defines the ``TestThing`` class, and then runs a LabThings server in its ``__name__ == "__main__"`` block. This means we should be able to run the server with: + +.. code-block:: bash + + python counter.py + +Visiting http://localhost:5000/counter/ will show the thing description, and you can interact with the actions and properties using the Swagger UI at http://localhost:5000/docs/. + +You can also interact with it from another Python instance, for example by running: + +.. literalinclude:: counter_client.py + :language: python + +It's best to write ``Thing`` subclasses in Python packages that can be imported. This makes them easier to re-use and distribute, and also allows us to run a LabThings server from the command line, configured by a configuration file. An example config file is below: + +.. literalinclude:: example_config.json + :language: JSON + +Paste this into ``example_config.json`` and then run a server using: + +.. code-block:: bash + + labthings-server -c example_config.json + +Bear in mind that this won't work if `counter.py` above is still running - both will try to use port 5000. + +As before, you can visit http://localhost:5000/docs or http://localhost:5000/example/ to see the OpenAPI docs or Thing Description, and you can use the Python client module with the second of those URLs. \ No newline at end of file From 196efe189270a6b851449aecf629319320d8b0af Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 5 Mar 2025 22:26:38 +0000 Subject: [PATCH 2/6] Unit test for quickstart docs This checks that the server code in the quickstart page actually runs. It tests for successful server start-up, using the same code as test_server_cli.py. This does not verify that the HTTP server actually works - but it should catch obvious errors in the example. --- tests/test_docs_quickstart.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_docs_quickstart.py diff --git a/tests/test_docs_quickstart.py b/tests/test_docs_quickstart.py new file mode 100644 index 00000000..20cd5ed3 --- /dev/null +++ b/tests/test_docs_quickstart.py @@ -0,0 +1,18 @@ +from subprocess import Popen, PIPE, STDOUT +import os +from pathlib import Path +import runpy +from test_server_cli import MonitoredProcess + + +def run_quickstart_counter(): + this_file = Path(__file__) + repo = this_file.parents[1] + quickstart = repo / "docs" / "source" / "quickstart" / "counter.py" + runpy.run_path(quickstart) + + +def test_quickstart_counter(): + """Check we can create a server from the command line""" + p = MonitoredProcess(target=run_quickstart_counter) + p.run_monitored(terminate_outputs=["Application startup complete"]) \ No newline at end of file From ccce6c51507f8265ef70ff6cf38552e2a1be227b Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Wed, 5 Mar 2025 23:23:36 +0000 Subject: [PATCH 3/6] Update dependencies docs I've also added a test for the example in here. --- docs/source/dependencies.rst | 30 ----------------------- docs/source/dependencies/dependencies.rst | 23 +++++++++++++++++ docs/source/dependencies/example.py | 23 +++++++++++++++++ docs/source/index.rst | 2 +- tests/test_docs.py | 28 +++++++++++++++++++++ tests/test_docs_quickstart.py | 18 -------------- 6 files changed, 75 insertions(+), 49 deletions(-) delete mode 100644 docs/source/dependencies.rst create mode 100644 docs/source/dependencies/dependencies.rst create mode 100644 docs/source/dependencies/example.py create mode 100644 tests/test_docs.py delete mode 100644 tests/test_docs_quickstart.py diff --git a/docs/source/dependencies.rst b/docs/source/dependencies.rst deleted file mode 100644 index 5c016391..00000000 --- a/docs/source/dependencies.rst +++ /dev/null @@ -1,30 +0,0 @@ - -Dependencies -============ - -Often, a :class:`~labthings_fastapi.thing.Thing` will want to make use of other :class:`~labthings_fastapi.thing.Thing` instances on the server. To make this simple, we use *dependency injection*, which looks at the type hints on your Action's arguments and supplies the appropriate :class:`~labthings_fastapi.thing.Thing` when the action is called. We are piggy-backing on FastAPI's dependency injection mechanism, and you can see the `FastAPI documentation`_ for more information. - -The easiest way to access another :class:`~labthings_fastapi.thing.Thing` is using the function :func:`~labthings_fastapi.dependencies.thing.direct_thing_client_dependency`. This generates a type annotation that you can use when you define your actions. Optionally, you can specify the actions that you're going to use - this can be helpful if the thing you're depending on has a lot of actions and you only need a few of them, because all of the dependencies of those actions get added to your action. Most of the time, you can just use the default, which is to include all actions. - -.. code-block:: python - - from labthings_fastapi.thing import Thing - from labthings_fastapi.decorators import thing_action - from labthings_fastapi.dependencies.thing import direct_thing_client_dependency - from labthings_fastapi.example_thing import MyThing - - MyThingDep = direct_thing_client_dependency(MyThing) - - class TestThing(Thing): - """A test thing with a counter property and a couple of actions""" - - @thing_action - def increment_counter(self, my_thing: MyThingDep) -> None: - """Increment the counter on another thing""" - my_thing.increment_counter() - -In the example above, the :func:`increment_counter` action on :class:`TestThing` takes a :class:`MyThing` as an argument. When the action is called, the :class:`MyThing` instance is passed in as the ``my_thing`` argument. The recipe above doesn't return the `MyThing` instance directly, it returns something that works in a similar way to the Python client. That means any dependencies of actions on the :class:`MyThing` are automatically supplied, so you only need to worry about the arguments you'd supply when calling it over the network. The aim of this is to ensure that the code you write for your :class:`Thing` is as similar as possible to the code you'd write if you were using it through the Python client module. - -If you need access to the actual Python object (e.g. you need to access methods that are not decorated as actions), you can use the :func:`~labthings_fastapi.dependencies.raw_thing.raw_thing_dependency` function instead. This will give you the actual Python object, but you will need to manage the dependencies yourself. - -.. _`FastAPI documentation`: https://fastapi.tiangolo.com/tutorial/dependencies/ \ No newline at end of file diff --git a/docs/source/dependencies/dependencies.rst b/docs/source/dependencies/dependencies.rst new file mode 100644 index 00000000..4b3bf1e2 --- /dev/null +++ b/docs/source/dependencies/dependencies.rst @@ -0,0 +1,23 @@ + +Dependencies +============ + +Simple actions depend only on their input parameters and the :class:`~labthings_fastapi.thing.Thing` on which they are defined. However, it's quite common to need something else, for example accessing another :class:`~labthings_fastapi.thing.Thing` instance on the same LabThings server. There are two important principles to bear in mind here: + +* Other :class:`~labthings_fastapi.thing.Thing` instances should be accessed using a :class:`~labthings_fastapi.client.in_server.DirectThingClient` subclass if possible. This creates a wrapper object that should work like a :class:`~labthings_fastapi.client.ThingClient`, meaning your code should work either on the server or in a client script. This makes the code much easier to debug. +* LabThings uses the FastAPI "dependency injection" mechanism, where you specify what's needed with type hints, and the argument is supplied automatically at run-time. You can see the `FastAPI documentation`_ for more information. + +LabThings provides a shortcut to create the annotated type needed to declare a dependency on another :class:`~labthings_fastapi.thing.Thing`, with the function :func:`~labthings_fastapi.dependencies.thing.direct_thing_client_dependency`. This generates a type annotation that you can use when you define your actions, that will supply a client object when the action is called. + +:func:`~labthings_fastapi.dependencies.thing.direct_thing_client_dependency` takes a :func:`~labthings_fastapi.thing.Thing` class and a path as arguments: these should match the configuration of your LabThings server. Optionally, you can specify the actions that you're going to use. The default behaviour is to make all actions available, however it is more efficient to specify only the actions you will use. + +Dependencies are added recursively - so if you depend on another Thing, and some of its actions have their own dependencies, those dependencies are also added to your action. Using the ``actions`` argument means you only need the dependencies of the actions you are going to use, which is more efficient. + +.. literalinclude:: example.py + :language: python + +In the example above, the :func:`increment_counter` action on :class:`TestThing` takes a :class:`MyThing` as an argument. When the action is called, the ``my_thing`` argument is supplied automatically. The argument is not the :class:`MyThing` instance, instead it is a wrapper class (a dynamically generated :class:`~labthings_fastapi.client.in_server.DirectThingClient` subclass). The wrapper should have the same signature as a :class:`~labthings_fastapi.client.ThingClient`. This means any dependencies of actions on the :class:`MyThing` are automatically supplied, so you only need to worry about the arguments that are not dependencies. The aim of this is to ensure that the code you write for your :class:`Thing` is as similar as possible to the code you'd write if you were using it through the Python client module. + +If you need access to the actual Python object (e.g. you need to access methods that are not decorated as actions), you can use the :func:`~labthings_fastapi.dependencies.raw_thing.raw_thing_dependency` function instead. This will give you the actual Python object, but you will need to supply all the arguments of the actions, including dependencies, yourself. + +.. _`FastAPI documentation`: https://fastapi.tiangolo.com/tutorial/dependencies/ \ No newline at end of file diff --git a/docs/source/dependencies/example.py b/docs/source/dependencies/example.py new file mode 100644 index 00000000..fab5eb43 --- /dev/null +++ b/docs/source/dependencies/example.py @@ -0,0 +1,23 @@ +from labthings_fastapi.thing import Thing +from labthings_fastapi.decorators import thing_action +from labthings_fastapi.dependencies.thing import direct_thing_client_dependency +from labthings_fastapi.example_things import MyThing +from labthings_fastapi.server import ThingServer + +MyThingDep = direct_thing_client_dependency(MyThing, "/mything/") + +class TestThing(Thing): + """A test thing with a counter property and a couple of actions""" + + @thing_action + def increment_counter(self, my_thing: MyThingDep) -> None: + """Increment the counter on another thing""" + my_thing.increment_counter() + +server = ThingServer() +server.add_thing(MyThing(), "/mything/") +server.add_thing(TestThing(), "/testthing/") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(server.app, port=5000) \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index f66e6fe4..0b6bb13c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,7 +12,7 @@ Welcome to labthings-fastapi's documentation! core_concepts.rst quickstart/quickstart.rst - dependencies.rst + dependencies/dependencies.rst apidocs/index diff --git a/tests/test_docs.py b/tests/test_docs.py new file mode 100644 index 00000000..a00340a7 --- /dev/null +++ b/tests/test_docs.py @@ -0,0 +1,28 @@ +from subprocess import Popen, PIPE, STDOUT +import os +from pathlib import Path +from runpy import run_path +from test_server_cli import MonitoredProcess +from fastapi.testclient import TestClient +from labthings_fastapi.client import ThingClient + + +this_file = Path(__file__) +repo = this_file.parents[1] +docs = repo / "docs" / "source" + +def run_quickstart_counter(): + # A server is started in the `__name__ == "__main__" block` + run_path(docs / "quickstart" / "counter.py") + + +def test_quickstart_counter(): + """Check we can create a server from the command line""" + p = MonitoredProcess(target=run_quickstart_counter) + p.run_monitored(terminate_outputs=["Application startup complete"]) + +def test_dependency_example(): + globals = run_path(docs / "dependencies" / "example.py", run_name="not_main") + with TestClient(globals["server"].app) as client: + testthing = ThingClient.from_url("/testthing/", client=client) + testthing.increment_counter() diff --git a/tests/test_docs_quickstart.py b/tests/test_docs_quickstart.py deleted file mode 100644 index 20cd5ed3..00000000 --- a/tests/test_docs_quickstart.py +++ /dev/null @@ -1,18 +0,0 @@ -from subprocess import Popen, PIPE, STDOUT -import os -from pathlib import Path -import runpy -from test_server_cli import MonitoredProcess - - -def run_quickstart_counter(): - this_file = Path(__file__) - repo = this_file.parents[1] - quickstart = repo / "docs" / "source" / "quickstart" / "counter.py" - runpy.run_path(quickstart) - - -def test_quickstart_counter(): - """Check we can create a server from the command line""" - p = MonitoredProcess(target=run_quickstart_counter) - p.run_monitored(terminate_outputs=["Application startup complete"]) \ No newline at end of file From dd6e92ce18f3bc86931c179b0b0803f63b3b3e44 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Thu, 6 Mar 2025 00:21:45 +0000 Subject: [PATCH 4/6] Lint/format example code --- docs/source/dependencies/example.py | 5 ++++- docs/source/quickstart/counter.py | 2 +- tests/test_docs.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/source/dependencies/example.py b/docs/source/dependencies/example.py index fab5eb43..75f7d267 100644 --- a/docs/source/dependencies/example.py +++ b/docs/source/dependencies/example.py @@ -6,6 +6,7 @@ MyThingDep = direct_thing_client_dependency(MyThing, "/mything/") + class TestThing(Thing): """A test thing with a counter property and a couple of actions""" @@ -14,10 +15,12 @@ def increment_counter(self, my_thing: MyThingDep) -> None: """Increment the counter on another thing""" my_thing.increment_counter() + server = ThingServer() server.add_thing(MyThing(), "/mything/") server.add_thing(TestThing(), "/testthing/") if __name__ == "__main__": import uvicorn - uvicorn.run(server.app, port=5000) \ No newline at end of file + + uvicorn.run(server.app, port=5000) diff --git a/docs/source/quickstart/counter.py b/docs/source/quickstart/counter.py index 12a27f2d..66d2f509 100644 --- a/docs/source/quickstart/counter.py +++ b/docs/source/quickstart/counter.py @@ -28,6 +28,7 @@ def slowly_increase_counter(self) -> None: model=int, initial_value=0, readonly=True, description="A pointless counter" ) + if __name__ == "__main__": from labthings_fastapi.server import ThingServer import uvicorn @@ -39,4 +40,3 @@ def slowly_increase_counter(self) -> None: # We run the server using `uvicorn`: uvicorn.run(server.app, port=5000) - diff --git a/tests/test_docs.py b/tests/test_docs.py index a00340a7..8cebbebd 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -1,5 +1,3 @@ -from subprocess import Popen, PIPE, STDOUT -import os from pathlib import Path from runpy import run_path from test_server_cli import MonitoredProcess @@ -11,6 +9,7 @@ repo = this_file.parents[1] docs = repo / "docs" / "source" + def run_quickstart_counter(): # A server is started in the `__name__ == "__main__" block` run_path(docs / "quickstart" / "counter.py") @@ -21,6 +20,7 @@ def test_quickstart_counter(): p = MonitoredProcess(target=run_quickstart_counter) p.run_monitored(terminate_outputs=["Application startup complete"]) + def test_dependency_example(): globals = run_path(docs / "dependencies" / "example.py", run_name="not_main") with TestClient(globals["server"].app) as client: From f9e90f388439899e3e2bd434c0134c6629a84b88 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Sat, 8 Mar 2025 01:50:11 +0000 Subject: [PATCH 5/6] Wrote a bash script to test the example This is not yet run in CI, but should check the example. --- docs/source/quickstart/quickstart.rst | 21 ++++++-- docs/source/quickstart/quickstart_example.sh | 14 ++++++ .../quickstart/test_quickstart_example.sh | 50 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 docs/source/quickstart/quickstart_example.sh create mode 100644 docs/source/quickstart/test_quickstart_example.sh diff --git a/docs/source/quickstart/quickstart.rst b/docs/source/quickstart/quickstart.rst index 02927c1f..c91fff52 100644 --- a/docs/source/quickstart/quickstart.rst +++ b/docs/source/quickstart/quickstart.rst @@ -1,11 +1,20 @@ Quick start =========== -You can install `labthings-fastapi` using `pip`. Create a virtual environment for you project, then install labthings with: +You can install `labthings-fastapi` using `pip`. We recommend you create a virtual environment, for example: -.. code-block:: bash - pip install labthings-fastapi[server] +.. literalinclude:: quickstart_example.sh + :language: bash + :start-after: BEGIN venv + :end-before: END venv + +then install labthings with: + +.. literalinclude:: quickstart_example.sh + :language: bash + :start-after: BEGIN install + :end-before: END install To define a simple example ``Thing``, paste the following into a python file, ``counter.py``: @@ -14,9 +23,11 @@ To define a simple example ``Thing``, paste the following into a python file, `` ``counter.py`` defines the ``TestThing`` class, and then runs a LabThings server in its ``__name__ == "__main__"`` block. This means we should be able to run the server with: -.. code-block:: bash - python counter.py +.. literalinclude:: quickstart_example.sh + :language: bash + :start-after: BEGIN serve + :end-before: END serve Visiting http://localhost:5000/counter/ will show the thing description, and you can interact with the actions and properties using the Swagger UI at http://localhost:5000/docs/. diff --git a/docs/source/quickstart/quickstart_example.sh b/docs/source/quickstart/quickstart_example.sh new file mode 100644 index 00000000..10abe7a4 --- /dev/null +++ b/docs/source/quickstart/quickstart_example.sh @@ -0,0 +1,14 @@ +echo "Setting up environemnt" +# BEGIN venv +python -m venv .venv --prompt labthings +source .venv/bin/activate # or .venv/Scripts/activate on Windows +# END venv +echo "Installing labthings-fastapi" +# BEGIN install +pip install labthings-fastapi[server] +# END install +echo "running example" +# BEGIN serve +python counter.py +# END serve +echo $! > example_server.pid diff --git a/docs/source/quickstart/test_quickstart_example.sh b/docs/source/quickstart/test_quickstart_example.sh new file mode 100644 index 00000000..b01d9ae3 --- /dev/null +++ b/docs/source/quickstart/test_quickstart_example.sh @@ -0,0 +1,50 @@ +set -e + +# Run the quickstart code in the background +bash ./quickstart_example.sh 2>&1 & +quickstart_example_pid=$! +echo "Spawned example with PID $quickstart_example_pid" + +# Wait for it to respond +# Loop until the command is successful or the maximum number of attempts is reached +ret=7 +attempt_num=0 +while [[ $ret == 7 ]] && [ $attempt_num -le 50 ]; do + # Execute the command + ret=0 + curl -sf -m 10 http://localhost:5000/counter/counter || ret=$? + if [[ $ret == 7 ]]; then + echo "Curl didn't connect on attempt $attempt_num" + + # Check the example process hasn't died + ps $quickstart_example_pid > /dev/null + if [[ $? != 0 ]]; then + echo "Child process (the server example) died without responding." + exit -1 + fi + + attempt_num=$(( attempt_num + 1 )) + sleep 1 + fi +done +echo "Final return value $ret on attempt $attempt_num" + +# Check the Python client code +echo "Running Python client code" +(. .venv/bin/activate && python counter_client.py) + + +# Get the spawned server's PID +children=$(ps -o pid= --ppid "$quickstart_example_pid") +kill $children +echo "Killed spawned processes: $children" + +wait + +if [[ $ret == 0 ]]; then + echo "Success" + exit 0 +else + echo "Curl returned $ret, likely something went wrong." + exit -1 +fi \ No newline at end of file From eba356c1e035c4c2a74e85fd50acb5052f646741 Mon Sep 17 00:00:00 2001 From: Richard Bowman Date: Thu, 13 Mar 2025 00:12:27 +0000 Subject: [PATCH 6/6] Add test to CI and improve portability I fixed a few path gotchas and added the test so it runs with unpinned dependencies. --- .github/workflows/test.yml | 4 ++ .../quickstart/test_quickstart_example.sh | 47 ++++++++++++------- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55144b7c..019b532a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -97,6 +97,10 @@ jobs: if: success() || failure() run: pytest --cov=src --cov-report=lcov + - name: Check quickstart, including installation + if: success() || failure() + run: bash ./docs/source/quickstart/test_quickstart_example.sh + coverage: runs-on: ubuntu-latest needs: [base_coverage, test] diff --git a/docs/source/quickstart/test_quickstart_example.sh b/docs/source/quickstart/test_quickstart_example.sh index b01d9ae3..740fdb99 100644 --- a/docs/source/quickstart/test_quickstart_example.sh +++ b/docs/source/quickstart/test_quickstart_example.sh @@ -1,15 +1,31 @@ -set -e - +# cd to this directory +cd "${BASH_SOURCE%/*}/" +rm -rf .venv +# Override the virtual environment so we use the package +# from this repo, not from pypi +python -m venv .venv +source .venv/bin/activate +pip install ../../.. # Run the quickstart code in the background -bash ./quickstart_example.sh 2>&1 & +bash "quickstart_example.sh" 2>&1 & quickstart_example_pid=$! echo "Spawned example with PID $quickstart_example_pid" +function killserver { + # Stop the server that we spawned. + children=$(ps -o pid= --ppid "$quickstart_example_pid") + kill $children + echo "Killed spawned processes: $children" + + wait +} +trap killserver EXIT + # Wait for it to respond # Loop until the command is successful or the maximum number of attempts is reached ret=7 attempt_num=0 -while [[ $ret == 7 ]] && [ $attempt_num -le 50 ]; do +while [[ $ret == 7 ]]; do # && [ $attempt_num -le 50 ] # Execute the command ret=0 curl -sf -m 10 http://localhost:5000/counter/counter || ret=$? @@ -29,22 +45,17 @@ while [[ $ret == 7 ]] && [ $attempt_num -le 50 ]; do done echo "Final return value $ret on attempt $attempt_num" -# Check the Python client code -echo "Running Python client code" -(. .venv/bin/activate && python counter_client.py) - - -# Get the spawned server's PID -children=$(ps -o pid= --ppid "$quickstart_example_pid") -kill $children -echo "Killed spawned processes: $children" - -wait - if [[ $ret == 0 ]]; then echo "Success" - exit 0 else echo "Curl returned $ret, likely something went wrong." exit -1 -fi \ No newline at end of file +fi + +# Check the Python client code +echo "Running Python client code" +(source .venv/bin/activate && python counter_client.py) +if [[ $? != 0 ]]; then + echo "Python client code did not run OK." + exit -1 +fi