shaped 1.0.1__py3-none-any.whl → 2.0.0__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.
shaped/cli/shaped_cli.py CHANGED
@@ -1,8 +1,6 @@
1
1
  import json
2
2
  import sys
3
- import urllib
4
3
  from pathlib import Path
5
- from typing import List, Optional
6
4
 
7
5
  import pandas as pd
8
6
  import pyarrow.parquet as pq
@@ -40,7 +38,7 @@ def _read_config() -> Config:
40
38
 
41
39
 
42
40
  def _get_shaped_url(config: Config) -> str:
43
- return f"https://api.{config.env}.shaped.ai/v1"
41
+ return f"https://api.{config.env}.shaped.ai/v2"
44
42
 
45
43
 
46
44
  def _parse_file_as_json(file: typer.FileText) -> str:
@@ -64,19 +62,45 @@ def _parse_response_as_yaml(content: str) -> str:
64
62
 
65
63
 
66
64
  @app.command()
67
- def init(api_key: str = typer.Option(...), env: str = typer.Option("prod")):
65
+ def init(
66
+ api_key: str = typer.Option(..., help="Your Shaped API key."),
67
+ env: str = typer.Option("prod", help="Environment to use (e.g., prod, dev, staging)."),
68
+ ):
69
+ """
70
+ Initialize the Shaped CLI with your API key and environment.
71
+
72
+ This command saves your configuration locally so you don't need to
73
+ provide your API key for every command.
74
+ """
68
75
  config = Config(api_key=api_key, env=env)
69
76
  _write_config(config)
70
77
  typer.echo(f"Initializing with config: {config.dict()}")
71
78
 
72
79
 
80
+ ##############
81
+ # ENGINE API #
82
+ ##############
83
+
73
84
  @app.command()
74
- def create_model(
75
- file: typer.FileText = typer.Option(None),
85
+ def create_engine(
86
+ file: typer.FileText = typer.Option(
87
+ None, help="Path to a JSON or YAML file containing the engine configuration."
88
+ ),
76
89
  ):
90
+ """
91
+ Create a new engine.
92
+
93
+ The engine configuration can be provided via:
94
+ - --file: Path to a JSON/YAML file
95
+ - stdin: Pipe JSON/YAML content
96
+ """
77
97
  config = _read_config()
78
- url = f"{_get_shaped_url(config)}/models"
79
- headers = {"accept": "application/json", "x-api-key": config.api_key}
98
+ url = f"{_get_shaped_url(config)}/engines"
99
+ headers = {
100
+ "accept": "application/json",
101
+ "content-type": "application/json",
102
+ "x-api-key": config.api_key,
103
+ }
80
104
 
81
105
  if not sys.stdin.isatty():
82
106
  payload = sys.stdin.read()
@@ -87,14 +111,32 @@ def create_model(
87
111
 
88
112
  typer.echo(payload)
89
113
  response = requests.post(url, headers=headers, data=payload)
114
+ if response.status_code not in [200, 201]:
115
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
116
+ raise typer.Exit(1)
90
117
  typer.echo(_parse_response_as_yaml(response.text))
91
118
 
92
119
 
93
120
  @app.command()
94
- def update_model(file: typer.FileText = typer.Option(None)):
121
+ def update_engine(
122
+ file: typer.FileText = typer.Option(
123
+ None, help="Path to a JSON or YAML file containing the engine configuration."
124
+ ),
125
+ ):
126
+ """
127
+ Update an existing engine.
128
+
129
+ The engine configuration can be provided via:
130
+ - --file: Path to a JSON/YAML file
131
+ - stdin: Pipe JSON/YAML content
132
+ """
95
133
  config = _read_config()
96
- url = f"{_get_shaped_url(config)}/models"
97
- headers = {"accept": "application/json", "x-api-key": config.api_key}
134
+ url = f"{_get_shaped_url(config)}/engines"
135
+ headers = {
136
+ "accept": "application/json",
137
+ "content-type": "application/json",
138
+ "x-api-key": config.api_key,
139
+ }
98
140
 
99
141
  if not sys.stdin.isatty():
100
142
  payload = sys.stdin.read()
@@ -105,150 +147,80 @@ def update_model(file: typer.FileText = typer.Option(None)):
105
147
 
106
148
  typer.echo(payload)
107
149
  response = requests.patch(url, headers=headers, data=payload)
150
+ if response.status_code != 200:
151
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
152
+ raise typer.Exit(1)
108
153
  typer.echo(_parse_response_as_yaml(response.text))
109
154
 
110
155
 
111
156
  @app.command()
112
- def list_models():
113
- config = _read_config()
114
- url = f"{_get_shaped_url(config)}/models"
115
- headers = {"accept": "application/json", "x-api-key": config.api_key}
116
- response = requests.get(url, headers=headers)
117
- typer.echo(_parse_response_as_yaml(response.text))
118
-
119
-
120
- @app.command()
121
- def view_model(model_name: str = typer.Option(...)):
157
+ def list_engines():
158
+ """
159
+ List all engines.
160
+ """
122
161
  config = _read_config()
123
- url = f"{_get_shaped_url(config)}/models/{model_name}"
162
+ url = f"{_get_shaped_url(config)}/engines"
124
163
  headers = {"accept": "application/json", "x-api-key": config.api_key}
125
164
  response = requests.get(url, headers=headers)
165
+ if response.status_code != 200:
166
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
167
+ raise typer.Exit(1)
126
168
  typer.echo(_parse_response_as_yaml(response.text))
127
169
 
128
170
 
129
171
  @app.command()
130
- def delete_model(model_name: str = typer.Option(...)):
131
- config = _read_config()
132
- url = f"{_get_shaped_url(config)}/models/{model_name}"
133
- headers = {"accept": "application/json", "x-api-key": config.api_key}
134
- response = requests.delete(url, headers=headers)
135
- typer.echo(_parse_response_as_yaml(response.text))
136
-
137
-
138
- @app.command()
139
- def rank(
140
- model_name: str = typer.Option(...),
141
- user_id=typer.Option(None),
142
- limit=typer.Option(15),
143
- filter_predicate=typer.Option(None),
144
- text_query: str = typer.Option(None),
145
- return_metadata: bool = typer.Option(False),
146
- flush_paginations: bool = typer.Option(False),
147
- exploration_factor: float = typer.Option(0.0),
148
- diversity_factor: float = typer.Option(0.0),
149
- extra_query_args: str = typer.Option(None),
172
+ def view_engine(
173
+ engine_name: str = typer.Option(..., help="Name of the engine to view."),
150
174
  ):
175
+ """
176
+ View the configuration of a specific engine.
177
+ """
151
178
  config = _read_config()
152
- url = f"{_get_shaped_url(config)}/models/{model_name}/rank"
179
+ url = f"{_get_shaped_url(config)}/engines/{engine_name}"
153
180
  headers = {"accept": "application/json", "x-api-key": config.api_key}
154
- if exploration_factor < 0.0 or exploration_factor > 1.0:
155
- raise typer.BadParameter("`exploration_factor` must be between 0.0 and 1.0")
156
-
157
- if diversity_factor < 0.0:
158
- raise typer.BadParameter("`diversity_factor` must be greater than 0.0")
159
-
160
- query_args = {
161
- "exploration_factor": exploration_factor,
162
- "diversity_factor": diversity_factor,
163
- "limit": str(limit),
164
- }
165
-
166
- if user_id is not None:
167
- query_args |= {"user_id": user_id}
168
- if filter_predicate is not None:
169
- query_args |= {"filter_predicate": filter_predicate}
170
- if return_metadata is not None:
171
- query_args |= {"return_metadata": bool(return_metadata)}
172
- if text_query is not None:
173
- query_args |= {"text_query": text_query}
174
- if flush_paginations is not None:
175
- query_args |= {"flush_paginations": flush_paginations}
176
- if extra_query_args is not None:
177
- extra_query_args = json.loads(extra_query_args)
178
- query_args |= extra_query_args
179
-
180
- response = requests.post(url, headers=headers, json=query_args)
181
+ response = requests.get(url, headers=headers)
182
+ if response.status_code != 200:
183
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
184
+ raise typer.Exit(1)
181
185
  typer.echo(_parse_response_as_yaml(response.text))
182
186
 
183
187
 
184
188
  @app.command()
185
- def complement_items(
186
- model_name: str = typer.Option(...),
187
- item_ids: List[str] = typer.Option(...),
188
- user_id: Optional[str] = typer.Option(None),
189
- return_metadata: bool = typer.Option(False),
190
- limit=typer.Option(15),
189
+ def delete_engine(
190
+ engine_name: str = typer.Option(..., help="Name of the engine to delete."),
191
191
  ):
192
+ """
193
+ Delete an engine.
194
+ """
192
195
  config = _read_config()
193
- url = f"{_get_shaped_url(config)}/models/{model_name}/complement_items"
196
+ url = f"{_get_shaped_url(config)}/engines/{engine_name}"
194
197
  headers = {"accept": "application/json", "x-api-key": config.api_key}
195
- query_args = {
196
- "config": {
197
- "limit": str(limit),
198
- },
199
- "item_ids": item_ids,
200
- }
201
- if user_id is not None:
202
- query_args |= {"user_id": user_id}
203
- if return_metadata is not None:
204
- query_args |= {"return_metadata": bool(return_metadata)}
205
-
206
- response = requests.post(url, headers=headers, json=query_args)
207
- typer.echo(_parse_response_as_yaml(response.text))
198
+ response = requests.delete(url, headers=headers)
199
+ if response.status_code not in [200, 204]:
200
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
201
+ raise typer.Exit(1)
202
+ if response.text:
203
+ typer.echo(_parse_response_as_yaml(response.text))
208
204
 
209
205
 
210
- @app.command()
211
- def similar_items(
212
- model_name: str = typer.Option(...),
213
- item_id: str = typer.Option(...),
214
- user_id: Optional[str] = typer.Option(None),
215
- limit=typer.Option(15),
216
- extra_query_args: str = typer.Option(None),
217
- ):
218
- config = _read_config()
219
- item_id = urllib.parse.quote(item_id, safe="/", encoding=None, errors=None)
220
- path = f"/models/{model_name}/similar_items"
221
- path += f"?limit={limit}&item_id={item_id}"
222
- if user_id is not None:
223
- user_id = urllib.parse.quote(user_id, safe="/", encoding=None, errors=None)
224
- path += f"&user_id={user_id}"
225
-
226
- url = f"{_get_shaped_url(config)}{path}"
227
- headers = {"accept": "application/json", "x-api-key": config.api_key}
228
- response = requests.get(url, headers=headers)
229
- typer.echo(_parse_response_as_yaml(response.text))
206
+ ##############
207
+ # TABLE API #
208
+ ##############
230
209
 
231
210
 
232
211
  @app.command()
233
- def similar_users(
234
- model_name: str = typer.Option(...),
235
- user_id: str = typer.Option(...),
236
- limit=typer.Option(15),
237
- ):
238
- config = _read_config()
239
- url = f"{_get_shaped_url(config)}/models/{model_name}/similar_users"
240
- url += f"?limit={limit}&user_id={user_id}"
241
- headers = {"accept": "application/json", "x-api-key": config.api_key}
242
- response = requests.get(url, headers=headers)
243
- typer.echo(_parse_response_as_yaml(response.text))
244
-
245
-
246
- @app.command()
247
- def create_dataset_from_uri(
248
- name: str = typer.Option(...),
249
- path: str = typer.Option(...),
250
- type: str = typer.Option(...),
212
+ def create_table_from_uri(
213
+ name: str = typer.Option(..., help="Name of the table to create."),
214
+ path: str = typer.Option(..., help="Path to the data file."),
215
+ type: str = typer.Option(
216
+ ..., help="File type. One of: parquet, csv, tsv, json, jsonl."
217
+ ),
251
218
  ):
219
+ """
220
+ Create a table from a data file and automatically insert the data.
221
+
222
+ The table schema is inferred from the first chunk of data.
223
+ """
252
224
  chunks = _read_chunks(path, type, chunk_size=1)
253
225
  chunk = next(iter(chunks))
254
226
  if chunk is None:
@@ -259,18 +231,29 @@ def create_dataset_from_uri(
259
231
  raise ValueError("No columns found.")
260
232
 
261
233
  chunk_column_definitions = {col: "String" for col in chunk_columns}
262
- dataset_definition = {
263
- "dataset_name": name,
234
+ table_definition = {
235
+ "name": name,
264
236
  "schema_type": "CUSTOM",
265
- "schema": chunk_column_definitions,
237
+ "column_schema": chunk_column_definitions,
266
238
  }
267
- dataset_created = _create_dataset_from_payload(json.dumps(dataset_definition))
268
- if dataset_created:
269
- dataset_insert(name, path, type)
239
+ table_created = _create_table_from_payload(json.dumps(table_definition))
240
+ if table_created:
241
+ table_insert(name, path, type)
270
242
 
271
243
 
272
244
  @app.command()
273
- def create_dataset(file: typer.FileText = typer.Option(None)):
245
+ def create_table(
246
+ file: typer.FileText = typer.Option(
247
+ None, help="Path to a JSON or YAML file containing the table configuration."
248
+ ),
249
+ ):
250
+ """
251
+ Create a new table.
252
+
253
+ The table configuration can be provided via:
254
+ - --file: Path to a JSON/YAML file
255
+ - stdin: Pipe JSON/YAML content
256
+ """
274
257
  if not sys.stdin.isatty():
275
258
  payload = sys.stdin.read()
276
259
  elif file is not None:
@@ -278,42 +261,78 @@ def create_dataset(file: typer.FileText = typer.Option(None)):
278
261
  else:
279
262
  raise ValueError("Must provide either a '--file' or stdin input.")
280
263
 
281
- _create_dataset_from_payload(payload)
264
+ _create_table_from_payload(payload)
282
265
 
283
266
 
284
- def _create_dataset_from_payload(payload: str) -> bool:
267
+ def _create_table_from_payload(payload: str) -> bool:
285
268
  config = _read_config()
286
- url = f"{_get_shaped_url(config)}/datasets"
287
- headers = {"accept": "application/json", "x-api-key": config.api_key}
269
+ url = f"{_get_shaped_url(config)}/tables"
270
+ headers = {
271
+ "accept": "application/json",
272
+ "content-type": "application/json",
273
+ "x-api-key": config.api_key,
274
+ }
288
275
  typer.echo(payload)
289
276
  response = requests.post(url, headers=headers, data=payload)
277
+ if response.status_code not in [200, 201]:
278
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
279
+ raise typer.Exit(1)
290
280
  typer.echo(_parse_response_as_yaml(response.text))
291
- return response.status_code == 200
281
+ return response.status_code in [200, 201]
292
282
 
293
283
 
294
284
  @app.command()
295
- def list_datasets():
285
+ def list_tables():
286
+ """
287
+ List all tables.
288
+ """
296
289
  config = _read_config()
297
- url = f"{_get_shaped_url(config)}/datasets"
290
+ url = f"{_get_shaped_url(config)}/tables"
298
291
  headers = {"accept": "application/json", "x-api-key": config.api_key}
299
292
  response = requests.get(url, headers=headers)
293
+ if response.status_code != 200:
294
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
295
+ raise typer.Exit(1)
300
296
  typer.echo(_parse_response_as_yaml(response.text))
301
297
 
302
298
 
303
299
  @app.command()
304
- def view_dataset(dataset_name: str = typer.Option(...)):
300
+ def view_table(
301
+ table_name: str = typer.Option(..., help="Name of the table to view."),
302
+ ):
303
+ """
304
+ View the configuration of a specific table.
305
+ """
305
306
  config = _read_config()
306
- url = f"{_get_shaped_url(config)}/datasets/{dataset_name}"
307
+ url = f"{_get_shaped_url(config)}/tables/{table_name}"
307
308
  headers = {"accept": "application/json", "x-api-key": config.api_key}
308
309
  response = requests.get(url, headers=headers)
310
+ if response.status_code != 200:
311
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
312
+ raise typer.Exit(1)
309
313
  typer.echo(_parse_response_as_yaml(response.text))
310
314
 
311
315
 
312
316
  @app.command()
313
- def update_dataset(file: typer.FileText = typer.Option(None)):
317
+ def update_table(
318
+ file: typer.FileText = typer.Option(
319
+ None, help="Path to a JSON or YAML file containing the table configuration."
320
+ ),
321
+ ):
322
+ """
323
+ Update an existing table.
324
+
325
+ The table configuration can be provided via:
326
+ - --file: Path to a JSON/YAML file
327
+ - stdin: Pipe JSON/YAML content
328
+ """
314
329
  config = _read_config()
315
- url = f"{_get_shaped_url(config)}/datasets"
316
- headers = {"accept": "application/json", "x-api-key": config.api_key}
330
+ url = f"{_get_shaped_url(config)}/tables"
331
+ headers = {
332
+ "accept": "application/json",
333
+ "content-type": "application/json",
334
+ "x-api-key": config.api_key,
335
+ }
317
336
 
318
337
  if not sys.stdin.isatty():
319
338
  payload = sys.stdin.read()
@@ -324,18 +343,32 @@ def update_dataset(file: typer.FileText = typer.Option(None)):
324
343
 
325
344
  typer.echo(payload)
326
345
  response = requests.patch(url, headers=headers, data=payload)
346
+ if response.status_code != 200:
347
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
348
+ raise typer.Exit(1)
327
349
  typer.echo(_parse_response_as_yaml(response.text))
328
350
 
329
351
 
330
352
  @app.command()
331
- def dataset_insert(
332
- dataset_name: str = typer.Option(...),
333
- file: str = typer.Option(...),
334
- type: str = typer.Option(...),
353
+ def table_insert(
354
+ table_name: str = typer.Option(..., help="Name of the table to insert data into."),
355
+ file: str = typer.Option(..., help="Path to the data file to insert."),
356
+ type: str = typer.Option(
357
+ ..., help="File type. One of: parquet, csv, tsv, json, jsonl."
358
+ ),
335
359
  ):
360
+ """
361
+ Insert data into a table from a file.
362
+
363
+ Data is read in chunks and uploaded progressively with a progress bar.
364
+ """
336
365
  config = _read_config()
337
- url = f"{_get_shaped_url(config)}/datasets/{dataset_name}/insert"
338
- headers = {"accept": "application/json", "x-api-key": config.api_key}
366
+ url = f"{_get_shaped_url(config)}/tables/{table_name}/insert"
367
+ headers = {
368
+ "accept": "application/json",
369
+ "content-type": "application/json",
370
+ "x-api-key": config.api_key,
371
+ }
339
372
  bar = tqdm(unit=" Records")
340
373
 
341
374
  def _write_chunk(chunk: pd.DataFrame):
@@ -343,14 +376,15 @@ def dataset_insert(
343
376
  payload = json.dumps({"data": json.loads(chunk.to_json(orient="records"))})
344
377
  response = requests.post(url, headers=headers, data=payload)
345
378
  if response.status_code != 200:
346
- typer.echo(response.text)
347
- return
348
-
349
- _parse_response_as_yaml(response.text)
379
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
380
+ bar.close()
381
+ raise typer.Exit(1)
350
382
 
351
383
  # Chunk read and upload.
352
384
  for chunk in _read_chunks(file, type, chunk_size=1000):
353
385
  _write_chunk(chunk)
386
+
387
+ bar.close()
354
388
 
355
389
 
356
390
  def _read_chunks(file: str, type: str, chunk_size: int) -> pd.DataFrame:
@@ -381,11 +415,254 @@ def _read_chunks(file: str, type: str, chunk_size: int) -> pd.DataFrame:
381
415
 
382
416
 
383
417
  @app.command()
384
- def delete_dataset(dataset_name: str = typer.Option(...)):
418
+ def delete_table(
419
+ table_name: str = typer.Option(..., help="Name of the table to delete."),
420
+ ):
421
+ """
422
+ Delete a table.
423
+ """
385
424
  config = _read_config()
386
- url = f"{_get_shaped_url(config)}/datasets/{dataset_name}"
425
+ url = f"{_get_shaped_url(config)}/tables/{table_name}"
387
426
  headers = {"accept": "application/json", "x-api-key": config.api_key}
388
427
  response = requests.delete(url, headers=headers)
428
+ if response.status_code not in [200, 204]:
429
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
430
+ raise typer.Exit(1)
431
+ if response.text:
432
+ typer.echo(_parse_response_as_yaml(response.text))
433
+
434
+
435
+ @app.command()
436
+ def create_view(
437
+ file: typer.FileText = typer.Option(
438
+ None, help="Path to a JSON or YAML file containing the view configuration."
439
+ ),
440
+ ):
441
+ """
442
+ Create a new view.
443
+
444
+ The view configuration can be provided via:
445
+ - --file: Path to a JSON/YAML file
446
+ - stdin: Pipe JSON/YAML content
447
+ """
448
+ config = _read_config()
449
+ url = f"{_get_shaped_url(config)}/views"
450
+ headers = {
451
+ "accept": "application/json",
452
+ "content-type": "application/json",
453
+ "x-api-key": config.api_key,
454
+ }
455
+
456
+ if not sys.stdin.isatty():
457
+ payload = sys.stdin.read()
458
+ elif file is not None:
459
+ payload = _parse_file_as_json(file)
460
+ else:
461
+ raise ValueError("Must provide either a '--file' or stdin input.")
462
+
463
+ typer.echo(payload)
464
+ response = requests.post(url, headers=headers, data=payload)
465
+ if response.status_code not in [200, 201]:
466
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
467
+ raise typer.Exit(1)
468
+ typer.echo(_parse_response_as_yaml(response.text))
469
+
470
+
471
+ #################
472
+ # VIEW API #
473
+ #################
474
+
475
+
476
+ @app.command()
477
+ def list_views():
478
+ """
479
+ List all views.
480
+ """
481
+ config = _read_config()
482
+ url = f"{_get_shaped_url(config)}/views"
483
+ headers = {"accept": "application/json", "x-api-key": config.api_key}
484
+ response = requests.get(url, headers=headers)
485
+ if response.status_code != 200:
486
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
487
+ raise typer.Exit(1)
488
+ typer.echo(_parse_response_as_yaml(response.text))
489
+
490
+
491
+ @app.command()
492
+ def view_view(
493
+ view_name: str = typer.Option(..., help="Name of the view to view."),
494
+ ):
495
+ """
496
+ View the configuration of a specific view.
497
+ """
498
+ config = _read_config()
499
+ url = f"{_get_shaped_url(config)}/views/{view_name}"
500
+ headers = {"accept": "application/json", "x-api-key": config.api_key}
501
+ response = requests.get(url, headers=headers)
502
+ if response.status_code != 200:
503
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
504
+ raise typer.Exit(1)
505
+ typer.echo(_parse_response_as_yaml(response.text))
506
+
507
+
508
+ @app.command()
509
+ def update_view(
510
+ file: typer.FileText = typer.Option(
511
+ None, help="Path to a JSON or YAML file containing the view configuration."
512
+ ),
513
+ ):
514
+ """
515
+ Update an existing view.
516
+
517
+ The view configuration can be provided via:
518
+ - --file: Path to a JSON/YAML file
519
+ - stdin: Pipe JSON/YAML content
520
+ """
521
+ config = _read_config()
522
+ url = f"{_get_shaped_url(config)}/views"
523
+ headers = {
524
+ "accept": "application/json",
525
+ "content-type": "application/json",
526
+ "x-api-key": config.api_key,
527
+ }
528
+
529
+ if not sys.stdin.isatty():
530
+ payload = sys.stdin.read()
531
+ elif file is not None:
532
+ payload = _parse_file_as_json(file)
533
+ else:
534
+ raise ValueError("Must provide either a '--file' or stdin input.")
535
+
536
+ typer.echo(payload)
537
+ response = requests.patch(url, headers=headers, data=payload)
538
+ if response.status_code != 200:
539
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
540
+ raise typer.Exit(1)
541
+ typer.echo(_parse_response_as_yaml(response.text))
542
+
543
+
544
+ @app.command()
545
+ def delete_view(
546
+ view_name: str = typer.Option(..., help="Name of the view to delete."),
547
+ ):
548
+ """
549
+ Delete a view.
550
+ """
551
+ config = _read_config()
552
+ url = f"{_get_shaped_url(config)}/views/{view_name}"
553
+ headers = {"accept": "application/json", "x-api-key": config.api_key}
554
+ response = requests.delete(url, headers=headers)
555
+ if response.status_code not in [200, 204]:
556
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
557
+ raise typer.Exit(1)
558
+ if response.text:
559
+ typer.echo(_parse_response_as_yaml(response.text))
560
+
561
+
562
+ #################
563
+ # QUERY API #
564
+ #################
565
+
566
+
567
+ @app.command()
568
+ def query(
569
+ engine_name: str = typer.Option(..., help="Name of the engine to execute the query against."),
570
+ query_file: typer.FileText = typer.Option(
571
+ None, help="Path to a JSON or YAML file containing the query."
572
+ ),
573
+ query: str = typer.Option(
574
+ None, help="JSON string containing the query. Can be used instead of --query-file."
575
+ ),
576
+ ):
577
+ """
578
+ Execute an ad-hoc query against an engine.
579
+
580
+ The query can be provided via:
581
+ - --query-file: Path to a JSON/YAML file
582
+ - --query: JSON string directly
583
+ - stdin: Pipe JSON/YAML content
584
+ """
585
+ config = _read_config()
586
+ url = f"{_get_shaped_url(config)}/engines/{engine_name}/query"
587
+ headers = {
588
+ "accept": "application/json",
589
+ "content-type": "application/json",
590
+ "x-api-key": config.api_key,
591
+ }
592
+
593
+ if not sys.stdin.isatty():
594
+ payload = sys.stdin.read()
595
+ elif query_file is not None:
596
+ payload = _parse_file_as_json(query_file)
597
+ elif query is not None:
598
+ payload = query
599
+ else:
600
+ raise ValueError(
601
+ "Must provide either a '--query-file', '--query' JSON string, or stdin input."
602
+ )
603
+
604
+ typer.echo(payload)
605
+ response = requests.post(url, headers=headers, data=payload)
606
+ if response.status_code != 200:
607
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
608
+ raise typer.Exit(1)
609
+ typer.echo(_parse_response_as_yaml(response.text))
610
+
611
+
612
+ @app.command()
613
+ def execute_saved_query(
614
+ engine_name: str = typer.Option(..., help="Name of the engine containing the saved query."),
615
+ query_name: str = typer.Option(..., help="Name of the saved query to execute."),
616
+ ):
617
+ """
618
+ Execute a previously saved query by name.
619
+ """
620
+ config = _read_config()
621
+ url = f"{_get_shaped_url(config)}/engines/{engine_name}/queries/{query_name}"
622
+ headers = {
623
+ "accept": "application/json",
624
+ "content-type": "application/json",
625
+ "x-api-key": config.api_key,
626
+ }
627
+ response = requests.post(url, headers=headers)
628
+ if response.status_code != 200:
629
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
630
+ raise typer.Exit(1)
631
+ typer.echo(_parse_response_as_yaml(response.text))
632
+
633
+
634
+ @app.command()
635
+ def view_saved_query(
636
+ engine_name: str = typer.Option(..., help="Name of the engine containing the saved query."),
637
+ query_name: str = typer.Option(..., help="Name of the saved query to view."),
638
+ ):
639
+ """
640
+ View the definition of a saved query.
641
+ """
642
+ config = _read_config()
643
+ url = f"{_get_shaped_url(config)}/engines/{engine_name}/queries/{query_name}"
644
+ headers = {"accept": "application/json", "x-api-key": config.api_key}
645
+ response = requests.get(url, headers=headers)
646
+ if response.status_code != 200:
647
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
648
+ raise typer.Exit(1)
649
+ typer.echo(_parse_response_as_yaml(response.text))
650
+
651
+
652
+ @app.command()
653
+ def list_saved_queries(
654
+ engine_name: str = typer.Option(..., help="Name of the engine to list saved queries for."),
655
+ ):
656
+ """
657
+ List all saved queries for an engine.
658
+ """
659
+ config = _read_config()
660
+ url = f"{_get_shaped_url(config)}/engines/{engine_name}/queries"
661
+ headers = {"accept": "application/json", "x-api-key": config.api_key}
662
+ response = requests.get(url, headers=headers)
663
+ if response.status_code != 200:
664
+ typer.echo(f"Error: {response.status_code}\n{response.text}")
665
+ raise typer.Exit(1)
389
666
  typer.echo(_parse_response_as_yaml(response.text))
390
667
 
391
668
 
@@ -0,0 +1,341 @@
1
+ Metadata-Version: 2.4
2
+ Name: shaped
3
+ Version: 2.0.0
4
+ Summary: CLI and SDK tools for interacting with the Shaped API.
5
+ Home-page: https://github.com/shaped-ai/shaped-cli
6
+ Author: Shaped Team
7
+ Author-email: support@shaped.ai
8
+ Keywords: shaped-ai
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Classifier: Operating System :: MacOS :: MacOS X
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Requires-Python: >=3.9, <3.14
15
+ Description-Content-Type: text/markdown
16
+ Requires-Dist: pyarrow==20.0.0
17
+ Requires-Dist: pandas==2.3.0
18
+ Requires-Dist: numpy==1.26.4
19
+ Requires-Dist: typer>=0.7.0
20
+ Requires-Dist: requests>=2.28.1
21
+ Requires-Dist: pydantic>=2.8.2
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: tqdm==4.67.1
24
+ Requires-Dist: s3fs==0.4.2
25
+ Requires-Dist: fsspec==2023.5.0
26
+ Requires-Dist: urllib3<2.1.0,>=1.25.3
27
+ Requires-Dist: python-dateutil
28
+ Requires-Dist: typing-extensions>=4.7.1
29
+ Requires-Dist: pytest>=6.2.5
30
+ Requires-Dist: pytest-mock==3.14.0
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: keywords
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+ # Shaped CLI
43
+
44
+ CLI for interactions with the Shaped API.
45
+
46
+ ## Installing the Shaped CLI
47
+
48
+ ```
49
+ pip install shaped
50
+ ```
51
+
52
+ ## Initialize
53
+
54
+ ```
55
+ shaped init --api-key <API_KEY> [--env <ENV>]
56
+ ```
57
+
58
+ The `--env` option defaults to `prod` and can be set to other environments like `dev` or `staging`.
59
+
60
+ ## Engine API
61
+
62
+ ### Create Engine
63
+
64
+ ```
65
+ shaped create-engine --file <PATH_TO_FILE>
66
+ ```
67
+
68
+ Or pipe from stdin:
69
+
70
+ ```
71
+ cat <PATH_TO_FILE> | shaped create-engine
72
+ ```
73
+
74
+ ### Update Engine
75
+
76
+ ```
77
+ shaped update-engine --file <PATH_TO_FILE>
78
+ ```
79
+
80
+ Or pipe from stdin:
81
+
82
+ ```
83
+ cat <PATH_TO_FILE> | shaped update-engine
84
+ ```
85
+
86
+ ### List Engines
87
+
88
+ ```
89
+ shaped list-engines
90
+ ```
91
+
92
+ ### View Engine
93
+
94
+ ```
95
+ shaped view-engine --engine-name <ENGINE_NAME>
96
+ ```
97
+
98
+ ### Delete Engine
99
+
100
+ ```
101
+ shaped delete-engine --engine-name <ENGINE_NAME>
102
+ ```
103
+
104
+ ## Table API
105
+
106
+ ### Create Table
107
+
108
+ ```
109
+ shaped create-table --file <PATH_TO_FILE>
110
+ ```
111
+
112
+ Or pipe from stdin:
113
+
114
+ ```
115
+ cat <PATH_TO_FILE> | shaped create-table
116
+ ```
117
+
118
+ ### Create Table from URI
119
+
120
+ Create a table and automatically insert data from a file:
121
+
122
+ ```
123
+ shaped create-table-from-uri --name <TABLE_NAME> --path <PATH_TO_FILE> --type <FILE_TYPE>
124
+ ```
125
+
126
+ Supported file types: `parquet`, `csv`, `tsv`, `json`, `jsonl`
127
+
128
+ ### List Tables
129
+
130
+ ```
131
+ shaped list-tables
132
+ ```
133
+
134
+ ### Table Insert
135
+
136
+ Insert data into an existing table:
137
+
138
+ ```
139
+ shaped table-insert --table-name <TABLE_NAME> --file <DATAFRAME_FILE> --type <FILE_TYPE>
140
+ ```
141
+
142
+ Supported file types: `parquet`, `csv`, `tsv`, `json`, `jsonl`
143
+
144
+ ### View Table
145
+
146
+ ```
147
+ shaped view-table --table-name <TABLE_NAME>
148
+ ```
149
+
150
+ ### Update Table
151
+
152
+ ```
153
+ shaped update-table --file <PATH_TO_FILE>
154
+ ```
155
+
156
+ Or pipe from stdin:
157
+
158
+ ```
159
+ cat <PATH_TO_FILE> | shaped update-table
160
+ ```
161
+
162
+ ### Delete Table
163
+
164
+ ```
165
+ shaped delete-table --table-name <TABLE_NAME>
166
+ ```
167
+
168
+ ## View API
169
+
170
+ ### Create View
171
+
172
+ ```
173
+ shaped create-view --file <PATH_TO_FILE>
174
+ ```
175
+
176
+ Or pipe from stdin:
177
+
178
+ ```
179
+ cat <PATH_TO_FILE> | shaped create-view
180
+ ```
181
+
182
+ ### List Views
183
+
184
+ ```
185
+ shaped list-views
186
+ ```
187
+
188
+ ### View View
189
+
190
+ ```
191
+ shaped view-view --view-name <VIEW_NAME>
192
+ ```
193
+
194
+ ### Update View
195
+
196
+ ```
197
+ shaped update-view --file <PATH_TO_FILE>
198
+ ```
199
+
200
+ Or pipe from stdin:
201
+
202
+ ```
203
+ cat <PATH_TO_FILE> | shaped update-view
204
+ ```
205
+
206
+ ### Delete View
207
+
208
+ ```
209
+ shaped delete-view --view-name <VIEW_NAME>
210
+ ```
211
+
212
+ ## Query API
213
+
214
+ ### Execute Query
215
+
216
+ Execute an ad-hoc query against an engine. The query can be provided in three ways:
217
+
218
+ **From a file:**
219
+ ```
220
+ shaped query --engine-name <ENGINE_NAME> --query-file <PATH_TO_FILE>
221
+ ```
222
+
223
+ **As a JSON string:**
224
+ ```
225
+ shaped query --engine-name <ENGINE_NAME> --query '{"sql": "SELECT * FROM table"}'
226
+ ```
227
+
228
+ **From stdin:**
229
+ ```
230
+ cat <PATH_TO_FILE> | shaped query --engine-name <ENGINE_NAME>
231
+ ```
232
+
233
+ ### Execute Saved Query
234
+
235
+ Execute a previously saved query by name:
236
+
237
+ ```
238
+ shaped execute-saved-query --engine-name <ENGINE_NAME> --query-name <QUERY_NAME>
239
+ ```
240
+
241
+ ### List Saved Queries
242
+
243
+ List all saved queries for an engine:
244
+
245
+ ```
246
+ shaped list-saved-queries --engine-name <ENGINE_NAME>
247
+ ```
248
+
249
+ ### View Saved Query
250
+
251
+ View the definition of a saved query:
252
+
253
+ ```
254
+ shaped view-saved-query --engine-name <ENGINE_NAME> --query-name <QUERY_NAME>
255
+ ```
256
+
257
+ ---
258
+
259
+ # Python SDK
260
+
261
+ ## Installation
262
+
263
+ ### Pip Installation
264
+
265
+ ```sh
266
+ pip install shaped
267
+ ```
268
+
269
+ ## Rank
270
+
271
+ ```python
272
+ import shaped
273
+
274
+ api_key = 'your_api_key'
275
+ client = shaped.Client(api_key=api_key)
276
+
277
+ api_response = client.rank(
278
+ model_name="amazon_beauty_product_recommendations",
279
+ user_id="A2FRWMTWYJUK7P",
280
+ return_metadata=True,
281
+ item_ids=['B01AHSUT8M', 'B00GW7H5EY', 'B01GLA54SA', 'B0016PKWK6', 'B00PJD7KPG', 'B00BCI8OP2', 'B004FK7R02'],
282
+ )
283
+ print(api_response)
284
+ ```
285
+
286
+ ## Retrieve
287
+ ```python
288
+ import shaped
289
+
290
+ api_key = 'your_api_key'
291
+ client = shaped.Client(api_key=api_key)
292
+
293
+ api_response = client.retrieve(
294
+ model_name="amazon_beauty_product_recommendations",
295
+ )
296
+ print(api_response)
297
+ ```
298
+
299
+ ## Similar Items
300
+ ```python
301
+ import shaped
302
+
303
+ api_key = 'your_api_key'
304
+ client = shaped.Client(api_key=api_key)
305
+
306
+ api_response = client.similar_items(
307
+ model_name="amazon_beauty_product_recommendations",
308
+ item_id="B000FOI48G",
309
+ )
310
+ print(api_response)
311
+ ```
312
+
313
+ ## Similar Users
314
+ ```python
315
+ import shaped
316
+
317
+ api_key = 'your_api_key'
318
+ client = shaped.Client(api_key=api_key)
319
+
320
+ api_response = client.similar_users(
321
+ model_name="amazon_beauty_product_recommendations",
322
+ user_id="A2FRWMTWYJUK7P",
323
+ )
324
+ print(api_response)
325
+ ```
326
+
327
+ ## Complement Items
328
+ ```python
329
+ import shaped
330
+
331
+ api_key = 'your_api_key'
332
+ client = shaped.Client(api_key=api_key)
333
+
334
+ api_response = client.complement_items(
335
+ model_name="amazon_beauty_product_recommendations",
336
+ item_ids=['B000URXP6E', 'B0012Y0ZG2'],
337
+ )
338
+ print(api_response)
339
+ ```
340
+
341
+ ---
@@ -64,10 +64,10 @@ shaped/autogen/models/validation_error.py,sha256=kVstUJ1nwQ6Y9kssgDBpeoKGFScWegp
64
64
  shaped/autogen/models/value_type.py,sha256=uxlGNPto3CK17jHKDRvAl3BqNCPkHlxfRBPdofi_qdM,2917
65
65
  shaped/autogen/models/view_model_response.py,sha256=4xzesZSHMcPCZHsevo5Bt_3P9jBNiEbd_PFTWQ5Q9KU,5757
66
66
  shaped/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
- shaped/cli/shaped_cli.py,sha256=kOxCiPivUEjNPs4mNETGpi43_PzTXp61dN-dr9fp7C0,12822
68
- shaped-1.0.1.dist-info/METADATA,sha256=idA0EhKKTsUJgbAS2UWQLRShSaI78cds6XoCHo1hBck,5433
69
- shaped-1.0.1.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
70
- shaped-1.0.1.dist-info/entry_points.txt,sha256=4xoVmnNTKtmzjGNX5ezp9MCWLgi7880TtfqFByN1u5U,53
71
- shaped-1.0.1.dist-info/top_level.txt,sha256=w-lDaoadQVYpze9N9gZyK9qngb7fZCJ-KCdHLGvt0SU,7
72
- shaped-1.0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
- shaped-1.0.1.dist-info/RECORD,,
67
+ shaped/cli/shaped_cli.py,sha256=DzBbnRlk2XWkCq8eGGUVRatm6ntVHclfsrNHYEYxdyA,20562
68
+ shaped-2.0.0.dist-info/METADATA,sha256=uNlIBs-grJV6aNnpdVM-NyICOtzi0mEUMxeqJdktkSU,5761
69
+ shaped-2.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
70
+ shaped-2.0.0.dist-info/entry_points.txt,sha256=4xoVmnNTKtmzjGNX5ezp9MCWLgi7880TtfqFByN1u5U,53
71
+ shaped-2.0.0.dist-info/top_level.txt,sha256=w-lDaoadQVYpze9N9gZyK9qngb7fZCJ-KCdHLGvt0SU,7
72
+ shaped-2.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
+ shaped-2.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.7.0)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,256 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: shaped
3
- Version: 1.0.1
4
- Summary: CLI and SDK tools for interacting with the Shaped API.
5
- Home-page: https://github.com/shaped-ai/shaped-cli
6
- Author: Shaped Team
7
- Author-email: support@shaped.ai
8
- Keywords: shaped-ai
9
- Classifier: Operating System :: POSIX :: Linux
10
- Classifier: Operating System :: MacOS :: MacOS X
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Development Status :: 5 - Production/Stable
13
- Classifier: Intended Audience :: Developers
14
- Requires-Python: >=3.8, <3.12
15
- Description-Content-Type: text/markdown
16
- Requires-Dist: typer>=0.7.0
17
- Requires-Dist: requests>=2.28.1
18
- Requires-Dist: pydantic>=2.8.2
19
- Requires-Dist: pyyaml>=6.0
20
- Requires-Dist: pyarrow==11.0.0
21
- Requires-Dist: pandas==1.5.3
22
- Requires-Dist: tqdm==4.65.0
23
- Requires-Dist: s3fs==0.4.2
24
- Requires-Dist: fsspec==2023.5.0
25
- Requires-Dist: numpy==1.26.4
26
- Requires-Dist: urllib3<2.1.0,>=1.25.3
27
- Requires-Dist: python-dateutil
28
- Requires-Dist: typing-extensions>=4.7.1
29
- Requires-Dist: pytest>=6.2.5
30
- Requires-Dist: pytest-mock==3.14.0
31
-
32
- # Python SDK
33
-
34
- ## Installation
35
-
36
- ### Local Development
37
-
38
- ```sh
39
- pip install -e .
40
- ```
41
-
42
- ### Pip Installation
43
-
44
- ```sh
45
- pip install shaped
46
- ```
47
-
48
- ## Rank
49
-
50
- ```python
51
- import shaped
52
-
53
- api_key = 'your_api_key'
54
- client = shaped.Client(api_key=api_key)
55
-
56
- api_response = client.rank(
57
- model_name="amazon_beauty_product_recommendations",
58
- user_id="A2FRWMTWYJUK7P",
59
- return_metadata=True,
60
- item_ids=['B01AHSUT8M', 'B00GW7H5EY', 'B01GLA54SA', 'B0016PKWK6', 'B00PJD7KPG', 'B00BCI8OP2', 'B004FK7R02'],
61
- )
62
- print(api_response)
63
- ```
64
-
65
- ## Retrieve
66
- ```python
67
- import shaped
68
-
69
- api_key = 'your_api_key'
70
- client = shaped.Client(api_key=api_key)
71
-
72
- api_response = client.retrieve(
73
- model_name="amazon_beauty_product_recommendations",
74
- )
75
- print(api_response)
76
- ```
77
-
78
- ## Similar Items
79
- ```python
80
- import shaped
81
-
82
- api_key = 'your_api_key'
83
- client = shaped.Client(api_key=api_key)
84
-
85
- api_response = client.similar_items(
86
- model_name="amazon_beauty_product_recommendations",
87
- item_id="B000FOI48G",
88
- )
89
- print(api_response)
90
- ```
91
-
92
- ## Similar Users
93
- ```python
94
- import shaped
95
-
96
- api_key = 'your_api_key'
97
- client = shaped.Client(api_key=api_key)
98
-
99
- api_response = client.similar_users(
100
- model_name="amazon_beauty_product_recommendations",
101
- user_id="A2FRWMTWYJUK7P",
102
- )
103
- print(api_response)
104
- ```
105
-
106
- ## Complement Items
107
- ```python
108
- import shaped
109
-
110
- api_key = 'your_api_key'
111
- client = shaped.Client(api_key=api_key)
112
-
113
- api_response = client.complement_items(
114
- model_name="amazon_beauty_product_recommendations",
115
- item_ids=['B000URXP6E', 'B0012Y0ZG2'],
116
- )
117
- print(api_response)
118
- ```
119
-
120
- ## Maintainer Notes
121
-
122
- To recreate the autogenerated code:
123
-
124
- 1. `brew install openapi-generator`
125
- 2. Copy across `openapi.yaml` from the `shaped-docs` repository to `~`
126
- 1. Change `-g python` to the language you want to generate
127
- 1. Change `-o python/` to the directory you want to output to
128
- 3. Navigate to `~` and run the command:
129
-
130
- ### Python
131
- ```bash
132
- openapi-generator generate -g python -i openapi.yaml -o python/ -p packageName=shaped.autogen --global-property models,apis,apiDocs=false,modelDocs=false,modelTests=false,apiTests=false,supportingFiles=api_client.py:api_response.py:configuration.py:exceptions.py:rest.py:__init__.py
133
- ```
134
-
135
- ### Node.js
136
- ```bash
137
- openapi-generator generate -i openapi.yaml -g javascript -o nodejs/src -c config.yaml --global-property models,apis,apiDocs=false,modelDocs=false,modelTests=false,apiTests=false,supportingFiles=ApiClient.js
138
- ```
139
-
140
- ## Testing
141
- `pytest python/tests/test_rank.py --api-key 'api_key'`
142
- `npm test`
143
- ---
144
-
145
- # Shaped CLI
146
-
147
- CLI for interactions with the Shaped API.
148
-
149
- ## Installing the Shaped CLI
150
-
151
- ```
152
- pip install shaped
153
- ```
154
-
155
- ## Initialize
156
-
157
- ```
158
- shaped init --api-key <API_KEY>
159
- ```
160
-
161
- ## Model API
162
-
163
- ### Create Model (File)
164
-
165
- ```
166
- shaped create-model --file <PATH_TO_FILE>
167
- ```
168
-
169
- ### Create Model (STDIN)
170
-
171
- ```
172
- cat $(PATH_TO_FILE) | shaped create-model
173
- ```
174
-
175
- ### List Models
176
-
177
- ```
178
- shaped list-models
179
- ```
180
-
181
- ### View Model
182
-
183
- ```
184
- shaped view-model --model-name <MODEL_NAME>
185
- ```
186
-
187
- ## Delete Model
188
-
189
- ```
190
- shaped delete-model --model-name <MODEL_NAME>
191
- ```
192
-
193
- ## Dataset API
194
-
195
- ### Create Dataset
196
-
197
- ```
198
- shaped create-dataset --file <PATH_TO_FILE>
199
- ```
200
-
201
- ### List Datasets
202
-
203
- ```
204
- shaped list-datasets
205
- ```
206
-
207
- ### Dataset Insert
208
-
209
- ```
210
- shaped dataset-insert --dataset-name <DATASET_NAME> --file <DATAFRAME_FILE> --type <FILE_TYPE>
211
- ```
212
-
213
- ## Delete dataset
214
-
215
- ```
216
- shaped delete-dataset --dataset-name <DATASET_NAME>
217
- ```
218
-
219
- ## Rank API
220
-
221
- ### Rank
222
-
223
- ```
224
- shaped rank --model-name <MODEL_NAME> --user-id <USER_ID>
225
- ```
226
-
227
- ### Similar Items
228
-
229
- ```
230
- shaped similar --model-name <MODEL_NAME> --item-id <ITEM_ID>
231
- ```
232
-
233
- ### Similar Users
234
-
235
- ```
236
- shaped similar --model-name <MODEL_NAME> --user-id <USER_ID>
237
- ```
238
-
239
- # Development
240
-
241
- ## Installing the Shaped CLI from Test PyPI
242
-
243
- Upon all pushes to main branch, a new version of the CLI is published to Test PyPI. To install the latest version of the CLI from Test PyPI, run the following commands:
244
-
245
- ```bash
246
- conda create -n cli-dev python=3.9
247
- conda activate cli-dev
248
- export PACKAGE_VERSION={} # Specify the version you want to install
249
- pip install --extra-index-url https://test.pypi.org/simple/ shaped-cli==$PACKAGE_VERSION
250
- ```
251
-
252
- ## Releasing a new CLI version to PyPI
253
-
254
- To release a new version of the CLI to PyPI, open a PR changing the version of the package in `setup.py`, following [Semantic Versioning](https://semver.org) principles, e.g. `0.1.1`.
255
-
256
- CircleCI will generate an approval prompt when this branch is merged to main, and upon approval will publish to PyPI.