Skip to content

Commit cbe45ad

Browse files
committed
FEA: Add octue question ask CLI command
skipci
1 parent a4c1450 commit cbe45ad

File tree

6 files changed

+149
-20
lines changed

6 files changed

+149
-20
lines changed

octue/cli.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
from google import auth
1212

1313
from octue.cloud import pub_sub, storage
14+
from octue.cloud.deployment.google.answer_pub_sub_question import answer_question
15+
from octue.cloud.events.utils import make_originator_question_event
1416
from octue.cloud.pub_sub.service import Service
1517
from octue.cloud.service_id import create_sruid, get_sruid_parts
1618
from octue.cloud.storage import GoogleCloudStorageClient
@@ -70,6 +72,78 @@ def octue_cli(id, logger_uri, log_level, force_reset):
7072
global_cli_context["log_handler"] = get_remote_handler(logger_uri=global_cli_context["logger_uri"])
7173

7274

75+
@octue_cli.group()
76+
def question():
77+
"""Ask and interact with questions to an Octue Twined data service."""
78+
79+
80+
@question.command()
81+
@click.argument("sruid", type=str)
82+
@click.option(
83+
"-i",
84+
"--input-values",
85+
type=str,
86+
default=None,
87+
help="Any input values for the question as a JSON-encoded string.",
88+
)
89+
@click.option(
90+
"-m",
91+
"--input-manifest",
92+
type=str,
93+
default=None,
94+
help="An optional input manifest for the question serialised as a JSON-encoded string.",
95+
)
96+
@click.option(
97+
"-c",
98+
"--service-config",
99+
type=click.Path(dir_okay=False),
100+
default=None,
101+
help="The path to an `octue.yaml` file defining the service to run. If not provided, the "
102+
"`OCTUE_SERVICE_CONFIGURATION_PATH` environment variable is used if present, otherwise the local path `octue.yaml` "
103+
"is used.",
104+
)
105+
def ask(sruid, input_values, input_manifest, service_config):
106+
"""Ask a question to a local or remote Octue Twined service.
107+
108+
SRUID should be:
109+
110+
- For remote services: a valid service revision unique identifier for an existing Octue Twined service
111+
112+
e.g. octue question ask octue/example-service:1.0.3
113+
114+
- For a local service: "local"
115+
116+
e.g. octue question ask local
117+
"""
118+
if sruid == "local":
119+
service_configuration, app_configuration = load_service_and_app_configuration(service_config)
120+
service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration)
121+
122+
child_sruid = create_sruid(namespace=service_namespace, name=service_name, revision_tag=service_revision_tag)
123+
parent_sruid = "local/local:local"
124+
125+
question = make_originator_question_event(
126+
input_values=input_values,
127+
input_manifest=input_manifest,
128+
parent_sruid=parent_sruid,
129+
child_sruid=child_sruid,
130+
)
131+
132+
backend_configuration_values = (app_configuration.configuration_values or {}).get("backend")
133+
134+
if backend_configuration_values:
135+
backend_configuration_values = copy.deepcopy(backend_configuration_values)
136+
backend = service_backends.get_backend(backend_configuration_values.pop("name"))(
137+
**backend_configuration_values
138+
)
139+
else:
140+
# If no backend details are provided, use Google Pub/Sub with the default project.
141+
_, project_name = auth.default()
142+
backend = service_backends.get_backend()(project_name=project_name)
143+
144+
answer_question(question=question, project_name=backend.project_name, service_configuration=service_config)
145+
146+
73147
@octue_cli.command()
74148
@click.option(
75149
"-c",

octue/cloud/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import importlib.metadata
2+
13
import octue.exceptions
24
import twined.exceptions
35
from octue.utils.exceptions import create_exceptions_mapping
46

57

8+
LOCAL_SDK_VERSION = importlib.metadata.version("octue")
9+
10+
611
EXCEPTIONS_MAPPING = create_exceptions_mapping(
712
globals()["__builtins__"],
813
vars(twined.exceptions),

octue/cloud/deployment/google/answer_pub_sub_question.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111
logger = logging.getLogger(__name__)
1212

1313

14-
def answer_question(question, project_name):
14+
def answer_question(question, project_name, service_configuration=None, app_configuration=None):
1515
"""Answer a question sent to an app deployed in Google Cloud.
1616
17-
:param dict|tuple question:
17+
:param dict question:
1818
:param str project_name:
19+
:param service_configuration:
20+
:param app_configuration:
1921
:return None:
2022
"""
21-
service_configuration, app_configuration = load_service_and_app_configuration()
23+
if not service_configuration:
24+
service_configuration, app_configuration = load_service_and_app_configuration()
25+
2226
service_namespace, service_name, service_revision_tag = get_sruid_parts(service_configuration)
2327

2428
service_sruid = create_sruid(

octue/cloud/events/utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import datetime
2+
import uuid
3+
4+
from octue.cloud import LOCAL_SDK_VERSION
5+
6+
7+
def make_originator_question_event(input_values, input_manifest, parent_sruid, child_sruid, question_uuid=None):
8+
question_uuid = question_uuid or uuid.uuid4()
9+
10+
return {
11+
"event": {
12+
"input_values": input_values,
13+
"input_manifest": input_manifest,
14+
},
15+
"attributes": make_attributes(
16+
question_uuid=question_uuid,
17+
parent_question_uuid=question_uuid,
18+
originator_question_uuid=question_uuid,
19+
parent=parent_sruid,
20+
originator=parent_sruid,
21+
sender=parent_sruid,
22+
recipient=child_sruid,
23+
),
24+
}
25+
26+
27+
def make_attributes(
28+
parent_question_uuid,
29+
originator_question_uuid,
30+
parent,
31+
originator,
32+
sender,
33+
recipient,
34+
question_uuid=None,
35+
retry_count=0,
36+
):
37+
return {
38+
"uuid": str(uuid.uuid4()),
39+
"datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
40+
"question_uuid": question_uuid or str(uuid.uuid4()),
41+
"parent_question_uuid": parent_question_uuid,
42+
"originator_question_uuid": originator_question_uuid,
43+
"parent": parent,
44+
"originator": originator,
45+
"sender": sender,
46+
"sender_sdk_version": LOCAL_SDK_VERSION,
47+
"recipient": recipient,
48+
"retry_count": retry_count,
49+
}

octue/cloud/pub_sub/service.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import concurrent.futures
22
import copy
3-
import datetime
43
import functools
54
import importlib.metadata
65
import json
@@ -15,7 +14,9 @@
1514
from google.cloud import pubsub_v1
1615

1716
import octue.exceptions
17+
from octue.cloud import LOCAL_SDK_VERSION
1818
from octue.cloud.events import OCTUE_SERVICES_PREFIX
19+
from octue.cloud.events.utils import make_attributes
1920
from octue.cloud.events.validation import raise_if_event_is_invalid
2021
from octue.cloud.pub_sub import Subscription, Topic
2122
from octue.cloud.pub_sub.events import GoogleCloudPubSubEventHandler, extract_event_and_attributes_from_pub_sub_message
@@ -85,7 +86,6 @@ def __init__(self, backend, service_id=None, run_function=None, service_registri
8586
self.service_registries = service_registries
8687

8788
self._pub_sub_id = convert_service_id_to_pub_sub_form(self.id)
88-
self._local_sdk_version = importlib.metadata.version("octue")
8989
self._event_handler = None
9090

9191
def __repr__(self):
@@ -288,7 +288,7 @@ def answer(self, question, heartbeat_interval=120, timeout=30):
288288
if heartbeater is not None:
289289
heartbeater.cancel()
290290

291-
warn_if_incompatible(child_sdk_version=self._local_sdk_version, parent_sdk_version=parent_sdk_version)
291+
warn_if_incompatible(child_sdk_version=LOCAL_SDK_VERSION, parent_sdk_version=parent_sdk_version)
292292
self.send_exception(timeout=timeout, **routing_metadata)
293293
raise error
294294

@@ -538,19 +538,16 @@ def _emit_event(
538538
attributes = attributes or {}
539539

540540
attributes.update(
541-
{
542-
"uuid": str(uuid.uuid4()),
543-
"datetime": datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
544-
"question_uuid": question_uuid,
545-
"parent_question_uuid": parent_question_uuid,
546-
"originator_question_uuid": originator_question_uuid,
547-
"parent": parent,
548-
"originator": originator,
549-
"sender": self.id,
550-
"sender_sdk_version": self._local_sdk_version,
551-
"recipient": recipient,
552-
"retry_count": retry_count,
553-
}
541+
make_attributes(
542+
question_uuid=question_uuid,
543+
parent_question_uuid=parent_question_uuid,
544+
originator_question_uuid=originator_question_uuid,
545+
parent=parent,
546+
originator=originator,
547+
sender=self.id,
548+
recipient=recipient,
549+
retry_count=retry_count,
550+
)
554551
)
555552

556553
converted_attributes = {}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "octue"
3-
version = "0.62.0"
3+
version = "0.63.0"
44
description = "A package providing template applications for data services, and a python SDK to the Octue API."
55
readme = "README.md"
66
authors = ["Marcus Lugg <marcus@octue.com>", "Thomas Clark <support@octue.com>"]

0 commit comments

Comments
 (0)