unitysvc-services 0.1.24__py3-none-any.whl
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.
- unitysvc_services/__init__.py +4 -0
- unitysvc_services/api.py +421 -0
- unitysvc_services/cli.py +23 -0
- unitysvc_services/format_data.py +140 -0
- unitysvc_services/interactive_prompt.py +1132 -0
- unitysvc_services/list.py +216 -0
- unitysvc_services/models/__init__.py +71 -0
- unitysvc_services/models/base.py +1375 -0
- unitysvc_services/models/listing_data.py +118 -0
- unitysvc_services/models/listing_v1.py +56 -0
- unitysvc_services/models/provider_data.py +79 -0
- unitysvc_services/models/provider_v1.py +54 -0
- unitysvc_services/models/seller_data.py +120 -0
- unitysvc_services/models/seller_v1.py +42 -0
- unitysvc_services/models/service_data.py +114 -0
- unitysvc_services/models/service_v1.py +81 -0
- unitysvc_services/populate.py +207 -0
- unitysvc_services/publisher.py +1628 -0
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +688 -0
- unitysvc_services/scaffold.py +1103 -0
- unitysvc_services/schema/base.json +777 -0
- unitysvc_services/schema/listing_v1.json +1286 -0
- unitysvc_services/schema/provider_v1.json +952 -0
- unitysvc_services/schema/seller_v1.json +379 -0
- unitysvc_services/schema/service_v1.json +1306 -0
- unitysvc_services/test.py +965 -0
- unitysvc_services/unpublisher.py +505 -0
- unitysvc_services/update.py +287 -0
- unitysvc_services/utils.py +533 -0
- unitysvc_services/validator.py +731 -0
- unitysvc_services-0.1.24.dist-info/METADATA +184 -0
- unitysvc_services-0.1.24.dist-info/RECORD +37 -0
- unitysvc_services-0.1.24.dist-info/WHEEL +5 -0
- unitysvc_services-0.1.24.dist-info/entry_points.txt +3 -0
- unitysvc_services-0.1.24.dist-info/licenses/LICENSE +21 -0
- unitysvc_services-0.1.24.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
"""Interactive prompt system for creating data files.
|
|
2
|
+
|
|
3
|
+
This module provides a systematic way to prompt users for field values
|
|
4
|
+
using a field registry approach, making it easy to add/remove/modify fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import tomllib
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# =============================================================================
|
|
23
|
+
# HELPER FUNCTIONS FOR AUTO-DISCOVERY
|
|
24
|
+
# =============================================================================
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_seller_name(data_dir: Path | None = None) -> str | None:
|
|
28
|
+
"""Find seller name from seller.json or seller.toml in data directory.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
data_dir: Directory to search for seller file (defaults to search common locations)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Seller name if found, None otherwise
|
|
35
|
+
"""
|
|
36
|
+
# Search in multiple common locations
|
|
37
|
+
search_dirs = []
|
|
38
|
+
|
|
39
|
+
if data_dir is not None:
|
|
40
|
+
search_dirs.append(data_dir)
|
|
41
|
+
else:
|
|
42
|
+
# Common locations to search
|
|
43
|
+
cwd = Path.cwd()
|
|
44
|
+
search_dirs.extend(
|
|
45
|
+
[
|
|
46
|
+
cwd / "data", # ./data (most common)
|
|
47
|
+
cwd, # current directory
|
|
48
|
+
cwd.parent / "data", # ../data (if we're in a subdirectory)
|
|
49
|
+
cwd.parent, # parent directory
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Look for seller file in each search directory
|
|
54
|
+
for search_dir in search_dirs:
|
|
55
|
+
for filename in ["seller.json", "seller.toml"]:
|
|
56
|
+
seller_file = search_dir / filename
|
|
57
|
+
if seller_file.exists():
|
|
58
|
+
try:
|
|
59
|
+
if filename.endswith(".json"):
|
|
60
|
+
with open(seller_file) as f:
|
|
61
|
+
data = json.load(f)
|
|
62
|
+
else: # .toml
|
|
63
|
+
with open(seller_file, "rb") as f:
|
|
64
|
+
data = tomllib.load(f)
|
|
65
|
+
|
|
66
|
+
seller_name = data.get("name")
|
|
67
|
+
if seller_name:
|
|
68
|
+
return seller_name
|
|
69
|
+
except Exception:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def prompt_for_pricing() -> dict[str, Any]:
|
|
76
|
+
"""Interactively prompt for pricing information (for seller_price).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Dictionary with pricing data
|
|
80
|
+
"""
|
|
81
|
+
console.print("\n[bold cyan]Adding pricing information[/bold cyan]")
|
|
82
|
+
|
|
83
|
+
# Required field: pricing type (now inside price_data)
|
|
84
|
+
# Note: revenue_share is only valid for seller_price, which is the only context
|
|
85
|
+
# where this function is called
|
|
86
|
+
pricing_type = Prompt.ask(
|
|
87
|
+
"[bold blue]Pricing type[/bold blue] [red]*[/red]",
|
|
88
|
+
choices=["one_million_tokens", "one_second", "image", "step", "revenue_share"],
|
|
89
|
+
default="one_million_tokens",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Optional fields
|
|
93
|
+
description = Prompt.ask("[bold blue]Description[/bold blue] [dim](optional)[/dim]", default="")
|
|
94
|
+
currency = Prompt.ask("[bold blue]Currency code[/bold blue] [dim](optional, e.g., 'USD')[/dim]", default="USD")
|
|
95
|
+
reference = Prompt.ask(
|
|
96
|
+
"[bold blue]Reference URL[/bold blue] [dim](optional, link to upstream pricing page)[/dim]", default=""
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Build price_data based on pricing type
|
|
100
|
+
price_data: dict[str, Any]
|
|
101
|
+
|
|
102
|
+
if pricing_type == "revenue_share":
|
|
103
|
+
# Revenue share pricing - just needs a percentage
|
|
104
|
+
console.print("\n[dim]Revenue share: seller receives a percentage of customer charge[/dim]")
|
|
105
|
+
percentage = Prompt.ask(
|
|
106
|
+
"[bold blue]Percentage[/bold blue] [dim](0-100, e.g., '70' for 70%)[/dim]",
|
|
107
|
+
default="70",
|
|
108
|
+
)
|
|
109
|
+
price_data = {"type": pricing_type, "percentage": percentage}
|
|
110
|
+
else:
|
|
111
|
+
# Other pricing types - ask for price structure
|
|
112
|
+
console.print("\n[dim]Price data structure options:[/dim]")
|
|
113
|
+
console.print('[dim] 1. Simple: {"type": "...", "price": "10.00"}[/dim]')
|
|
114
|
+
console.print('[dim] 2. Input/Output (LLMs): {"type": "...", "input": "5.00", "output": "15.00"}[/dim]')
|
|
115
|
+
console.print('[dim] 3. Custom: any JSON with "type" field included[/dim]')
|
|
116
|
+
|
|
117
|
+
structure = Prompt.ask(
|
|
118
|
+
"\n[bold blue]Price data structure[/bold blue]",
|
|
119
|
+
choices=["simple", "input_output", "custom"],
|
|
120
|
+
default="simple",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if structure == "simple":
|
|
124
|
+
amount = Prompt.ask(
|
|
125
|
+
"[bold blue]Price amount[/bold blue] [dim](e.g., '0.50')[/dim]",
|
|
126
|
+
default="0",
|
|
127
|
+
)
|
|
128
|
+
price_data = {"type": pricing_type, "price": amount}
|
|
129
|
+
|
|
130
|
+
elif structure == "input_output":
|
|
131
|
+
input_amount = Prompt.ask(
|
|
132
|
+
"[bold blue]Input price amount[/bold blue] [dim](e.g., '0.50')[/dim]",
|
|
133
|
+
default="0",
|
|
134
|
+
)
|
|
135
|
+
output_amount = Prompt.ask(
|
|
136
|
+
"[bold blue]Output price amount[/bold blue] [dim](e.g., '1.50')[/dim]",
|
|
137
|
+
default="0",
|
|
138
|
+
)
|
|
139
|
+
price_data = {"type": pricing_type, "input": input_amount, "output": output_amount}
|
|
140
|
+
|
|
141
|
+
else: # custom
|
|
142
|
+
console.print(
|
|
143
|
+
f'\n[dim]Enter additional price_data fields as JSON (type "{pricing_type}" will be added)[/dim]'
|
|
144
|
+
)
|
|
145
|
+
console.print('[dim]Example: {"price": "0.05"}[/dim]')
|
|
146
|
+
while True:
|
|
147
|
+
json_input = Prompt.ask("[bold blue]Additional price data JSON[/bold blue]", default="{}")
|
|
148
|
+
try:
|
|
149
|
+
custom_data = json.loads(json_input)
|
|
150
|
+
if not isinstance(custom_data, dict):
|
|
151
|
+
console.print("[red]Error: Price data must be a JSON object (dict)[/red]")
|
|
152
|
+
continue
|
|
153
|
+
price_data = {"type": pricing_type, **custom_data}
|
|
154
|
+
break
|
|
155
|
+
except json.JSONDecodeError as e:
|
|
156
|
+
console.print(f"[red]Invalid JSON: {e}[/red]")
|
|
157
|
+
console.print("[dim]Try again or press Ctrl+C to cancel[/dim]")
|
|
158
|
+
|
|
159
|
+
# Build pricing dict
|
|
160
|
+
pricing: dict[str, Any] = {
|
|
161
|
+
"price_data": price_data,
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if description:
|
|
165
|
+
pricing["description"] = description
|
|
166
|
+
if currency:
|
|
167
|
+
pricing["currency"] = currency
|
|
168
|
+
if reference:
|
|
169
|
+
pricing["reference"] = reference
|
|
170
|
+
|
|
171
|
+
return pricing
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def prompt_for_document(listing_dir: Path) -> dict[str, Any]:
|
|
175
|
+
"""Interactively prompt for a single document.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
listing_dir: Directory where the listing file will be created (for validating file paths)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary with document data
|
|
182
|
+
"""
|
|
183
|
+
console.print("\n[bold cyan]Adding a document[/bold cyan]")
|
|
184
|
+
|
|
185
|
+
# Required fields
|
|
186
|
+
title = Prompt.ask("[bold blue]Document title[/bold blue] [red]*[/red]")
|
|
187
|
+
|
|
188
|
+
mime_type = Prompt.ask(
|
|
189
|
+
"[bold blue]MIME type[/bold blue] [red]*[/red]",
|
|
190
|
+
choices=["markdown", "python", "javascript", "bash", "html", "text", "pdf", "jpeg", "png", "svg", "url"],
|
|
191
|
+
default="markdown",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
category = Prompt.ask(
|
|
195
|
+
"[bold blue]Document category[/bold blue] [red]*[/red]",
|
|
196
|
+
choices=[
|
|
197
|
+
"getting_started",
|
|
198
|
+
"api_reference",
|
|
199
|
+
"tutorial",
|
|
200
|
+
"code_example",
|
|
201
|
+
"use_case",
|
|
202
|
+
"troubleshooting",
|
|
203
|
+
"changelog",
|
|
204
|
+
"best_practice",
|
|
205
|
+
"specification",
|
|
206
|
+
"service_level_agreement",
|
|
207
|
+
"terms_of_service",
|
|
208
|
+
"logo",
|
|
209
|
+
"other",
|
|
210
|
+
],
|
|
211
|
+
default="getting_started",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Optional fields
|
|
215
|
+
description = Prompt.ask("[bold blue]Description[/bold blue] [dim](optional)[/dim]", default="")
|
|
216
|
+
|
|
217
|
+
# At least one of file_path or external_url must be specified
|
|
218
|
+
file_path = ""
|
|
219
|
+
external_url = ""
|
|
220
|
+
|
|
221
|
+
while not file_path and not external_url:
|
|
222
|
+
console.print("[dim]At least one of file path or external URL must be specified[/dim]")
|
|
223
|
+
|
|
224
|
+
file_path = Prompt.ask(
|
|
225
|
+
"[bold blue]File path[/bold blue] [dim](relative to listing dir, e.g., 'docs/guide.md')[/dim]",
|
|
226
|
+
default="",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# If file_path is provided, validate it exists
|
|
230
|
+
if file_path:
|
|
231
|
+
file_full_path = listing_dir / file_path
|
|
232
|
+
if not file_full_path.exists():
|
|
233
|
+
console.print(f"[yellow]Warning: File not found at {file_full_path}[/yellow]")
|
|
234
|
+
if not Confirm.ask("[bold blue]Use this path anyway?[/bold blue]", default=False):
|
|
235
|
+
file_path = ""
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# If no file_path, must provide external_url
|
|
239
|
+
if not file_path:
|
|
240
|
+
external_url = Prompt.ask(
|
|
241
|
+
"[bold blue]External URL[/bold blue] [dim](required if no file path)[/dim]",
|
|
242
|
+
default="",
|
|
243
|
+
)
|
|
244
|
+
if not external_url:
|
|
245
|
+
console.print("[red]Either file path or external URL must be provided[/red]")
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
is_public = Confirm.ask("[bold blue]Is public?[/bold blue]", default=False)
|
|
249
|
+
|
|
250
|
+
# Build document dict
|
|
251
|
+
doc: dict[str, Any] = {
|
|
252
|
+
"title": title,
|
|
253
|
+
"mime_type": mime_type,
|
|
254
|
+
"category": category,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if description:
|
|
258
|
+
doc["description"] = description
|
|
259
|
+
if file_path:
|
|
260
|
+
doc["file_path"] = file_path
|
|
261
|
+
if external_url:
|
|
262
|
+
doc["external_url"] = external_url
|
|
263
|
+
if is_public:
|
|
264
|
+
doc["is_public"] = is_public
|
|
265
|
+
|
|
266
|
+
return doc
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def find_service_name(service_dir: Path | None = None) -> str | None:
|
|
270
|
+
"""Find service name from service.json or service.toml in service directory.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
service_dir: Directory to search for service file (defaults to search common locations)
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Service name if found, None otherwise
|
|
277
|
+
"""
|
|
278
|
+
# Search in multiple common locations
|
|
279
|
+
search_dirs = []
|
|
280
|
+
|
|
281
|
+
if service_dir is not None:
|
|
282
|
+
search_dirs.append(service_dir)
|
|
283
|
+
else:
|
|
284
|
+
# Common locations to search
|
|
285
|
+
cwd = Path.cwd()
|
|
286
|
+
search_dirs.extend(
|
|
287
|
+
[
|
|
288
|
+
cwd, # current directory (most common for services)
|
|
289
|
+
cwd.parent, # parent directory (if we're in a subdirectory)
|
|
290
|
+
]
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Look for service file in each search directory
|
|
294
|
+
for search_dir in search_dirs:
|
|
295
|
+
for filename in ["service.json", "service.toml"]:
|
|
296
|
+
service_file = search_dir / filename
|
|
297
|
+
if service_file.exists():
|
|
298
|
+
try:
|
|
299
|
+
if filename.endswith(".json"):
|
|
300
|
+
with open(service_file) as f:
|
|
301
|
+
data = json.load(f)
|
|
302
|
+
else: # .toml
|
|
303
|
+
with open(service_file, "rb") as f:
|
|
304
|
+
data = tomllib.load(f)
|
|
305
|
+
|
|
306
|
+
service_name = data.get("name")
|
|
307
|
+
if service_name:
|
|
308
|
+
return service_name
|
|
309
|
+
except Exception:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
class FieldDef:
|
|
317
|
+
"""Definition for a single field to prompt.
|
|
318
|
+
|
|
319
|
+
Attributes:
|
|
320
|
+
name: Field name in the schema
|
|
321
|
+
prompt_text: Text to display to the user
|
|
322
|
+
field_type: Type of field (string, email, choice, boolean, integer)
|
|
323
|
+
required: Whether the field is required
|
|
324
|
+
default: Default value (can be callable that takes context)
|
|
325
|
+
choices: List of choices for choice-type fields
|
|
326
|
+
description: Help text for the field
|
|
327
|
+
skip_if: Callable that returns True if field should be skipped
|
|
328
|
+
validate: Optional validation function
|
|
329
|
+
group: Logical grouping for related fields
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
name: str
|
|
333
|
+
prompt_text: str
|
|
334
|
+
field_type: str = "string" # string, email, uri, choice, boolean, integer
|
|
335
|
+
required: bool = False
|
|
336
|
+
default: Any = None
|
|
337
|
+
choices: list[str] | None = None
|
|
338
|
+
description: str | None = None
|
|
339
|
+
skip_if: Callable[[dict], bool] | None = None
|
|
340
|
+
validate: Callable[[Any], Any] | None = None
|
|
341
|
+
group: str = "general"
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass
|
|
345
|
+
class FieldGroup:
|
|
346
|
+
"""Group of related fields."""
|
|
347
|
+
|
|
348
|
+
name: str
|
|
349
|
+
title: str
|
|
350
|
+
fields: list[FieldDef] = field(default_factory=list)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class PromptEngine:
|
|
354
|
+
"""Engine for prompting users based on field definitions."""
|
|
355
|
+
|
|
356
|
+
def __init__(self, groups: list[FieldGroup]):
|
|
357
|
+
"""Initialize prompt engine with field groups.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
groups: List of field groups to prompt for
|
|
361
|
+
"""
|
|
362
|
+
self.groups = groups
|
|
363
|
+
|
|
364
|
+
def prompt_all(self, context: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
365
|
+
"""Prompt for all fields in all groups.
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
context: Initial context/data (e.g., name provided via CLI arg)
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Dictionary with all prompted values
|
|
372
|
+
"""
|
|
373
|
+
if context is None:
|
|
374
|
+
context = {}
|
|
375
|
+
|
|
376
|
+
data = {}
|
|
377
|
+
|
|
378
|
+
for group in self.groups:
|
|
379
|
+
if group.fields:
|
|
380
|
+
console.print(f"\n[bold cyan]{group.title}[/bold cyan]")
|
|
381
|
+
|
|
382
|
+
for field_def in group.fields:
|
|
383
|
+
# Skip if condition met
|
|
384
|
+
if field_def.skip_if and field_def.skip_if(context):
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
# Skip if already in context (provided via CLI)
|
|
388
|
+
if field_def.name in context:
|
|
389
|
+
value = context[field_def.name]
|
|
390
|
+
console.print(f"[dim]{field_def.prompt_text}: {value} (from CLI)[/dim]")
|
|
391
|
+
data[field_def.name] = value
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
# Get default value (can be callable)
|
|
395
|
+
default_value = field_def.default
|
|
396
|
+
if callable(default_value):
|
|
397
|
+
default_value = default_value(context, data)
|
|
398
|
+
|
|
399
|
+
# Prompt based on field type
|
|
400
|
+
value = self._prompt_field(field_def, default_value, data)
|
|
401
|
+
|
|
402
|
+
# Only add non-None values (unless required)
|
|
403
|
+
if value is not None or field_def.required:
|
|
404
|
+
data[field_def.name] = value
|
|
405
|
+
|
|
406
|
+
# Add to context for subsequent fields
|
|
407
|
+
context[field_def.name] = value
|
|
408
|
+
|
|
409
|
+
return data
|
|
410
|
+
|
|
411
|
+
def _prompt_field(self, field_def: FieldDef, default_value: Any, current_data: dict[str, Any]) -> Any:
|
|
412
|
+
"""Prompt for a single field.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
field_def: Field definition
|
|
416
|
+
default_value: Default value to suggest
|
|
417
|
+
current_data: Currently collected data (for validation)
|
|
418
|
+
|
|
419
|
+
Returns:
|
|
420
|
+
User input value
|
|
421
|
+
"""
|
|
422
|
+
# Indicate if field is optional
|
|
423
|
+
required_marker = " [red]*[/red]" if field_def.required else " [dim](optional)[/dim]"
|
|
424
|
+
prompt_label = f"[bold blue]{field_def.prompt_text}{required_marker}[/bold blue]"
|
|
425
|
+
|
|
426
|
+
if field_def.description:
|
|
427
|
+
console.print(f"[dim]{field_def.description}[/dim]")
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
if field_def.field_type == "boolean":
|
|
431
|
+
return Confirm.ask(prompt_label, default=default_value if default_value is not None else False)
|
|
432
|
+
|
|
433
|
+
elif field_def.field_type == "integer":
|
|
434
|
+
while True:
|
|
435
|
+
try:
|
|
436
|
+
if not field_def.required:
|
|
437
|
+
# Optional integer - allow empty input
|
|
438
|
+
default_str = str(default_value) if default_value is not None else ""
|
|
439
|
+
result = Prompt.ask(prompt_label, default=default_str)
|
|
440
|
+
if result == "":
|
|
441
|
+
return None
|
|
442
|
+
value = int(result)
|
|
443
|
+
elif default_value is not None:
|
|
444
|
+
value = IntPrompt.ask(prompt_label, default=default_value)
|
|
445
|
+
else:
|
|
446
|
+
value = IntPrompt.ask(prompt_label)
|
|
447
|
+
|
|
448
|
+
# Apply custom validation if provided
|
|
449
|
+
if field_def.validate:
|
|
450
|
+
value = field_def.validate(value)
|
|
451
|
+
return value
|
|
452
|
+
except ValueError:
|
|
453
|
+
console.print("[red]Please enter a valid integer[/red]")
|
|
454
|
+
|
|
455
|
+
elif field_def.field_type == "choice":
|
|
456
|
+
if not field_def.choices:
|
|
457
|
+
raise ValueError(f"Field {field_def.name} is type 'choice' but has no choices defined")
|
|
458
|
+
|
|
459
|
+
return Prompt.ask(
|
|
460
|
+
prompt_label,
|
|
461
|
+
choices=field_def.choices,
|
|
462
|
+
default=default_value if default_value else field_def.choices[0],
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
else: # string, email, uri
|
|
466
|
+
while True:
|
|
467
|
+
if not field_def.required and default_value is None:
|
|
468
|
+
# Optional field with no default - allow empty
|
|
469
|
+
result = Prompt.ask(prompt_label, default="")
|
|
470
|
+
if result == "":
|
|
471
|
+
return None
|
|
472
|
+
elif default_value is not None:
|
|
473
|
+
result = Prompt.ask(prompt_label, default=str(default_value))
|
|
474
|
+
else:
|
|
475
|
+
result = Prompt.ask(prompt_label)
|
|
476
|
+
|
|
477
|
+
# Validate input
|
|
478
|
+
if self._validate_field_value(field_def, result):
|
|
479
|
+
# Apply custom validation if provided
|
|
480
|
+
if field_def.validate:
|
|
481
|
+
try:
|
|
482
|
+
result = field_def.validate(result)
|
|
483
|
+
except Exception as e:
|
|
484
|
+
console.print(f"[red]Validation error: {e}[/red]")
|
|
485
|
+
continue
|
|
486
|
+
return result
|
|
487
|
+
else:
|
|
488
|
+
# Validation failed, loop to retry
|
|
489
|
+
pass
|
|
490
|
+
|
|
491
|
+
except KeyboardInterrupt:
|
|
492
|
+
console.print("\n[yellow]Cancelled by user[/yellow]")
|
|
493
|
+
raise typer.Abort()
|
|
494
|
+
except Exception as e:
|
|
495
|
+
console.print(f"[red]Error prompting for {field_def.name}: {e}[/red]")
|
|
496
|
+
raise
|
|
497
|
+
|
|
498
|
+
def _validate_field_value(self, field_def: FieldDef, value: Any) -> bool:
|
|
499
|
+
"""Validate field value based on field type.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
field_def: Field definition
|
|
503
|
+
value: Value to validate
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
True if valid, False otherwise (with error message printed)
|
|
507
|
+
"""
|
|
508
|
+
if value is None or value == "":
|
|
509
|
+
if field_def.required:
|
|
510
|
+
console.print("[red]This field is required[/red]")
|
|
511
|
+
return False
|
|
512
|
+
return True
|
|
513
|
+
|
|
514
|
+
# Type-specific validation
|
|
515
|
+
if field_def.field_type == "email":
|
|
516
|
+
if "@" not in value or "." not in value.split("@")[-1]:
|
|
517
|
+
console.print("[red]Please enter a valid email address[/red]")
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
elif field_def.field_type == "uri":
|
|
521
|
+
if not value.startswith(("http://", "https://")):
|
|
522
|
+
console.print("[red]URL must start with http:// or https://[/red]")
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
return True
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
# =============================================================================
|
|
529
|
+
# SELLER FIELD REGISTRY
|
|
530
|
+
# =============================================================================
|
|
531
|
+
|
|
532
|
+
SELLER_GROUPS = [
|
|
533
|
+
FieldGroup(
|
|
534
|
+
name="basic",
|
|
535
|
+
title="Basic Information",
|
|
536
|
+
fields=[
|
|
537
|
+
FieldDef(
|
|
538
|
+
name="name",
|
|
539
|
+
prompt_text="Seller ID (URL-friendly)",
|
|
540
|
+
field_type="string",
|
|
541
|
+
required=True,
|
|
542
|
+
description="Unique identifier (e.g., 'acme-corp', 'john-doe')",
|
|
543
|
+
),
|
|
544
|
+
FieldDef(
|
|
545
|
+
name="display_name",
|
|
546
|
+
prompt_text="Display name",
|
|
547
|
+
field_type="string",
|
|
548
|
+
required=False,
|
|
549
|
+
default=lambda ctx, data: ctx.get("name", "").replace("-", " ").replace("_", " ").title(),
|
|
550
|
+
description="Human-readable name (e.g., 'ACME Corporation')",
|
|
551
|
+
),
|
|
552
|
+
FieldDef(
|
|
553
|
+
name="seller_type",
|
|
554
|
+
prompt_text="Seller type",
|
|
555
|
+
field_type="choice",
|
|
556
|
+
choices=["individual", "organization", "partnership", "corporation"],
|
|
557
|
+
default="individual",
|
|
558
|
+
description="Type of seller entity",
|
|
559
|
+
),
|
|
560
|
+
],
|
|
561
|
+
),
|
|
562
|
+
FieldGroup(
|
|
563
|
+
name="contact",
|
|
564
|
+
title="Contact Information",
|
|
565
|
+
fields=[
|
|
566
|
+
FieldDef(
|
|
567
|
+
name="contact_email",
|
|
568
|
+
prompt_text="Primary contact email",
|
|
569
|
+
field_type="email",
|
|
570
|
+
required=True,
|
|
571
|
+
),
|
|
572
|
+
FieldDef(
|
|
573
|
+
name="secondary_contact_email",
|
|
574
|
+
prompt_text="Secondary contact email",
|
|
575
|
+
field_type="email",
|
|
576
|
+
required=False,
|
|
577
|
+
),
|
|
578
|
+
FieldDef(
|
|
579
|
+
name="homepage",
|
|
580
|
+
prompt_text="Homepage URL",
|
|
581
|
+
field_type="uri",
|
|
582
|
+
required=False,
|
|
583
|
+
),
|
|
584
|
+
],
|
|
585
|
+
),
|
|
586
|
+
FieldGroup(
|
|
587
|
+
name="details",
|
|
588
|
+
title="Additional Details",
|
|
589
|
+
fields=[
|
|
590
|
+
FieldDef(
|
|
591
|
+
name="description",
|
|
592
|
+
prompt_text="Description",
|
|
593
|
+
field_type="string",
|
|
594
|
+
required=False,
|
|
595
|
+
default=lambda ctx, data: f"{ctx.get('name', 'seller')} - {data.get('seller_type', 'seller')}",
|
|
596
|
+
),
|
|
597
|
+
FieldDef(
|
|
598
|
+
name="account_manager",
|
|
599
|
+
prompt_text="Account manager email",
|
|
600
|
+
field_type="email",
|
|
601
|
+
required=False,
|
|
602
|
+
description="Email of the user managing this seller account (must be a registered user)",
|
|
603
|
+
),
|
|
604
|
+
FieldDef(
|
|
605
|
+
name="business_registration",
|
|
606
|
+
prompt_text="Business registration number",
|
|
607
|
+
field_type="string",
|
|
608
|
+
required=False,
|
|
609
|
+
skip_if=lambda ctx: ctx.get("seller_type") == "individual",
|
|
610
|
+
description="Required for organizations",
|
|
611
|
+
),
|
|
612
|
+
FieldDef(
|
|
613
|
+
name="tax_id",
|
|
614
|
+
prompt_text="Tax ID (EIN, VAT, etc.)",
|
|
615
|
+
field_type="string",
|
|
616
|
+
required=False,
|
|
617
|
+
skip_if=lambda ctx: ctx.get("seller_type") == "individual",
|
|
618
|
+
),
|
|
619
|
+
],
|
|
620
|
+
),
|
|
621
|
+
FieldGroup(
|
|
622
|
+
name="status",
|
|
623
|
+
title="Status & Verification",
|
|
624
|
+
fields=[
|
|
625
|
+
FieldDef(
|
|
626
|
+
name="status",
|
|
627
|
+
prompt_text="Status",
|
|
628
|
+
field_type="choice",
|
|
629
|
+
choices=["active", "pending", "disabled", "incomplete"],
|
|
630
|
+
default="active",
|
|
631
|
+
),
|
|
632
|
+
FieldDef(
|
|
633
|
+
name="is_verified",
|
|
634
|
+
prompt_text="Is verified (KYC complete)?",
|
|
635
|
+
field_type="boolean",
|
|
636
|
+
default=False,
|
|
637
|
+
),
|
|
638
|
+
],
|
|
639
|
+
),
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
# =============================================================================
|
|
644
|
+
# PROVIDER FIELD REGISTRY
|
|
645
|
+
# =============================================================================
|
|
646
|
+
|
|
647
|
+
PROVIDER_GROUPS = [
|
|
648
|
+
FieldGroup(
|
|
649
|
+
name="basic",
|
|
650
|
+
title="Basic Information",
|
|
651
|
+
fields=[
|
|
652
|
+
FieldDef(
|
|
653
|
+
name="name",
|
|
654
|
+
prompt_text="Provider ID (URL-friendly)",
|
|
655
|
+
field_type="string",
|
|
656
|
+
required=True,
|
|
657
|
+
description="Unique identifier (e.g., 'openai', 'fireworks')",
|
|
658
|
+
),
|
|
659
|
+
FieldDef(
|
|
660
|
+
name="display_name",
|
|
661
|
+
prompt_text="Display name",
|
|
662
|
+
field_type="string",
|
|
663
|
+
required=False,
|
|
664
|
+
default=lambda ctx, data: ctx.get("name", "").replace("-", " ").replace("_", " ").title(),
|
|
665
|
+
description="Human-readable name (e.g., 'OpenAI', 'Fireworks.ai')",
|
|
666
|
+
),
|
|
667
|
+
FieldDef(
|
|
668
|
+
name="description",
|
|
669
|
+
prompt_text="Description",
|
|
670
|
+
field_type="string",
|
|
671
|
+
required=False,
|
|
672
|
+
),
|
|
673
|
+
],
|
|
674
|
+
),
|
|
675
|
+
FieldGroup(
|
|
676
|
+
name="contact",
|
|
677
|
+
title="Contact & Web",
|
|
678
|
+
fields=[
|
|
679
|
+
FieldDef(
|
|
680
|
+
name="contact_email",
|
|
681
|
+
prompt_text="Contact email",
|
|
682
|
+
field_type="email",
|
|
683
|
+
required=True,
|
|
684
|
+
),
|
|
685
|
+
FieldDef(
|
|
686
|
+
name="secondary_contact_email",
|
|
687
|
+
prompt_text="Secondary contact email",
|
|
688
|
+
field_type="email",
|
|
689
|
+
required=False,
|
|
690
|
+
),
|
|
691
|
+
FieldDef(
|
|
692
|
+
name="homepage",
|
|
693
|
+
prompt_text="Homepage URL",
|
|
694
|
+
field_type="uri",
|
|
695
|
+
required=True,
|
|
696
|
+
),
|
|
697
|
+
],
|
|
698
|
+
),
|
|
699
|
+
FieldGroup(
|
|
700
|
+
name="access",
|
|
701
|
+
title="Provider Access (API Credentials)",
|
|
702
|
+
fields=[
|
|
703
|
+
FieldDef(
|
|
704
|
+
name="base_url",
|
|
705
|
+
prompt_text="API endpoint URL",
|
|
706
|
+
field_type="uri",
|
|
707
|
+
required=True,
|
|
708
|
+
description="Base URL for API access (e.g., 'https://api.openai.com/v1')",
|
|
709
|
+
),
|
|
710
|
+
FieldDef(
|
|
711
|
+
name="api_key",
|
|
712
|
+
prompt_text="API key (optional, can be set later)",
|
|
713
|
+
field_type="string",
|
|
714
|
+
required=False,
|
|
715
|
+
description="Leave empty if you'll set it later or use env var",
|
|
716
|
+
),
|
|
717
|
+
FieldDef(
|
|
718
|
+
name="access_method",
|
|
719
|
+
prompt_text="Access method",
|
|
720
|
+
field_type="choice",
|
|
721
|
+
choices=["http", "websocket", "grpc"],
|
|
722
|
+
default="http",
|
|
723
|
+
),
|
|
724
|
+
],
|
|
725
|
+
),
|
|
726
|
+
FieldGroup(
|
|
727
|
+
name="status",
|
|
728
|
+
title="Status",
|
|
729
|
+
fields=[
|
|
730
|
+
FieldDef(
|
|
731
|
+
name="status",
|
|
732
|
+
prompt_text="Provider status",
|
|
733
|
+
field_type="choice",
|
|
734
|
+
choices=["active", "pending", "disabled", "incomplete"],
|
|
735
|
+
default="active",
|
|
736
|
+
),
|
|
737
|
+
],
|
|
738
|
+
),
|
|
739
|
+
FieldGroup(
|
|
740
|
+
name="automation",
|
|
741
|
+
title="Service Population (Optional)",
|
|
742
|
+
fields=[
|
|
743
|
+
FieldDef(
|
|
744
|
+
name="enable_services_populator",
|
|
745
|
+
prompt_text="Enable automated service population?",
|
|
746
|
+
field_type="boolean",
|
|
747
|
+
required=False,
|
|
748
|
+
default=False,
|
|
749
|
+
description="Use a script to automatically populate service offerings and listings",
|
|
750
|
+
),
|
|
751
|
+
FieldDef(
|
|
752
|
+
name="populator_command",
|
|
753
|
+
prompt_text="Populator script command",
|
|
754
|
+
field_type="string",
|
|
755
|
+
required=False,
|
|
756
|
+
skip_if=lambda ctx: not ctx.get("enable_services_populator", False),
|
|
757
|
+
description="Command to execute (e.g., 'python scripts/populate.py'). Run by 'usvc populate'",
|
|
758
|
+
),
|
|
759
|
+
],
|
|
760
|
+
),
|
|
761
|
+
]
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# =============================================================================
|
|
765
|
+
# SERVICE OFFERING FIELD REGISTRY
|
|
766
|
+
# =============================================================================
|
|
767
|
+
|
|
768
|
+
OFFERING_GROUPS = [
|
|
769
|
+
FieldGroup(
|
|
770
|
+
name="basic",
|
|
771
|
+
title="Basic Information",
|
|
772
|
+
fields=[
|
|
773
|
+
FieldDef(
|
|
774
|
+
name="name",
|
|
775
|
+
prompt_text="Service name (e.g., 'gpt-4', 'llama-3-1-405b')",
|
|
776
|
+
field_type="string",
|
|
777
|
+
required=True,
|
|
778
|
+
description="Usually the model name or service identifier",
|
|
779
|
+
),
|
|
780
|
+
FieldDef(
|
|
781
|
+
name="display_name",
|
|
782
|
+
prompt_text="Display name",
|
|
783
|
+
field_type="string",
|
|
784
|
+
required=True,
|
|
785
|
+
default=lambda ctx, data: ctx.get("name", "").replace("-", " ").title(),
|
|
786
|
+
),
|
|
787
|
+
FieldDef(
|
|
788
|
+
name="version",
|
|
789
|
+
prompt_text="Version",
|
|
790
|
+
field_type="string",
|
|
791
|
+
required=False,
|
|
792
|
+
default=None,
|
|
793
|
+
),
|
|
794
|
+
FieldDef(
|
|
795
|
+
name="description",
|
|
796
|
+
prompt_text="Description",
|
|
797
|
+
field_type="string",
|
|
798
|
+
required=True,
|
|
799
|
+
description="Brief description of the service",
|
|
800
|
+
),
|
|
801
|
+
],
|
|
802
|
+
),
|
|
803
|
+
FieldGroup(
|
|
804
|
+
name="classification",
|
|
805
|
+
title="Service Classification",
|
|
806
|
+
fields=[
|
|
807
|
+
FieldDef(
|
|
808
|
+
name="service_type",
|
|
809
|
+
prompt_text="Service type",
|
|
810
|
+
field_type="choice",
|
|
811
|
+
choices=["llm", "embedding", "vision", "audio", "image", "video", "other"],
|
|
812
|
+
default="llm",
|
|
813
|
+
required=True,
|
|
814
|
+
),
|
|
815
|
+
FieldDef(
|
|
816
|
+
name="upstream_status",
|
|
817
|
+
prompt_text="Upstream status",
|
|
818
|
+
field_type="choice",
|
|
819
|
+
choices=["uploading", "ready", "deprecated"],
|
|
820
|
+
default="ready",
|
|
821
|
+
),
|
|
822
|
+
],
|
|
823
|
+
),
|
|
824
|
+
FieldGroup(
|
|
825
|
+
name="access",
|
|
826
|
+
title="Upstream Access Interface",
|
|
827
|
+
fields=[
|
|
828
|
+
FieldDef(
|
|
829
|
+
name="upstream_base_url",
|
|
830
|
+
prompt_text="Upstream API endpoint URL",
|
|
831
|
+
field_type="uri",
|
|
832
|
+
required=True,
|
|
833
|
+
description="Base URL for accessing this service upstream",
|
|
834
|
+
),
|
|
835
|
+
FieldDef(
|
|
836
|
+
name="upstream_api_key",
|
|
837
|
+
prompt_text="Upstream API key (optional)",
|
|
838
|
+
field_type="string",
|
|
839
|
+
required=False,
|
|
840
|
+
description="Leave empty if using provider's API key",
|
|
841
|
+
),
|
|
842
|
+
FieldDef(
|
|
843
|
+
name="add_upstream_documents",
|
|
844
|
+
prompt_text="Add documents to upstream access interface?",
|
|
845
|
+
field_type="boolean",
|
|
846
|
+
default=False,
|
|
847
|
+
description="API docs, code examples, etc. for accessing the upstream service",
|
|
848
|
+
),
|
|
849
|
+
],
|
|
850
|
+
),
|
|
851
|
+
FieldGroup(
|
|
852
|
+
name="pricing",
|
|
853
|
+
title="Seller Pricing (Optional)",
|
|
854
|
+
fields=[
|
|
855
|
+
FieldDef(
|
|
856
|
+
name="add_seller_pricing",
|
|
857
|
+
prompt_text="Add seller pricing information?",
|
|
858
|
+
field_type="boolean",
|
|
859
|
+
default=False,
|
|
860
|
+
description="The agreed rate between seller and UnitySVC",
|
|
861
|
+
),
|
|
862
|
+
],
|
|
863
|
+
),
|
|
864
|
+
FieldGroup(
|
|
865
|
+
name="additional",
|
|
866
|
+
title="Additional Information (Optional)",
|
|
867
|
+
fields=[
|
|
868
|
+
FieldDef(
|
|
869
|
+
name="tagline",
|
|
870
|
+
prompt_text="Tagline",
|
|
871
|
+
field_type="string",
|
|
872
|
+
required=False,
|
|
873
|
+
description="Short elevator pitch for the service",
|
|
874
|
+
),
|
|
875
|
+
],
|
|
876
|
+
),
|
|
877
|
+
]
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
# =============================================================================
|
|
881
|
+
# SERVICE LISTING FIELD REGISTRY
|
|
882
|
+
# =============================================================================
|
|
883
|
+
|
|
884
|
+
LISTING_GROUPS = [
|
|
885
|
+
FieldGroup(
|
|
886
|
+
name="basic",
|
|
887
|
+
title="Basic Information",
|
|
888
|
+
fields=[
|
|
889
|
+
FieldDef(
|
|
890
|
+
name="service_name",
|
|
891
|
+
prompt_text="Service name (must match service.json)",
|
|
892
|
+
field_type="string",
|
|
893
|
+
required=False,
|
|
894
|
+
default=lambda ctx, data: find_service_name(),
|
|
895
|
+
description="Auto-detected from service.json in current directory",
|
|
896
|
+
),
|
|
897
|
+
FieldDef(
|
|
898
|
+
name="name",
|
|
899
|
+
prompt_text="Listing identifier",
|
|
900
|
+
field_type="string",
|
|
901
|
+
required=False,
|
|
902
|
+
description="Name identifier for the service listing (defaults to filename)",
|
|
903
|
+
),
|
|
904
|
+
FieldDef(
|
|
905
|
+
name="display_name",
|
|
906
|
+
prompt_text="Display name",
|
|
907
|
+
field_type="string",
|
|
908
|
+
required=False,
|
|
909
|
+
description="Human-readable listing name (e.g., 'Premium GPT-4 Access')",
|
|
910
|
+
),
|
|
911
|
+
],
|
|
912
|
+
),
|
|
913
|
+
FieldGroup(
|
|
914
|
+
name="seller",
|
|
915
|
+
title="Seller Information",
|
|
916
|
+
fields=[
|
|
917
|
+
FieldDef(
|
|
918
|
+
name="seller_name",
|
|
919
|
+
prompt_text="Seller name (must match seller.json)",
|
|
920
|
+
field_type="string",
|
|
921
|
+
required=False,
|
|
922
|
+
default=lambda ctx, data: find_seller_name(),
|
|
923
|
+
description="Auto-detected from seller.json in data directory",
|
|
924
|
+
),
|
|
925
|
+
],
|
|
926
|
+
),
|
|
927
|
+
FieldGroup(
|
|
928
|
+
name="status",
|
|
929
|
+
title="Status",
|
|
930
|
+
fields=[
|
|
931
|
+
FieldDef(
|
|
932
|
+
name="listing_status",
|
|
933
|
+
prompt_text="Listing status",
|
|
934
|
+
field_type="choice",
|
|
935
|
+
choices=["draft", "ready", "deprecated"],
|
|
936
|
+
default="draft",
|
|
937
|
+
),
|
|
938
|
+
],
|
|
939
|
+
),
|
|
940
|
+
FieldGroup(
|
|
941
|
+
name="documents",
|
|
942
|
+
title="Documents (Optional)",
|
|
943
|
+
fields=[
|
|
944
|
+
FieldDef(
|
|
945
|
+
name="add_documents",
|
|
946
|
+
prompt_text="Add documents (SLA, guides, etc.)?",
|
|
947
|
+
field_type="boolean",
|
|
948
|
+
default=False,
|
|
949
|
+
description="Documents provide additional information about the listing",
|
|
950
|
+
),
|
|
951
|
+
],
|
|
952
|
+
),
|
|
953
|
+
]
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
# =============================================================================
|
|
957
|
+
# HELPER FUNCTIONS
|
|
958
|
+
# =============================================================================
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def create_seller_data(user_input: dict[str, Any]) -> dict[str, Any]:
|
|
962
|
+
"""Create seller data structure from user input.
|
|
963
|
+
|
|
964
|
+
Args:
|
|
965
|
+
user_input: User-provided field values
|
|
966
|
+
|
|
967
|
+
Returns:
|
|
968
|
+
Complete seller data dictionary
|
|
969
|
+
"""
|
|
970
|
+
data = {
|
|
971
|
+
"schema": "seller_v1",
|
|
972
|
+
"time_created": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
# Add all non-None user input
|
|
976
|
+
for key, value in user_input.items():
|
|
977
|
+
if value is not None:
|
|
978
|
+
data[key] = value
|
|
979
|
+
|
|
980
|
+
return data
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
def create_provider_data(user_input: dict[str, Any]) -> dict[str, Any]:
|
|
984
|
+
"""Create provider data structure from user input.
|
|
985
|
+
|
|
986
|
+
Args:
|
|
987
|
+
user_input: User-provided field values
|
|
988
|
+
|
|
989
|
+
Returns:
|
|
990
|
+
Complete provider data dictionary
|
|
991
|
+
"""
|
|
992
|
+
# Extract access interface fields
|
|
993
|
+
access_fields = ["base_url", "api_key", "access_method"]
|
|
994
|
+
provider_access_info: dict[str, Any] = {}
|
|
995
|
+
|
|
996
|
+
for key in access_fields:
|
|
997
|
+
if key in user_input and user_input[key] is not None:
|
|
998
|
+
provider_access_info[key] = user_input[key]
|
|
999
|
+
|
|
1000
|
+
# Extract services_populator fields
|
|
1001
|
+
populator_fields = ["enable_services_populator", "populator_command"]
|
|
1002
|
+
services_populator: dict[str, Any] | None = None
|
|
1003
|
+
|
|
1004
|
+
if user_input.get("enable_services_populator") and user_input.get("populator_command"):
|
|
1005
|
+
services_populator = {"command": user_input["populator_command"]}
|
|
1006
|
+
|
|
1007
|
+
# Create base data
|
|
1008
|
+
data = {
|
|
1009
|
+
"schema": "provider_v1",
|
|
1010
|
+
"time_created": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
1011
|
+
"provider_access_info": provider_access_info,
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
# Add services_populator if configured
|
|
1015
|
+
if services_populator:
|
|
1016
|
+
data["services_populator"] = services_populator
|
|
1017
|
+
|
|
1018
|
+
# Add non-access, non-populator fields
|
|
1019
|
+
excluded_fields = access_fields + populator_fields
|
|
1020
|
+
for key, value in user_input.items():
|
|
1021
|
+
if key not in excluded_fields and value is not None:
|
|
1022
|
+
data[key] = value
|
|
1023
|
+
|
|
1024
|
+
return data
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def create_offering_data(user_input: dict[str, Any], offering_dir: Path | None = None) -> dict[str, Any]:
|
|
1028
|
+
"""Create service offering data structure from user input.
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
user_input: User-provided field values
|
|
1032
|
+
offering_dir: Directory where offering file will be created (for validating document file paths)
|
|
1033
|
+
|
|
1034
|
+
Returns:
|
|
1035
|
+
Complete service offering data dictionary
|
|
1036
|
+
"""
|
|
1037
|
+
# Extract upstream access interface fields
|
|
1038
|
+
upstream_fields = ["upstream_base_url", "upstream_api_key", "add_upstream_documents"]
|
|
1039
|
+
upstream_access_interface: dict[str, Any] = {}
|
|
1040
|
+
|
|
1041
|
+
for key in upstream_fields:
|
|
1042
|
+
# Map to the actual field names in AccessInterface
|
|
1043
|
+
if key == "upstream_base_url" and user_input.get(key):
|
|
1044
|
+
upstream_access_interface["base_url"] = user_input[key]
|
|
1045
|
+
elif key == "upstream_api_key" and user_input.get(key):
|
|
1046
|
+
upstream_access_interface["api_key"] = user_input[key]
|
|
1047
|
+
|
|
1048
|
+
# Handle documents for upstream access interface if user wants to add them
|
|
1049
|
+
if user_input.get("add_upstream_documents"):
|
|
1050
|
+
if offering_dir is None:
|
|
1051
|
+
console.print("[yellow]Warning: Cannot validate file paths without offering directory[/yellow]")
|
|
1052
|
+
offering_dir = Path.cwd()
|
|
1053
|
+
|
|
1054
|
+
console.print("\n[bold cyan]Add documents to upstream access interface[/bold cyan]")
|
|
1055
|
+
documents = []
|
|
1056
|
+
while True:
|
|
1057
|
+
doc = prompt_for_document(offering_dir)
|
|
1058
|
+
documents.append(doc)
|
|
1059
|
+
|
|
1060
|
+
if not Confirm.ask("\n[bold blue]Add another document?[/bold blue]", default=False):
|
|
1061
|
+
break
|
|
1062
|
+
|
|
1063
|
+
if documents:
|
|
1064
|
+
upstream_access_interface["documents"] = documents
|
|
1065
|
+
|
|
1066
|
+
# Handle seller pricing if user wants to add it
|
|
1067
|
+
seller_price = None
|
|
1068
|
+
if user_input.get("add_seller_pricing"):
|
|
1069
|
+
seller_price = prompt_for_pricing()
|
|
1070
|
+
|
|
1071
|
+
# Create base data
|
|
1072
|
+
data = {
|
|
1073
|
+
"schema": "service_v1",
|
|
1074
|
+
"time_created": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
1075
|
+
"upstream_access_interface": upstream_access_interface,
|
|
1076
|
+
"details": {}, # Required field, user can add details manually later
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
# Add seller price if provided
|
|
1080
|
+
if seller_price:
|
|
1081
|
+
data["seller_price"] = seller_price
|
|
1082
|
+
|
|
1083
|
+
# Add non-upstream fields (exclude add_upstream_documents and add_seller_pricing which are just flags)
|
|
1084
|
+
excluded_fields = upstream_fields + ["add_seller_pricing"]
|
|
1085
|
+
for key, value in user_input.items():
|
|
1086
|
+
if key not in excluded_fields and value is not None:
|
|
1087
|
+
data[key] = value
|
|
1088
|
+
|
|
1089
|
+
return data
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
def create_listing_data(user_input: dict[str, Any], listing_dir: Path | None = None) -> dict[str, Any]:
|
|
1093
|
+
"""Create service listing data structure from user input.
|
|
1094
|
+
|
|
1095
|
+
Args:
|
|
1096
|
+
user_input: User-provided field values
|
|
1097
|
+
listing_dir: Directory where listing file will be created (for validating document file paths)
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Complete service listing data dictionary
|
|
1101
|
+
"""
|
|
1102
|
+
data: dict[str, Any] = {
|
|
1103
|
+
"schema": "listing_v1",
|
|
1104
|
+
"time_created": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
|
|
1105
|
+
"user_access_interfaces": [], # Required field, user must add interfaces manually
|
|
1106
|
+
"customer_price": None, # Optional, can be added later
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
# Handle documents if user wants to add them
|
|
1110
|
+
documents = []
|
|
1111
|
+
if user_input.get("add_documents"):
|
|
1112
|
+
if listing_dir is None:
|
|
1113
|
+
console.print("[yellow]Warning: Cannot validate file paths without listing directory[/yellow]")
|
|
1114
|
+
listing_dir = Path.cwd()
|
|
1115
|
+
|
|
1116
|
+
console.print("\n[bold cyan]Add documents to listing[/bold cyan]")
|
|
1117
|
+
while True:
|
|
1118
|
+
doc = prompt_for_document(listing_dir)
|
|
1119
|
+
documents.append(doc)
|
|
1120
|
+
|
|
1121
|
+
if not Confirm.ask("\n[bold blue]Add another document?[/bold blue]", default=False):
|
|
1122
|
+
break
|
|
1123
|
+
|
|
1124
|
+
if documents:
|
|
1125
|
+
data["documents"] = documents
|
|
1126
|
+
|
|
1127
|
+
# Add all non-None user input (except add_documents which is just a flag)
|
|
1128
|
+
for key, value in user_input.items():
|
|
1129
|
+
if key != "add_documents" and value is not None:
|
|
1130
|
+
data[key] = value
|
|
1131
|
+
|
|
1132
|
+
return data
|