فهرست منبع

Merge branch 'release-0.0.1'

Arkadiusz Ryś 2 سال پیش
والد
کامیت
9130978b20
10فایلهای تغییر یافته به همراه328 افزوده شده و 12 حذف شده
  1. 4 0
      HISTORY.rst
  2. 50 0
      README.md
  3. 1 0
      data/mock_requirements.txt
  4. 1 0
      docs/templates/pyproject.toml
  5. 1 1
      mocka/__init__.py
  6. 42 2
      mocka/router.py
  7. 56 0
      pyproject.toml
  8. 10 9
      requirements.txt
  9. 114 0
      tasks.py
  10. 49 0
      tests/test_mock.py

+ 4 - 0
HISTORY.rst

@@ -2,6 +2,10 @@
 History
 =======
 
+0.0.1 (2023-09-22)
+------------------
+* Initial version of data exchange specification implemented.
+
 0.0.0 (yyyy-mm-dd)
 ------------------
 * No history yet.

+ 50 - 0
README.md

@@ -1,3 +1,53 @@
 # Mocka
 
 Mock Activity Endpoint.
+Automated activity enactments must be HTTP calls where users must specify the endpoint in the FTG's Transformation, and the timeout in the PM's Activity.
+As an activity gets triggered it requires knowledge of the control and artefact flow.
+This endpoint expects this context in the form of a json body following a highly specific format.
+
+    {
+        "ctrl": "<name of control port going into the activity>",
+        "input": {
+            "<name of artefact port going into the activity>": {
+                "type": "<inline|reference>",
+                "content": <the contents of the artefact going into this port in case it's inlined>,
+                "encoding": "<the encoding of said artefact>"
+            }
+        }
+    }
+
+Anything between `<` and `>` is to be filled in by the requester.
+Only `inline` type artefacts are supported at the moment.
+
+Barring any errors, mocka will retaliate with a json response in the same gist.
+
+
+    {
+        "ctrl": "<the name of the control port which should be taken out of the activity>",
+        "output": {
+            "<the name of the artefact which got generated>": {
+                "type": "<inline|reference>",
+                "content": <the contents of the generated artefact>,
+                "encoding": "<the encoding of said artefact>"
+            }
+        }
+    }
+
+This response can contain multiple artefacts.
+It can even be an error stating a timeout or broken input.
+The expectation is that the Workflow Enactment Engine will deal with the fallout.
+
+> NOTE: A call to store the artefact in the backend will be needed when the type of the output artefact is set to reference.
+
+## Deploying a mock activity endpoint
+
+> NOTE: Be sure to have ´libmagic´ installed. It is used to figure out the encoding of the artefacts being sent on their journey.
+
+Drop into a shell and sing the magic incantation `python3 -m mocka`.
+This will leave you haunted with an endpoint lingering on port `7999` by default.
+From this point onward you're on your own and can perform any request you want.
+
+## Wishful thinking
+
+It would be nice if this endpoint would support gradual progress updates.
+This would also require the Workflow Enactment Engine to do the same.

+ 1 - 0
data/mock_requirements.txt

@@ -0,0 +1 @@
+1. Don't be evil!

+ 1 - 0
docs/templates/pyproject.toml

@@ -6,6 +6,7 @@ build-backend = "flit_core.buildapi"
 name = "mocka"
 authors = [
     {name = "Arkadiusz Michał Ryś", email = "Arkadiusz.Michal.Rys@gmail.com"},
+    {name = "Lucas Albertins de Lima", email = "lucas.albertinsdelima@uantwerpen.be"},
 ]
 readme = "README.md"
 requires-python = ">=3.9"

+ 1 - 1
mocka/__init__.py

@@ -1,3 +1,3 @@
 """Mock Activity Endpoint."""
-__version__ = "0.0.0"
+__version__ = "0.0.1"
 __version_info__ = tuple((int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split(".")))

+ 42 - 2
mocka/router.py

@@ -1,6 +1,12 @@
+# https://fastapi.tiangolo.com/tutorial/body-multiple-params/#singular-values-in-body
+from pathlib import Path
 from typing import Any
+import magic
+import arklog
 from fastapi import APIRouter, Query, Request, Response
 from fastapi.responses import JSONResponse
+import time
+
 
 class MockRouter(APIRouter):
     """"""
@@ -14,5 +20,39 @@ class MockRouter(APIRouter):
 
         @self.get("/")
         async def root_mock(request: Request, query: str | None = Query(None)) -> Response:
-            """"""
-            return JSONResponse(status_code=400, content={"message": "Not Implemented!"})
+            """Example request response for a simulated activity."""
+            if query:
+                match query.lower():
+                    case "error":
+                        return JSONResponse(status_code=400, content={"ctrl": "error"})
+                    case "get":
+                        return JSONResponse(status_code=200, content={"ctrl": "ok"})
+
+            body = await request.json()
+            control = body.get("ctrl")
+            arklog.debug(control)
+            content = body.get("input").get("din").get("content")
+            file_path = Path(__file__).parent.parent / Path("data/mock_requirements.txt")
+            requirements = file_path.read_text() + "\n\nChecked!"
+            mime = magic.Magic(mime=True).from_file(file_path)
+            #time.sleep(5)
+            assert content + "\n\nChecked!" == requirements
+            return JSONResponse(status_code=200, content={
+                "ctrl": "ok",
+                "output": {
+                    "dout": {
+                        "type": "inline",
+                        "content": requirements,
+                        "name": file_path.name,
+                        "encoding": mime
+                    }
+                }
+            })
+
+        @self.put("/")
+        async def root_put_mock(request: Request, query: str | None = Query(None)) -> Response:
+            return await root_mock(request, query)
+
+        @self.post("/")
+        async def root_post_mock(request: Request, query: str | None = Query(None)) -> Response:
+            return await root_mock(request, query)

+ 56 - 0
pyproject.toml

@@ -0,0 +1,56 @@
+[build-system]
+requires = ["flit_core >=3.2,<4"]
+build-backend = "flit_core.buildapi"
+
+[project]
+name = "mocka"
+authors = [
+    {name = "Arkadiusz Michał Ryś", email = "Arkadiusz.Michal.Rys@gmail.com"},
+    {name = "Lucas Albertins de Lima", email = "lucas.albertinsdelima@uantwerpen.be"},
+]
+readme = "README.md"
+requires-python = ">=3.9"
+classifiers = [
+    "License :: OSI Approved :: MIT License",
+    "Programming Language :: Python :: 3",
+    "Development Status :: 2 - Pre-Alpha",
+    "Intended Audience :: Developers",
+    "Natural Language :: English",
+]
+dynamic = ["version", "description"]
+license = {file = "LICENSE"}
+keywords = ["mocka"]
+dependencies = [
+    "toml~=0.10.2",
+    "arklog~=0.5.1",
+    "dacite~=1.8.1",
+    "fastapi~=0.103.1",
+    "pyarrow~=13.0.0",
+    "requests~=2.31.0",
+    "starlette~=0.27.0",
+    "python-magic~=0.4.27",
+    "uvicorn[standard]~=0.23.2",
+]
+
+[project.optional-dependencies]
+test = [
+    "httpx~=0.25.0",
+    "pytest~=7.4.0",
+]
+doc = [
+    "sphinx~=7.2.6",
+]
+dev = [
+    "tox~=4.11.3",
+    "pip~=23.2.1",
+    "flit~=3.9.0",
+    "twine~=4.0.2",
+    "numpy~=1.26.0",
+    "invoke~=2.2.0",
+    "jinja2~=3.1.2",
+    "flake8~=6.1.0",
+    "coverage~=7.3.1",
+]
+
+[project.urls]
+source = "https://git.rys.one/dtdesign/mocka"

+ 10 - 9
requirements.txt

@@ -1,24 +1,25 @@
-# SpEndPoint
+# Mocka
 toml              ~= 0.10.2
 arklog            ~= 0.5.1
 dacite            ~= 1.8.1
-fastapi           ~= 0.101.0
-pyarrow           ~= 12.0.1
+fastapi           ~= 0.103.1
+pyarrow           ~= 13.0.0
 requests          ~= 2.31.0
-starlette         ~= 0.31.0
+starlette         ~= 0.27.0
 python-magic      ~= 0.4.27
 uvicorn[standard] ~= 0.23.2
 # Test
-pytest        ~= 7.4.0
+httpx  ~= 0.25.0
+pytest ~= 7.4.0
 # Doc
-sphinx ~= 7.1.2
+sphinx ~= 7.2.6
 # Dev
-tox      ~= 4.6.4
+tox      ~= 4.11.3
 pip      ~= 23.2.1
 flit     ~= 3.9.0
 twine    ~= 4.0.2
-numpy    ~= 1.25.2
+numpy    ~= 1.26.0
 invoke   ~= 2.2.0
 jinja2   ~= 3.1.2
 flake8   ~= 6.1.0
-coverage ~= 7.2.7
+coverage ~= 7.3.1

+ 114 - 0
tasks.py

@@ -0,0 +1,114 @@
+from pathlib import Path
+
+from invoke import task
+from jinja2 import Template
+
+system = "mocka"  # Directory name of the project
+
+@task
+def lint(c):
+    """"""
+    c.run(f"python3 -m black {system}")
+    c.run(f"python3 -m pylint {system}")
+
+
+@task(name="docs")
+def documentation(c):
+    """Build the documentation."""
+    c.run("python3 -m sphinx docs docs/build/html")
+
+
+@task(name="preview", aliases=("rst",))
+def preview(c):
+    """Show a preview of the README file."""
+    import sys
+    rst_view = c.run(f"restview --listen=8888 --browser --pypi-strict README.rst", asynchronous=True, out_stream=sys.stdout)
+    print("Listening on http://localhost:8888/")
+    rst_view.join()
+
+
+@task
+def clean(c):
+    """Remove all artefacts."""
+    patterns = ["build", "docs/build"]
+    for pattern in patterns:
+        c.run(f"rm -rf {pattern}")
+
+
+@task
+def test(c):
+    """Run all tests under the tests directory."""
+    c.run("python3 -m unittest discover tests 'test_*' -v")
+
+
+@task
+def coverage(c):
+    """Run coverage from the 'tests' directory."""
+    c.run("coverage run --source . -m unittest discover tests 'test_*' -v")
+    c.run("coverage html")
+
+
+@task
+def minimum(c):
+    """Check the minimum required python version for the project."""
+    c.run("vermin --no-parse-comments .")
+
+
+@task(name="migrate")
+def migrate_requirements(c):
+    """Copy requirements from the requirements.txt file to pyproject.toml."""
+    lines = Path("requirements.txt").read_text().split("\n")
+    current = system.lower().replace("-", "_")
+    requirements = {current: [], "test": [], "doc": [], "graphical": [], "dev": []}
+    for line in lines:
+        if line.startswith("#"):
+            candidate = line[1:].lower().strip().replace(" ", "_").replace("-", "_")
+            if candidate in requirements.keys():
+                current = candidate
+                continue
+        if line.strip() == "" or ("=" in line and "#" in line):
+            continue
+        requirements[current].append("".join(line.split()))
+    template = Template(Path("docs/templates/pyproject.toml").read_text())
+    Path("pyproject.toml").write_text(template.render(requirements=requirements))
+
+
+@task
+def release(c, version):
+    """"""
+    if version not in ["minor", "major", "patch"]:
+        print("Version can be either major, minor or patch.")
+        return
+
+    import importlib
+    current_module = importlib.import_module(system)
+    __version_info__ = current_module.__version_info__
+    __version__ = current_module.__version__
+    _major, _minor, _patch = __version_info__
+
+    if version == "patch":
+        _patch = _patch + 1
+    elif version == "minor":
+        _minor = _minor + 1
+        _patch = 0
+    elif version == "major":
+        _major = _major + 1
+        _minor = 0
+        _patch = 0
+
+    c.run(f"git checkout -b release-{_major}.{_minor}.{_patch} dev")
+    c.run(f"sed -i 's/{__version__}/{_major}.{_minor}.{_patch}/g' {system}/__init__.py")
+    print(f"Update the readme for version {_major}.{_minor}.{_patch}.")
+    input("Press enter when ready.")
+    c.run(f"git add -u")
+    c.run(f'git commit -m "Update changelog version {_major}.{_minor}.{_patch}"')
+    c.run(f"git push --set-upstream origin release-{_major}.{_minor}.{_patch}")
+    c.run(f"git checkout main")
+    c.run(f"git merge --no-ff release-{_major}.{_minor}.{_patch}")
+    c.run(f'git tag -a {_major}.{_minor}.{_patch} -m "Release {_major}.{_minor}.{_patch}"')
+    c.run(f"git push")
+    c.run(f"git checkout dev")
+    c.run(f"git merge --no-ff release-{_major}.{_minor}.{_patch}")
+    c.run(f"git push")
+    c.run(f"git branch -d release-{_major}.{_minor}.{_patch}")
+    c.run(f"git push origin --tags")

+ 49 - 0
tests/test_mock.py

@@ -0,0 +1,49 @@
+from pathlib import Path
+
+from fastapi.testclient import TestClient
+from mocka.main import get_application
+from mocka.configuration import Configuration, Server
+
+
+client = TestClient(get_application(Configuration(Server("localhost", 8585))))
+
+
+# def test_read_main():
+#     response = client.get("/", headers={})
+#     assert response.status_code == 201
+#     assert response.json() == {
+#                 "port": "ok",
+#                 "output": {
+#                     "artefact_1": "<uri>",
+#                     "artefact_2": "<uri>",
+#                 }
+#             }
+
+def test_post_main():
+    file_path = Path(__file__).parent.parent / Path("data/mock_requirements.txt")
+    requirements = file_path.read_text()
+    mock_input = {
+        "ctrl": "cin",
+        "input": {
+            "din": {
+                "type": "inline",
+                "content": requirements,
+                "encoding": "text/plain"
+            }
+        }
+    }
+    response = client.post("/", json=mock_input)
+    assert response.status_code == 200
+    assert response.json() == {
+        "ctrl": "ok",
+        "output": {
+            "dout": {
+                "type": "inline",
+                "content": requirements + "\n\nChecked!",
+                "name": file_path.name,
+                "encoding": "text/plain"
+            }
+        }
+    }
+
+