ocelescope-backend 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ocelescope_backend-0.1.0/PKG-INFO +58 -0
- ocelescope_backend-0.1.0/README.md +41 -0
- ocelescope_backend-0.1.0/pyproject.toml +31 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/__init__.py +1 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/__init__.py +1 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/dependencies.py +58 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/__init__.py +0 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/config.py +45 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/discovery/__init__.py +15 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/discovery/bootstrap.py +12 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/discovery/registry.py +137 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/docs.py +58 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/exceptions.py +42 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/logger.py +32 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/__init__.py +0 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/base.py +33 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/discovery.py +38 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/events.py +13 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/ocel.py +141 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/plugin.py +55 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/relations.py +111 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/resource.py +27 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/model/response.py +20 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/ocel/__init__.py +0 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/ocel/default_ocel.py +180 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/registrar.py +18 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/registry/__init__.py +3 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/registry/extension.py +47 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/registry/plugin.py +70 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/registry/registry_manager.py +188 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/registry/resource.py +89 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/session.py +177 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/tasks/__init__.py +0 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/tasks/base.py +80 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/tasks/discovery_task.py +161 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/tasks/plugin.py +187 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/tasks/system.py +170 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/tasks/system_tasks.py +192 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/__init__.py +0 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/cache.py +188 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/dynamic_import.py +56 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/hashing.py +41 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/misc.py +227 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/pandas.py +480 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/plugin_result.py +54 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/relations.py +119 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/spool_upload.py +38 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/util/types.py +13 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/internal/utils.py +82 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/middleware.py +29 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/modules/__init__.py +3 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/modules/base.py +27 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/modules/loader.py +59 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/__init__.py +16 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/discovery.py +158 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/ocels.py +533 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/plugins.py +267 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/resources.py +126 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/session.py +73 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/routes/tasks.py +48 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/app/sse_manager.py +96 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/cli.py +79 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/factory.py +80 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/main.py +3 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/py.typed +0 -0
- ocelescope_backend-0.1.0/src/ocelescope_backend/version.py +1 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ocelescope-backend
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ocelescope backend host application
|
|
5
|
+
Requires-Dist: pm4py~=2.7.22
|
|
6
|
+
Requires-Dist: pandas>=2.3.1
|
|
7
|
+
Requires-Dist: numpy>=2.3.2
|
|
8
|
+
Requires-Dist: cachetools>=6.1.0
|
|
9
|
+
Requires-Dist: pydantic>=2.11.7
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.2.1,<3
|
|
11
|
+
Requires-Dist: fastapi[standard]>=0.136.0
|
|
12
|
+
Requires-Dist: uvicorn>=0.49,<0.50
|
|
13
|
+
Requires-Dist: ocelescope[plugin]>=0.1.0,<0.2.0
|
|
14
|
+
Requires-Dist: orjson>=3.11.3
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# ocelescope-backend
|
|
19
|
+
|
|
20
|
+
The Ocelescope backend host application. It exposes the FastAPI server that
|
|
21
|
+
serves OCELs, resources and modules, and provides the `ocelescope-backend`
|
|
22
|
+
command-line interface.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install ocelescope-backend
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
Start the development server:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
ocelescope-backend serve
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Other commands (e.g. generating an OpenAPI schema for a module) are available
|
|
39
|
+
via:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ocelescope-backend --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Modules
|
|
46
|
+
|
|
47
|
+
The backend discovers modules through the `ocelescope_backend.modules` entry
|
|
48
|
+
point group. Installing a module package (for example
|
|
49
|
+
[`ocelescope-module-ocelot`](../modules/ocelescope-module-ocelot)) makes it
|
|
50
|
+
available to the host automatically.
|
|
51
|
+
|
|
52
|
+
## About
|
|
53
|
+
|
|
54
|
+
Part of [Ocelescope](https://github.com/promi4s/ocelescope), a framework for
|
|
55
|
+
working with Object-Centric Event Logs developed at the Chair of Process and
|
|
56
|
+
Data Science (PADS), RWTH Aachen University.
|
|
57
|
+
|
|
58
|
+
📖 Documentation: <https://www.ocelescope.org>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# ocelescope-backend
|
|
2
|
+
|
|
3
|
+
The Ocelescope backend host application. It exposes the FastAPI server that
|
|
4
|
+
serves OCELs, resources and modules, and provides the `ocelescope-backend`
|
|
5
|
+
command-line interface.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install ocelescope-backend
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
Start the development server:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ocelescope-backend serve
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Other commands (e.g. generating an OpenAPI schema for a module) are available
|
|
22
|
+
via:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
ocelescope-backend --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Modules
|
|
29
|
+
|
|
30
|
+
The backend discovers modules through the `ocelescope_backend.modules` entry
|
|
31
|
+
point group. Installing a module package (for example
|
|
32
|
+
[`ocelescope-module-ocelot`](../modules/ocelescope-module-ocelot)) makes it
|
|
33
|
+
available to the host automatically.
|
|
34
|
+
|
|
35
|
+
## About
|
|
36
|
+
|
|
37
|
+
Part of [Ocelescope](https://github.com/promi4s/ocelescope), a framework for
|
|
38
|
+
working with Object-Centric Event Logs developed at the Chair of Process and
|
|
39
|
+
Data Science (PADS), RWTH Aachen University.
|
|
40
|
+
|
|
41
|
+
📖 Documentation: <https://www.ocelescope.org>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ocelescope-backend"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Ocelescope backend host application"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"pm4py~=2.7.22",
|
|
9
|
+
"pandas>=2.3.1",
|
|
10
|
+
"numpy>=2.3.2",
|
|
11
|
+
"cachetools>=6.1.0",
|
|
12
|
+
"pydantic>=2.11.7",
|
|
13
|
+
"pydantic-settings>=2.2.1,<3",
|
|
14
|
+
"fastapi[standard]>=0.136.0",
|
|
15
|
+
"uvicorn>=0.49,<0.50",
|
|
16
|
+
"ocelescope[plugin]>=0.1.0,<0.2.0",
|
|
17
|
+
"orjson>=3.11.3",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.scripts]
|
|
21
|
+
ocelescope-backend = "ocelescope_backend.cli:app"
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.11.20,<0.12.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
package = true
|
|
29
|
+
|
|
30
|
+
[tool.uv.sources]
|
|
31
|
+
ocelescope = { workspace = true }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
|
+
|
|
5
|
+
from fastapi import Depends, HTTPException, Request
|
|
6
|
+
|
|
7
|
+
from ocelescope import OCEL
|
|
8
|
+
from ocelescope_backend.app.internal.exceptions import NotFound
|
|
9
|
+
from ocelescope_backend.app.internal.session import Session
|
|
10
|
+
from ocelescope_backend.app.internal.tasks.plugin import PluginTask
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_session(request: Request) -> Session:
|
|
14
|
+
session = getattr(request.state, "session", None)
|
|
15
|
+
if not session:
|
|
16
|
+
raise HTTPException(status_code=500, detail="Session middleware not set")
|
|
17
|
+
return session
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ApiSession = Annotated[Session, Depends(get_session)]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_ocel(
|
|
24
|
+
session: ApiSession,
|
|
25
|
+
ocel_id: str | None = None,
|
|
26
|
+
ocel_version: Literal["original", "filtered"] | None = "filtered",
|
|
27
|
+
):
|
|
28
|
+
# Used so the generated react queries don't required them so they can be injected from the session storage
|
|
29
|
+
if not ocel_id:
|
|
30
|
+
raise HTTPException(status_code=500, detail="Ocel id is required")
|
|
31
|
+
try:
|
|
32
|
+
return session.get_ocel(
|
|
33
|
+
ocel_id, use_original=False if ocel_version != "original" else True
|
|
34
|
+
)
|
|
35
|
+
except NotFound:
|
|
36
|
+
raise HTTPException(status_code=404, detail="OCEL not found")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
ApiOcel = Annotated[OCEL, Depends(get_ocel)]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_plugin_task(
|
|
43
|
+
session: ApiSession, plugin_id: str, method_name: str, task_id: str
|
|
44
|
+
) -> PluginTask:
|
|
45
|
+
plugin_task = session.get_task(task_id)
|
|
46
|
+
|
|
47
|
+
if (
|
|
48
|
+
plugin_task is None
|
|
49
|
+
or not isinstance(plugin_task, PluginTask)
|
|
50
|
+
or plugin_task.plugin_id != plugin_id
|
|
51
|
+
or plugin_task.method_name != method_name
|
|
52
|
+
):
|
|
53
|
+
raise NotFound("Task could not be found")
|
|
54
|
+
|
|
55
|
+
return plugin_task
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
ApiPluginTask = Annotated[PluginTask, Depends(get_plugin_task)]
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
from pydantic import DirectoryPath, Field
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
This file contains a Config class defining all environment parameters, including types, default values and descriptions.
|
|
8
|
+
.env.example (and the structure of .env) should be generated using the `export_settings_as_dotenv` util function.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OceanConfig(BaseSettings):
|
|
13
|
+
FRONTEND_URL: str = Field(
|
|
14
|
+
default="http://frontend:3000",
|
|
15
|
+
description="The frontend URL, relevant for CORS settings",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
SESSION_ID_HEADER: str = Field(
|
|
19
|
+
default="ocelescope-session-id",
|
|
20
|
+
description="The HTTP header name containing the session ID.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
EXPOSE_ERROR_DETAILS: bool = Field(
|
|
24
|
+
default=False,
|
|
25
|
+
description="When set to True, passes details of internal errors via the API. Always set to False in production environment.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DATA_DIR: DirectoryPath | None = Field(
|
|
29
|
+
default=None,
|
|
30
|
+
description="Path to the data directory, relative to `main.py`",
|
|
31
|
+
)
|
|
32
|
+
PLUGIN_DIR: DirectoryPath | None = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="Path to the directory, where plugins are stored",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
MODE: Literal["production", "development"] | None = Field(
|
|
38
|
+
default="development", description="The mode in which the backend is running"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
class Config:
|
|
42
|
+
env_file = ".env"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
config = OceanConfig() # type: ignore
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ocelescope_backend.app.internal.discovery.registry import (
|
|
2
|
+
DiscoveryMethodGroup,
|
|
3
|
+
DiscoveryMethodInfo,
|
|
4
|
+
DiscoveryRegistry,
|
|
5
|
+
discovery_registry,
|
|
6
|
+
register_discovery_methods_from_module,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DiscoveryMethodGroup",
|
|
11
|
+
"DiscoveryMethodInfo",
|
|
12
|
+
"DiscoveryRegistry",
|
|
13
|
+
"discovery_registry",
|
|
14
|
+
"register_discovery_methods_from_module",
|
|
15
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Register discovery methods shipped with the ocelescope SDK."""
|
|
2
|
+
|
|
3
|
+
from ocelescope.discovery import algorithms
|
|
4
|
+
from ocelescope_backend.app.internal.discovery.registry import (
|
|
5
|
+
register_discovery_methods_from_module,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register_builtin_discovery_methods() -> None:
|
|
10
|
+
# The SDK's `algorithms` package re-exports each built-in @discovery_method
|
|
11
|
+
# function; scanning the package module is enough to pick them all up.
|
|
12
|
+
register_discovery_methods_from_module(algorithms)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
from typing import Any, Callable, get_type_hints
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from ocelescope.discovery import DiscoveryMethodMeta
|
|
8
|
+
from ocelescope.ocel import OCEL
|
|
9
|
+
from ocelescope.resource import Resource
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, create_model
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _snake_to_camel(value: str) -> str:
|
|
14
|
+
first, *rest = value.split("_")
|
|
15
|
+
return first + "".join(part.capitalize() for part in rest)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_parameter_model(func: Callable[..., Resource]) -> type[BaseModel]:
|
|
19
|
+
"""Build a pydantic model that describes the function's non-OCEL parameters."""
|
|
20
|
+
sig = inspect.signature(func)
|
|
21
|
+
hints = get_type_hints(func, include_extras=True)
|
|
22
|
+
|
|
23
|
+
fields: dict[str, tuple[Any, Any]] = {}
|
|
24
|
+
for name, param in sig.parameters.items():
|
|
25
|
+
if name in ("ocel", "self"):
|
|
26
|
+
continue
|
|
27
|
+
annotation = hints.get(name, param.annotation)
|
|
28
|
+
default = param.default if param.default is not inspect.Parameter.empty else ...
|
|
29
|
+
fields[name] = (annotation, default)
|
|
30
|
+
|
|
31
|
+
config = ConfigDict(
|
|
32
|
+
alias_generator=_snake_to_camel,
|
|
33
|
+
populate_by_name=True,
|
|
34
|
+
arbitrary_types_allowed=True,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
model_name = f"{func.__name__.title().replace('_', '')}Parameters" # ty: ignore[unresolved-attribute]
|
|
38
|
+
return create_model(model_name, __config__=config, **fields) # type: ignore[call-overload] # ty: ignore[no-matching-overload]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DiscoveryMethodInfo:
|
|
43
|
+
method_id: str
|
|
44
|
+
name: str
|
|
45
|
+
description: str | None
|
|
46
|
+
func: Callable[..., Resource]
|
|
47
|
+
parameter_type: type[BaseModel]
|
|
48
|
+
resource_type: type[Resource]
|
|
49
|
+
|
|
50
|
+
def parameters_schema(self) -> dict[str, Any]:
|
|
51
|
+
return self.parameter_type.model_json_schema(by_alias=True)
|
|
52
|
+
|
|
53
|
+
def parse_parameters(self, data: dict[str, Any]) -> BaseModel:
|
|
54
|
+
return self.parameter_type.model_validate(data)
|
|
55
|
+
|
|
56
|
+
def dump_parameters(self, params: BaseModel) -> dict[str, Any]:
|
|
57
|
+
return params.model_dump(by_alias=False)
|
|
58
|
+
|
|
59
|
+
def run(self, *, ocel: OCEL, parameters: BaseModel) -> Resource:
|
|
60
|
+
return self.func(ocel=ocel, **parameters.model_dump(by_alias=False))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class DiscoveryMethodGroup:
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
name: str,
|
|
67
|
+
variants: list[DiscoveryMethodInfo],
|
|
68
|
+
) -> None:
|
|
69
|
+
self.name = name
|
|
70
|
+
self.variants = variants
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DiscoveryRegistry:
|
|
74
|
+
def __init__(self) -> None:
|
|
75
|
+
self._methods: dict[str, DiscoveryMethodInfo] = {}
|
|
76
|
+
|
|
77
|
+
def register(self, meta: DiscoveryMethodMeta) -> DiscoveryMethodInfo:
|
|
78
|
+
parameter_type = _build_parameter_model(meta.func)
|
|
79
|
+
|
|
80
|
+
for existing in self._methods.values():
|
|
81
|
+
if (
|
|
82
|
+
existing.name == meta.name
|
|
83
|
+
and existing.resource_type is meta.resource_type
|
|
84
|
+
):
|
|
85
|
+
raise TypeError(
|
|
86
|
+
f"Discovery method '{meta.name}' is already registered for resource "
|
|
87
|
+
f"type '{meta.resource_type.get_type()}'."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
info = DiscoveryMethodInfo(
|
|
91
|
+
method_id=str(uuid4()),
|
|
92
|
+
name=meta.name,
|
|
93
|
+
description=meta.description,
|
|
94
|
+
func=meta.func,
|
|
95
|
+
parameter_type=parameter_type,
|
|
96
|
+
resource_type=meta.resource_type,
|
|
97
|
+
)
|
|
98
|
+
self._methods[info.method_id] = info
|
|
99
|
+
return info
|
|
100
|
+
|
|
101
|
+
def unregister_by_func(self, func: Callable[..., Resource]) -> None:
|
|
102
|
+
for method_id, info in list(self._methods.items()):
|
|
103
|
+
if info.func is func:
|
|
104
|
+
del self._methods[method_id]
|
|
105
|
+
|
|
106
|
+
def get(self, method_id: str) -> DiscoveryMethodInfo:
|
|
107
|
+
info = self._methods.get(method_id)
|
|
108
|
+
if info is None:
|
|
109
|
+
raise KeyError(f"Discovery method '{method_id}' is not registered")
|
|
110
|
+
return info
|
|
111
|
+
|
|
112
|
+
def list_groups(self) -> list[DiscoveryMethodGroup]:
|
|
113
|
+
groups: dict[str, DiscoveryMethodGroup] = {}
|
|
114
|
+
for info in self._methods.values():
|
|
115
|
+
if info.name not in groups:
|
|
116
|
+
groups[info.name] = DiscoveryMethodGroup(
|
|
117
|
+
name=info.name,
|
|
118
|
+
variants=[info],
|
|
119
|
+
)
|
|
120
|
+
else:
|
|
121
|
+
groups[info.name].variants.append(info)
|
|
122
|
+
return list(groups.values())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
discovery_registry = DiscoveryRegistry()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def register_discovery_methods_from_module(
|
|
129
|
+
module: ModuleType,
|
|
130
|
+
) -> list[DiscoveryMethodInfo]:
|
|
131
|
+
"""Scan a module for functions tagged with `__discovery_meta__` and register them."""
|
|
132
|
+
registered: list[DiscoveryMethodInfo] = []
|
|
133
|
+
for value in vars(module).values():
|
|
134
|
+
meta = getattr(value, "__discovery_meta__", None)
|
|
135
|
+
if isinstance(meta, DiscoveryMethodMeta):
|
|
136
|
+
registered.append(discovery_registry.register(meta))
|
|
137
|
+
return registered
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Modifications to the SwaggerUI interface generated by FastAPI
|
|
2
|
+
|
|
3
|
+
# Hide curl commands
|
|
4
|
+
# source: https://github.com/tiangolo/fastapi/discussions/3853#discussioncomment-1388209
|
|
5
|
+
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from starlette.responses import HTMLResponse
|
|
10
|
+
|
|
11
|
+
from ocelescope_backend.app.internal.config import config
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_rapidoc_html(
|
|
15
|
+
*,
|
|
16
|
+
openapi_url: str,
|
|
17
|
+
title: str,
|
|
18
|
+
rapidoc_js_url: str = "https://unpkg.com/rapidoc/dist/rapidoc-min.js",
|
|
19
|
+
rapidoc_favicon_url: str = "https://rapidocweb.com/images/logo.png",
|
|
20
|
+
):
|
|
21
|
+
return HTMLResponse(
|
|
22
|
+
f"""
|
|
23
|
+
<!doctype html> <!-- Important: must specify -->
|
|
24
|
+
<html>
|
|
25
|
+
<head>
|
|
26
|
+
<meta charset="utf-8"> <!-- Important: rapi-doc uses utf8 characters -->
|
|
27
|
+
<script type="module" src="{rapidoc_js_url}"></script>
|
|
28
|
+
<title>{title}</title>
|
|
29
|
+
<link rel="shortcut icon" href="{rapidoc_favicon_url}">
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<rapi-doc
|
|
33
|
+
spec-url="{openapi_url}"
|
|
34
|
+
layout="row"
|
|
35
|
+
render-style="view"
|
|
36
|
+
allow-server-selection="false"
|
|
37
|
+
show-header="false"
|
|
38
|
+
show-components="true"
|
|
39
|
+
api-key-name="{config.SESSION_ID_HEADER}"
|
|
40
|
+
api-key-location="header"
|
|
41
|
+
api-key-value="-"
|
|
42
|
+
></rapi-doc>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
"""
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def init_custom_docs(app: FastAPI):
|
|
50
|
+
assert app.openapi_url is not None
|
|
51
|
+
|
|
52
|
+
@app.get("/docs", include_in_schema=False)
|
|
53
|
+
async def docs(ui: Literal["swaggerui", "rapidoc"] = "rapidoc", curl: bool = True):
|
|
54
|
+
if ui == "rapidoc":
|
|
55
|
+
return get_rapidoc_html(
|
|
56
|
+
openapi_url=app.openapi_url or "/",
|
|
57
|
+
title=app.title + " - rapidoc",
|
|
58
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
from starlette.status import (
|
|
3
|
+
HTTP_400_BAD_REQUEST,
|
|
4
|
+
HTTP_401_UNAUTHORIZED,
|
|
5
|
+
HTTP_402_PAYMENT_REQUIRED,
|
|
6
|
+
HTTP_403_FORBIDDEN,
|
|
7
|
+
HTTP_404_NOT_FOUND,
|
|
8
|
+
HTTP_405_METHOD_NOT_ALLOWED,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BadRequest(HTTPException):
|
|
13
|
+
def __init__(self, detail: str | None = None):
|
|
14
|
+
super().__init__(status_code=HTTP_400_BAD_REQUEST, detail=detail)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Unauthorized(HTTPException):
|
|
18
|
+
def __init__(self, detail: str | None = None):
|
|
19
|
+
super().__init__(status_code=HTTP_401_UNAUTHORIZED, detail=detail)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PaymentRequired(HTTPException):
|
|
23
|
+
def __init__(self, detail: str | None = None):
|
|
24
|
+
super().__init__(status_code=HTTP_402_PAYMENT_REQUIRED, detail=detail)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Forbidden(HTTPException):
|
|
28
|
+
def __init__(self, detail: str | None = None):
|
|
29
|
+
super().__init__(status_code=HTTP_403_FORBIDDEN, detail=detail)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class NotFound(HTTPException):
|
|
33
|
+
def __init__(self, detail: str | None = None):
|
|
34
|
+
super().__init__(status_code=HTTP_404_NOT_FOUND, detail=detail)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MethodNotAllowed(HTTPException):
|
|
38
|
+
def __init__(self, detail: str | None = None):
|
|
39
|
+
super().__init__(status_code=HTTP_405_METHOD_NOT_ALLOWED, detail=detail)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ...
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import uvicorn.config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class IgnoreOptionsRequestsFilter(logging.Filter):
|
|
7
|
+
def filter(self, record):
|
|
8
|
+
if record.args is None:
|
|
9
|
+
return True
|
|
10
|
+
ip, method, route, _, code = record.args
|
|
11
|
+
return method != "OPTIONS"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
ignore_options_request_filter = IgnoreOptionsRequestsFilter("ignore_options_requests")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
LOGGER_CONFIG = uvicorn.config.LOGGING_CONFIG
|
|
18
|
+
LOGGER_CONFIG["disable_existing_loggers"] = False
|
|
19
|
+
|
|
20
|
+
# logger = logging.getLogger("ocean")
|
|
21
|
+
|
|
22
|
+
# # Redirect own logger to uvicorn
|
|
23
|
+
# uvicorn_logger = logging.getLogger("uvicorn")
|
|
24
|
+
# for handler in uvicorn_logger.handlers:
|
|
25
|
+
# logger.addHandler(handler)
|
|
26
|
+
|
|
27
|
+
access_logger = logging.getLogger("uvicorn.access")
|
|
28
|
+
access_logger.addFilter(ignore_options_request_filter)
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("uvicorn.error")
|
|
31
|
+
# logger.setLevel(logging.INFO)
|
|
32
|
+
logger.setLevel(logging.DEBUG)
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, computed_field
|
|
6
|
+
|
|
7
|
+
from ..utils import custom_snake2camel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ApiBaseModel(BaseModel):
|
|
11
|
+
class Config:
|
|
12
|
+
alias_generator = custom_snake2camel
|
|
13
|
+
populate_by_name = True
|
|
14
|
+
arbitrary_types_allowed = True
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RequestBody(ApiBaseModel):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
T = TypeVar("T")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
25
|
+
response: T
|
|
26
|
+
page: int
|
|
27
|
+
page_size: int
|
|
28
|
+
total_items: int
|
|
29
|
+
|
|
30
|
+
@computed_field
|
|
31
|
+
@property
|
|
32
|
+
def total_pages(self) -> int:
|
|
33
|
+
return self.total_items // self.page_size
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
from ocelescope import BaseFilter
|
|
6
|
+
from ocelescope_backend.app.internal.model.base import ApiBaseModel, RequestBody
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilterEnvelope(ApiBaseModel):
|
|
10
|
+
name: str
|
|
11
|
+
payload: dict[str, Any] = Field(default_factory=dict)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DiscoveryRequest(BaseModel):
|
|
15
|
+
ocel_id: str
|
|
16
|
+
method_id: str
|
|
17
|
+
name: str
|
|
18
|
+
resource_type: str
|
|
19
|
+
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
20
|
+
filters: list[BaseFilter] = Field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CreateDiscoveryTaskBody(RequestBody):
|
|
24
|
+
method_id: str
|
|
25
|
+
parameters: dict[str, Any] = Field(default_factory=dict)
|
|
26
|
+
filters: list[FilterEnvelope] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DiscoveryVariant(ApiBaseModel):
|
|
30
|
+
method_id: str
|
|
31
|
+
resource_type: str
|
|
32
|
+
input_schema: dict[str, Any]
|
|
33
|
+
description: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class DiscoveryMethodMeta(ApiBaseModel):
|
|
37
|
+
name: str
|
|
38
|
+
variants: list[DiscoveryVariant]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Date_Distribution_Item(BaseModel):
|
|
5
|
+
start_timestamp: str
|
|
6
|
+
end_timestamp: str
|
|
7
|
+
entity_count: dict[str, int]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Entity_Time_Info(BaseModel):
|
|
11
|
+
start_time: str
|
|
12
|
+
end_time: str
|
|
13
|
+
date_distribution: list[Date_Distribution_Item]
|