Skip to content

Commit c2a3557

Browse files
authored
Merge pull request #115 from OpenSemanticLab/dev
refactor: extract reusable Prefect+OSW utils from examples
2 parents 5dfbd39 + 491f8bc commit c2a3557

6 files changed

Lines changed: 338 additions & 129 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ repos:
2424
hooks:
2525
- id: autoflake
2626
args: [
27-
# --in-place, # Use this to modify the files in place, without printing diffs, as opposed to --stdout
28-
--stdout,
27+
--in-place,
2928
--remove-all-unused-imports,
3029
--remove-unused-variables,
3130
]

examples/prefect/hello_world.py

Lines changed: 60 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,53 @@
1-
import asyncio
2-
import uuid
3-
from os import environ
41
from typing import Optional
52
from uuid import UUID, uuid4
63

7-
from prefect import flow, get_client, task
8-
from prefect.blocks.system import Secret
9-
from pydantic.v1 import Field
4+
from opensemantic.base.v1 import Article
5+
from prefect import flow, task
6+
from pydantic.v1 import BaseModel, Field
107

118
import osw.model.entity as model
12-
from osw.auth import CredentialManager
139
from osw.core import OSW
14-
from osw.utils.wiki import get_full_title
10+
from osw.utils.workflow import (
11+
ConnectionSettings,
12+
DeployConfig,
13+
DeployParam,
14+
WorkflowRequest,
15+
connect,
16+
deploy,
17+
)
1518
from osw.wtsite import WtSite
1619

17-
18-
class ConnectionSettings(model.OswBaseModel):
19-
"""Connection data for OSW"""
20-
21-
osw_user_name: Optional[str]
22-
"""The login username.
23-
Note: value of envar OSW_USER used of not given
24-
Note: value of envar OSW_PASSWORD used for login"""
25-
osw_domain: Optional[str]
26-
"""The domain of the instance
27-
Note: value of envar OSW_SERVER used of not given"""
20+
# Module-level OSW instance, set by the connect_osw task
21+
osw: Optional[OSW] = None
2822

2923

3024
@task
31-
def connect(settings: Optional[ConnectionSettings] = None):
32-
"""Initiates the connection to the OSW instance
33-
34-
Parameters
35-
----------
36-
settings
37-
see ConnectionSetttings dataclass
38-
"""
39-
if settings is None:
40-
settings = ConnectionSettings()
41-
global wtsite
42-
# define username
43-
if environ.get("OSW_USER") is not None and environ.get("OSW_USER") != "":
44-
settings.osw_user_name = environ.get("OSW_USER")
45-
if environ.get("OSW_SERVER") is not None and environ.get("OSW_SERVER") != "":
46-
settings.osw_domain = environ.get("OSW_SERVER")
47-
password = ""
48-
if environ.get("OSW_PASSWORD") is not None and environ.get("OSW_PASSWORD") != "":
49-
password = environ.get("OSW_PASSWORD")
50-
else:
51-
# fetch secret stored in prefect server from calculated name
52-
password = Secret.load(
53-
settings.osw_user_name.lower() + "-" + settings.osw_domain.replace(".", "-")
54-
).get() # e. g. mybot-wiki-dev-open-semantic-lab-org
55-
cm = CredentialManager()
56-
cm.add_credential(
57-
CredentialManager.UserPwdCredential(
58-
iri=settings.osw_domain, username=settings.osw_user_name, password=password
59-
)
60-
)
61-
wtsite = WtSite(WtSite.WtSiteConfig(iri=settings.osw_domain, cred_mngr=cm))
25+
def connect_osw(settings: Optional[ConnectionSettings] = None):
26+
"""Initiates the connection to the OSW instance"""
6227
global osw
63-
osw = OSW(site=wtsite)
28+
osw = connect(settings)
6429

6530

6631
@task
6732
def fetch_schema():
68-
"""this will load the current entity schema from the OSW instance."""
69-
# Load Article Schema on demand
70-
if not hasattr(model, "Article"):
71-
osw.fetch_schema(
72-
OSW.FetchSchemaParam(
73-
schema_title=[
74-
"Category:OSW77e749fc598341ac8b6d2fff21574058", # Software
75-
"Category:OSW72eae3c8f41f4a22a94dbc01974ed404", # PrefectFlow
76-
"Category:OSW92cc6b1a2e6b4bb7bad470dfdcfdaf26", # Article
77-
],
78-
mode="replace",
79-
)
80-
)
33+
"""Fetch custom schemas not yet available in packages.
34+
35+
Software, PrefectFlow, and Article are already provided by
36+
opensemantic.base, so no fetch is needed for this example.
37+
Uncomment and adapt the code below if your workflow uses
38+
schemas that are only available on the OSW instance.
39+
"""
40+
# osw.fetch_schema(
41+
# OSW.FetchSchemaParam(
42+
# schema_title=[
43+
# "Category:OSW...", # your custom category
44+
# ],
45+
# mode="replace",
46+
# )
47+
# )
8148

8249

83-
class Result(model.OswBaseModel):
50+
class Result(BaseModel):
8451
"""The result dataclass"""
8552

8653
uuid: Optional[UUID] = Field(default_factory=uuid4, title="UUID")
@@ -106,15 +73,21 @@ def store_and_document_result(result: Result):
10673
title = result.target_title
10774
else:
10875
title = "Item:" + osw.get_osw_id(result.uuid)
109-
entity = osw.load_entity(title)
76+
entity = osw.load_entity(
77+
OSW.LoadEntityParam(
78+
titles=[title],
79+
autofetch_schema=False,
80+
model_to_use=Article,
81+
)
82+
).entities[0]
11083
if entity is None:
11184
# does not exist yet - create a new one
112-
entity = model.Article(
85+
entity = Article(
11386
uuid=result.uuid, label=[model.Label(text="Article for dummy workflow")]
11487
)
11588

11689
# edit structured data
117-
entity = entity.cast(model.Article)
90+
entity = entity.cast(Article)
11891
entity.description = [model.Description(text="some descriptive text")]
11992
osw.store_entity(entity)
12093

@@ -127,13 +100,9 @@ def store_and_document_result(result: Result):
127100
print("FINISHED")
128101

129102

130-
class Request(model.OswBaseModel):
131-
uuid: UUID = Field(default_factory=uuid4, title="UUID")
132-
"""UUIDv4 of the request."""
133-
osw_domain: Optional[str] = "wiki-dev.open-semantic-lab.org"
134-
"""To domain of the OSW instance"""
135-
subject: Optional[str] = "Item:OSW56f9439d43244fe7a83163bab9414ee1"
136-
"""Where to store the results. For testing, we use a static default value"""
103+
class Request(WorkflowRequest):
104+
"""Request for the dummy workflow."""
105+
137106
msg: Optional[str] = "test message"
138107
"""The message you want to leave on the target page"""
139108

@@ -150,56 +119,24 @@ def dummy_workflow(request: Request):
150119
request
151120
see Request dataclass
152121
"""
153-
connect(ConnectionSettings(osw_domain=request.osw_domain))
122+
connect_osw(ConnectionSettings(osw_domain=request.osw_domain))
154123
fetch_schema()
155124
store_and_document_result(Result(msg=request.msg, target_title=request.subject))
156125

157126

158-
async def deploy():
159-
"""programmatic deployment supported in newer prefect versions"""
160-
flow = dummy_workflow
161-
# flow_name = flow.name
162-
deployment_name = flow.name + " Deployment"
163-
164-
# create a deployment and apply it
165-
config = await flow.to_deployment(name=deployment_name)
166-
await config.apply() # returns the deployment_uuid
167-
168-
# fetch flow uuid
169-
async with get_client() as client:
170-
response = await client.read_flow_by_name(flow.name)
171-
print(response.json())
172-
flow_uuid = response.id
173-
174-
await connect()
175-
await fetch_schema()
176-
# static UUIDv5 namespace for a stable UUID
177-
namespace_uuid = uuid.UUID("0dd6c54a-b162-4552-bab9-9942ccaf4f41")
178-
179-
# self-documentation / registration
180-
this_tool = model.Software(
181-
uuid=uuid.uuid5(namespace_uuid, flow.name),
182-
label=[model.Label(text=flow.name)],
183-
description=[model.Description(text=flow.description)],
184-
)
185-
186-
prefect_domain = environ.get("PREFECT_API_URL").split("//")[-1].split("/")[0]
187-
this_flow = model.PrefectFlow(
188-
uuid=flow_uuid,
189-
label=[model.Label(text=flow.name + " Prefect Flow")],
190-
description=[model.Description(text=flow.description)],
191-
flow_id=str(flow_uuid),
192-
hosted_software=[get_full_title(this_tool)],
193-
domain=prefect_domain,
194-
)
195-
196-
osw.store_entity(osw.StoreEntityParam(entities=[this_tool, this_flow]))
197-
198-
# start agent to serve deployment
199-
await dummy_workflow.serve(name=deployment_name)
200-
201-
202127
if __name__ == "__main__":
203-
# dummy_workflow(Request(msg="Test"))
204-
with asyncio.Runner() as runner:
205-
runner.run(deploy())
128+
# Direct run: dummy_workflow(Request(msg="Test"))
129+
130+
# Deploy and serve with OSW registration
131+
osw_instance = connect()
132+
deploy(
133+
DeployParam(
134+
deployments=[
135+
DeployConfig(
136+
flow=dummy_workflow,
137+
name="Dummy Workflow Deployment",
138+
)
139+
],
140+
osw=osw_instance,
141+
)
142+
)

setup.cfg

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ python_requires = >=3.10
5050
install_requires =
5151
oold>=0.11.1
5252
opensemantic
53-
opensemantic.core>=0.53.1
53+
opensemantic.core>=0.57.4
54+
opensemantic.base>=0.42.7
5455
pydantic[email]>=1.10.17
5556
datamodel-code-generator==0.51.0
5657
black
@@ -98,7 +99,7 @@ dataimport =
9899
UI =
99100
pysimplegui
100101
workflow =
101-
prefect==2.20.0
102+
prefect>=2.20.25,<3.0
102103
tutorial =
103104
%(dataimport)s
104105
all =

src/osw/core.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,8 @@ def _fetch_schema(
683683
use_title_as_name=True,
684684
use_schema_description=True,
685685
use_field_description=True,
686+
# https://github.com/koxudaxi/datamodel-code-generator/issues/2447
687+
# use_standard_collections=data_model_type != "pydantic.BaseModel",
686688
encoding="utf-8",
687689
use_double_quotes=True,
688690
collapse_root_models=True,
@@ -879,6 +881,16 @@ def _fetch_schema(
879881
"\n"
880882
)
881883

884+
# import Software, PrefectWorkflow from base
885+
if data_model_type == "pydantic.BaseModel":
886+
header += (
887+
"from opensemantic.base.v1 import Software, PrefectFlow\n"
888+
)
889+
else:
890+
header += (
891+
"from opensemantic.base import Software, PrefectFlow\n"
892+
)
893+
882894
content = re.sub(
883895
pattern=r"(^class\s*\S*\s*\(\s*[\S\s]*?\s*\)\s*:.*\n)",
884896
repl=header + r"\n\n\n\1",

src/osw/model/entity.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,8 @@
2121
WikiFile,
2222
PagePackage,
2323
) # noqa: F401, E402
24+
25+
from opensemantic.base.v1 import ( # isort:skip
26+
Software,
27+
PrefectFlow,
28+
) # noqa: F401, E402

0 commit comments

Comments
 (0)