cocoindex 0.1.41__cp313-cp313-macosx_11_0_arm64.whl → 0.1.42__cp313-cp313-macosx_11_0_arm64.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.
Binary file
cocoindex/cli.py CHANGED
@@ -1,89 +1,234 @@
1
1
  import click
2
2
  import datetime
3
+ import sys
4
+ import importlib.util
5
+ import os
6
+ import atexit
7
+ import types
3
8
 
9
+ from dotenv import load_dotenv, find_dotenv
4
10
  from rich.console import Console
5
11
  from rich.table import Table
6
12
 
7
- from . import flow, lib, setting
13
+ from . import flow, lib, setting, query
8
14
  from .setup import sync_setup, drop_setup, flow_names_with_setup, apply_setup_changes
9
15
 
16
+ # Create ServerSettings lazily upon first call, as environment variables may be loaded from files, etc.
17
+ COCOINDEX_HOST = 'https://cocoindex.io'
18
+
19
+ def _parse_app_flow_specifier(specifier: str) -> tuple[str, str | None]:
20
+ """Parses 'module_or_path[:flow_name]' into (module_or_path, flow_name | None)."""
21
+ parts = specifier.split(":", 1) # Split only on the first colon
22
+ app_ref = parts[0]
23
+
24
+ if not app_ref:
25
+ raise click.BadParameter(
26
+ f"Application module/path part is missing or invalid in specifier: '{specifier}'. "
27
+ "Expected format like 'myapp.py' or 'myapp:MyFlow'.",
28
+ param_hint="APP_SPECIFIER"
29
+ )
30
+
31
+ if len(parts) == 1:
32
+ return app_ref, None
33
+
34
+ flow_ref_part = parts[1]
35
+
36
+ if not flow_ref_part: # Handles empty string after colon
37
+ return app_ref, None
38
+
39
+ if not flow_ref_part.isidentifier():
40
+ raise click.BadParameter(
41
+ f"Invalid format for flow name part ('{flow_ref_part}') in specifier '{specifier}'. "
42
+ "If a colon separates the application from the flow name, the flow name should typically be "
43
+ "a valid identifier (e.g., alphanumeric with underscores, not starting with a number).",
44
+ param_hint="APP_SPECIFIER"
45
+ )
46
+ return app_ref, flow_ref_part
47
+
48
+ def _get_app_ref_from_specifier(
49
+ specifier: str,
50
+ ) -> str:
51
+ """
52
+ Parses the APP_TARGET to get the application reference (path or module).
53
+ Issues a warning if a flow name component is also provided in it.
54
+ """
55
+ app_ref, flow_ref = _parse_app_flow_specifier(specifier)
56
+
57
+ if flow_ref is not None:
58
+ click.echo(
59
+ click.style(
60
+ f"Ignoring flow name '{flow_ref}' in '{specifier}': "
61
+ f"this command operates on the entire app/module '{app_ref}'.",
62
+ fg='yellow'
63
+ ),
64
+ err=True
65
+ )
66
+ return app_ref
67
+
68
+ def _load_user_app(app_target: str) -> types.ModuleType:
69
+ """
70
+ Loads the user's application, which can be a file path or an installed module name.
71
+ Exits on failure.
72
+ """
73
+ if not app_target:
74
+ raise click.ClickException("Application target not provided.")
75
+
76
+ looks_like_path = os.sep in app_target or app_target.lower().endswith(".py")
77
+
78
+ if looks_like_path:
79
+ if not os.path.isfile(app_target):
80
+ raise click.ClickException(f"Application file path not found: {app_target}")
81
+ app_path = os.path.abspath(app_target)
82
+ app_dir = os.path.dirname(app_path)
83
+ module_name = os.path.splitext(os.path.basename(app_path))[0]
84
+
85
+ if app_dir not in sys.path:
86
+ sys.path.insert(0, app_dir)
87
+ try:
88
+ spec = importlib.util.spec_from_file_location(module_name, app_path)
89
+ if spec is None:
90
+ raise ImportError(f"Could not create spec for file: {app_path}")
91
+ module = importlib.util.module_from_spec(spec)
92
+ sys.modules[spec.name] = module
93
+ spec.loader.exec_module(module)
94
+ return module
95
+ except (ImportError, FileNotFoundError, PermissionError) as e:
96
+ raise click.ClickException(f"Failed importing file '{app_path}': {e}")
97
+ finally:
98
+ if app_dir in sys.path and sys.path[0] == app_dir:
99
+ sys.path.pop(0)
100
+
101
+ # Try as module
102
+ try:
103
+ return importlib.import_module(app_target)
104
+ except ImportError as e:
105
+ raise click.ClickException(f"Failed to load module '{app_target}': {e}")
106
+ except Exception as e:
107
+ raise click.ClickException(f"Unexpected error importing module '{app_target}': {e}")
108
+
10
109
  @click.group()
11
- def cli():
110
+ @click.version_option(package_name="cocoindex", message="%(prog)s version %(version)s")
111
+ @click.option(
112
+ "--env-file",
113
+ type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True),
114
+ help="Path to a .env file to load environment variables from. "
115
+ "If not provided, attempts to load '.env' from the current directory.",
116
+ default=None,
117
+ show_default=False
118
+ )
119
+ def cli(env_file: str | None):
12
120
  """
13
121
  CLI for Cocoindex.
14
122
  """
123
+ dotenv_path = env_file or find_dotenv(usecwd=True)
124
+
125
+ if load_dotenv(dotenv_path=dotenv_path):
126
+ loaded_env_path = os.path.abspath(dotenv_path)
127
+ click.echo(f"Loaded environment variables from: {loaded_env_path}", err=True)
128
+
129
+ try:
130
+ settings = setting.Settings.from_env()
131
+ lib.init(settings)
132
+ atexit.register(lib.stop)
133
+ except Exception as e:
134
+ raise click.ClickException(f"Failed to initialize CocoIndex library: {e}")
15
135
 
16
136
  @cli.command()
17
- @click.option(
18
- "-a", "--all", "show_all", is_flag=True, show_default=True, default=False,
19
- help="Also show all flows with persisted setup under the current app namespace, even if not defined in the current process.")
20
- def ls(show_all: bool):
137
+ @click.argument("app_target", type=str, required=False)
138
+ def ls(app_target: str | None):
21
139
  """
22
140
  List all flows.
141
+
142
+ If APP_TARGET (path/to/app.py or a module) is provided, lists flows
143
+ defined in the app and their backend setup status.
144
+
145
+ If APP_TARGET is omitted, lists all flows that have a persisted
146
+ setup in the backend.
23
147
  """
24
- current_flow_names = flow.flow_names()
25
- persisted_flow_names = flow_names_with_setup()
26
- remaining_persisted_flow_names = set(persisted_flow_names)
148
+ if app_target:
149
+ app_ref = _get_app_ref_from_specifier(app_target)
150
+ _load_user_app(app_ref)
27
151
 
28
- has_missing_setup = False
29
- has_extra_setup = False
152
+ current_flow_names = set(flow.flow_names())
153
+ persisted_flow_names = set(flow_names_with_setup())
30
154
 
31
- for name in current_flow_names:
32
- if name in remaining_persisted_flow_names:
33
- remaining_persisted_flow_names.remove(name)
34
- suffix = ''
35
- else:
36
- suffix = ' [+]'
37
- has_missing_setup = True
38
- click.echo(f'{name}{suffix}')
155
+ if not current_flow_names:
156
+ click.echo(f"No flows are defined in '{app_ref}'.")
157
+ return
39
158
 
40
- if show_all:
41
- for name in persisted_flow_names:
42
- if name in remaining_persisted_flow_names:
43
- click.echo(f'{name} [?]')
44
- has_extra_setup = True
45
-
46
- if has_missing_setup or has_extra_setup:
47
- click.echo('')
48
- click.echo('Notes:')
49
- if has_missing_setup:
159
+ has_missing = False
160
+ for name in sorted(current_flow_names):
161
+ if name in persisted_flow_names:
162
+ click.echo(name)
163
+ else:
164
+ click.echo(f"{name} [+]")
165
+ has_missing = True
166
+
167
+ if has_missing:
168
+ click.echo('')
169
+ click.echo('Notes:')
50
170
  click.echo(' [+]: Flows present in the current process, but missing setup.')
51
- if has_extra_setup:
52
- click.echo(' [?]: Flows with persisted setup, but not in the current process.')
171
+
172
+ else:
173
+ persisted_flow_names = sorted(flow_names_with_setup())
174
+
175
+ if not persisted_flow_names:
176
+ click.echo("No persisted flow setups found in the backend.")
177
+ return
178
+
179
+ for name in persisted_flow_names:
180
+ click.echo(name)
53
181
 
54
182
  @cli.command()
55
- @click.argument("flow_name", type=str, required=False)
183
+ @click.argument("app_flow_specifier", type=str)
56
184
  @click.option("--color/--no-color", default=True, help="Enable or disable colored output.")
57
185
  @click.option("--verbose", is_flag=True, help="Show verbose output with full details.")
58
- def show(flow_name: str | None, color: bool, verbose: bool):
186
+ def show(app_flow_specifier: str, color: bool, verbose: bool):
59
187
  """
60
- Show the flow spec and schema in a readable format with colored output.
188
+ Show the flow spec and schema.
189
+
190
+ APP_FLOW_SPECIFIER: Specifies the application and optionally the target flow.
191
+ Can be one of the following formats:
192
+
193
+ \b
194
+ - path/to/your_app.py
195
+ - an_installed.module_name
196
+ - path/to/your_app.py:SpecificFlowName
197
+ - an_installed.module_name:SpecificFlowName
198
+
199
+ :SpecificFlowName can be omitted only if the application defines a single flow.
61
200
  """
62
- flow = _flow_by_name(flow_name)
63
- console = Console(no_color=not color)
64
- console.print(flow._render_spec(verbose=verbose))
201
+ app_ref, flow_ref = _parse_app_flow_specifier(app_flow_specifier)
202
+ _load_user_app(app_ref)
65
203
 
204
+ fl = _flow_by_name(flow_ref)
205
+ console = Console(no_color=not color)
206
+ console.print(fl._render_spec(verbose=verbose))
66
207
  console.print()
67
208
  table = Table(
68
- title=f"Schema for Flow: {flow.full_name}",
69
- show_header=True,
209
+ title=f"Schema for Flow: {fl.name}",
210
+ title_style="cyan",
70
211
  header_style="bold magenta"
71
212
  )
72
213
  table.add_column("Field", style="cyan")
73
214
  table.add_column("Type", style="green")
74
215
  table.add_column("Attributes", style="yellow")
75
-
76
- for field_name, field_type, attr_str in flow._get_schema():
216
+ for field_name, field_type, attr_str in fl._get_schema():
77
217
  table.add_row(field_name, field_type, attr_str)
78
-
79
218
  console.print(table)
80
219
 
81
220
  @cli.command()
82
- def setup():
221
+ @click.argument("app_target", type=str)
222
+ def setup(app_target: str):
83
223
  """
84
224
  Check and apply backend setup changes for flows, including the internal and target storage
85
225
  (to export).
226
+
227
+ APP_TARGET: path/to/app.py or installed_module.
86
228
  """
229
+ app_ref = _get_app_ref_from_specifier(app_target)
230
+ _load_user_app(app_ref)
231
+
87
232
  setup_status = sync_setup()
88
233
  click.echo(setup_status)
89
234
  if setup_status.is_up_to_date():
@@ -94,80 +239,125 @@ def setup():
94
239
  return
95
240
  apply_setup_changes(setup_status)
96
241
 
97
- @cli.command()
242
+ @cli.command("drop")
243
+ @click.argument("app_target", type=str, required=False)
98
244
  @click.argument("flow_name", type=str, nargs=-1)
99
245
  @click.option(
100
246
  "-a", "--all", "drop_all", is_flag=True, show_default=True, default=False,
101
247
  help="Drop the backend setup for all flows with persisted setup, "
102
- "even if not defined in the current process.")
103
- def drop(flow_name: tuple[str, ...], drop_all: bool):
248
+ "even if not defined in the current process."
249
+ "If used, APP_TARGET and any listed flow names are ignored.")
250
+ def drop(app_target: str | None, flow_name: tuple[str, ...], drop_all: bool):
104
251
  """
105
- Drop the backend setup for specified flows.
106
- If no flow is specified, all flows defined in the current process will be dropped.
252
+ Drop the backend setup for flows.
253
+
254
+ Modes of operation:\n
255
+ 1. Drop ALL persisted setups: `cocoindex drop --all`\n
256
+ 2. Drop all flows defined in an app: `cocoindex drop <APP_TARGET>`\n
257
+ 3. Drop specific named flows: `cocoindex drop <APP_TARGET> [FLOW_NAME...]`
107
258
  """
259
+ app_ref = None
260
+ flow_names = []
261
+
108
262
  if drop_all:
263
+ if app_target or flow_name:
264
+ click.echo("Warning: When --all is used, APP_TARGET and any individual flow names are ignored.", err=True)
109
265
  flow_names = flow_names_with_setup()
110
- elif len(flow_name) == 0:
111
- flow_names = flow.flow_names()
266
+ elif app_target:
267
+ app_ref = _get_app_ref_from_specifier(app_target)
268
+ _load_user_app(app_ref)
269
+ if flow_name:
270
+ flow_names = list(flow_name)
271
+ click.echo(f"Preparing to drop specified flows: {', '.join(flow_names)} (in '{app_ref}').", err=True)
272
+ else:
273
+ flow_names = [fl.name for fl in flow.flows()]
274
+ if not flow_names:
275
+ click.echo(f"No flows found defined in '{app_ref}' to drop.")
276
+ return
277
+ click.echo(f"Preparing to drop all flows defined in '{app_ref}': {', '.join(flow_names)}.", err=True)
112
278
  else:
113
- flow_names = list(flow_name)
279
+ raise click.UsageError(
280
+ "Missing arguments. You must either provide an APP_TARGET (to target app-specific flows) "
281
+ "or use the --all flag."
282
+ )
283
+
284
+ if not flow_names:
285
+ click.echo("No flows identified for the drop operation.")
286
+ return
287
+
114
288
  setup_status = drop_setup(flow_names)
115
289
  click.echo(setup_status)
116
290
  if setup_status.is_up_to_date():
117
291
  click.echo("No flows need to be dropped.")
118
292
  return
119
293
  if not click.confirm(
120
- "Changes need to be pushed. Continue? [yes/N]", default=False, show_default=False):
294
+ f"\nThis will apply changes to drop setup for: {', '.join(flow_names)}. Continue? [yes/N]",
295
+ default=False, show_default=False):
296
+ click.echo("Drop operation aborted by user.")
121
297
  return
122
298
  apply_setup_changes(setup_status)
123
299
 
124
300
  @cli.command()
125
- @click.argument("flow_name", type=str, required=False)
301
+ @click.argument("app_flow_specifier", type=str)
126
302
  @click.option(
127
303
  "-L", "--live", is_flag=True, show_default=True, default=False,
128
304
  help="Continuously watch changes from data sources and apply to the target index.")
129
305
  @click.option(
130
306
  "-q", "--quiet", is_flag=True, show_default=True, default=False,
131
307
  help="Avoid printing anything to the standard output, e.g. statistics.")
132
- def update(flow_name: str | None, live: bool, quiet: bool):
308
+ def update(app_flow_specifier: str, live: bool, quiet: bool):
133
309
  """
134
310
  Update the index to reflect the latest data from data sources.
311
+
312
+ APP_FLOW_SPECIFIER: path/to/app.py, module, path/to/app.py:FlowName, or module:FlowName.
313
+ If :FlowName is omitted, updates all flows.
135
314
  """
315
+ app_ref, flow_ref = _parse_app_flow_specifier(app_flow_specifier)
316
+ _load_user_app(app_ref)
317
+
136
318
  options = flow.FlowLiveUpdaterOptions(live_mode=live, print_stats=not quiet)
137
- if flow_name is None:
319
+ if flow_ref is None:
138
320
  return flow.update_all_flows(options)
139
321
  else:
140
- with flow.FlowLiveUpdater(_flow_by_name(flow_name), options) as updater:
322
+ with flow.FlowLiveUpdater(_flow_by_name(flow_ref), options) as updater:
141
323
  updater.wait()
142
324
  return updater.update_stats()
143
325
 
144
326
  @cli.command()
145
- @click.argument("flow_name", type=str, required=False)
327
+ @click.argument("app_flow_specifier", type=str)
146
328
  @click.option(
147
329
  "-o", "--output-dir", type=str, required=False,
148
330
  help="The directory to dump the output to.")
149
331
  @click.option(
150
332
  "--cache/--no-cache", is_flag=True, show_default=True, default=True,
151
- help="Use already-cached intermediate data if available. "
152
- "Note that we only reuse existing cached data without updating the cache "
153
- "even if it's turned on.")
154
- def evaluate(flow_name: str | None, output_dir: str | None, cache: bool = True):
333
+ help="Use already-cached intermediate data if available.")
334
+ def evaluate(app_flow_specifier: str, output_dir: str | None, cache: bool = True):
155
335
  """
156
336
  Evaluate the flow and dump flow outputs to files.
157
337
 
158
338
  Instead of updating the index, it dumps what should be indexed to files.
159
339
  Mainly used for evaluation purpose.
340
+
341
+ APP_FLOW_SPECIFIER: Specifies the application and optionally the target flow.
342
+ Can be one of the following formats:\n
343
+ - path/to/your_app.py\n
344
+ - an_installed.module_name\n
345
+ - path/to/your_app.py:SpecificFlowName\n
346
+ - an_installed.module_name:SpecificFlowName
347
+
348
+ :SpecificFlowName can be omitted only if the application defines a single flow.
160
349
  """
161
- fl = _flow_by_name(flow_name)
350
+ app_ref, flow_ref = _parse_app_flow_specifier(app_flow_specifier)
351
+ _load_user_app(app_ref)
352
+
353
+ fl = _flow_by_name(flow_ref)
162
354
  if output_dir is None:
163
355
  output_dir = f"eval_{setting.get_app_namespace(trailing_delimiter='_')}{flow_name}_{datetime.datetime.now().strftime('%y%m%d_%H%M%S')}"
164
356
  options = flow.EvaluateAndDumpOptions(output_dir=output_dir, use_cache=cache)
165
357
  fl.evaluate_and_dump(options)
166
358
 
167
- # Create ServerSettings lazily upon first call, as environment variables may be loaded from files, etc.
168
- COCOINDEX_HOST = 'https://cocoindex.io'
169
-
170
359
  @cli.command()
360
+ @click.argument("app_target", type=str)
171
361
  @click.option(
172
362
  "-a", "--address", type=str,
173
363
  help="The address to bind the server to, in the format of IP:PORT. "
@@ -190,13 +380,18 @@ COCOINDEX_HOST = 'https://cocoindex.io'
190
380
  @click.option(
191
381
  "-q", "--quiet", is_flag=True, show_default=True, default=False,
192
382
  help="Avoid printing anything to the standard output, e.g. statistics.")
193
- def server(address: str | None, live_update: bool, quiet: bool, cors_origin: str | None,
194
- cors_cocoindex: bool, cors_local: int | None):
383
+ def server(app_target: str, address: str | None, live_update: bool, quiet: bool,
384
+ cors_origin: str | None, cors_cocoindex: bool, cors_local: int | None):
195
385
  """
196
386
  Start a HTTP server providing REST APIs.
197
387
 
198
388
  It will allow tools like CocoInsight to access the server.
389
+
390
+ APP_TARGET: path/to/app.py or installed_module.
199
391
  """
392
+ app_ref = _get_app_ref_from_specifier(app_target)
393
+ _load_user_app(app_ref)
394
+
200
395
  server_settings = setting.ServerSettings.from_env()
201
396
  cors_origins: set[str] = set(server_settings.cors_origins or [])
202
397
  if cors_origin is not None:
@@ -223,16 +418,20 @@ def server(address: str | None, live_update: bool, quiet: bool, cors_origin: str
223
418
 
224
419
  def _flow_name(name: str | None) -> str:
225
420
  names = flow.flow_names()
421
+ available = ', '.join(sorted(names))
226
422
  if name is not None:
227
423
  if name not in names:
228
- raise click.BadParameter(f"Flow {name} not found")
424
+ raise click.BadParameter(f"Flow '{name}' not found.\nAvailable: {available if names else 'None'}")
229
425
  return name
230
426
  if len(names) == 0:
231
- raise click.UsageError("No flows available")
427
+ raise click.UsageError("No flows available in the loaded application.")
232
428
  elif len(names) == 1:
233
429
  return names[0]
234
430
  else:
235
- raise click.UsageError("Multiple flows available, please specify --name")
431
+ raise click.UsageError(f"Multiple flows available, please specify which flow to target by appending :FlowName to the APP_TARGET.\nAvailable: {available}")
236
432
 
237
433
  def _flow_by_name(name: str | None) -> flow.Flow:
238
434
  return flow.flow_by_name(_flow_name(name))
435
+
436
+ if __name__ == "__main__":
437
+ cli()
cocoindex/flow.py CHANGED
@@ -489,7 +489,7 @@ class Flow:
489
489
  tree = Tree(f"Flow: {self.full_name}", style="cyan")
490
490
 
491
491
  def build_tree(label: str, lines: list):
492
- node = Tree(label, style="bold magenta" if lines else "cyan")
492
+ node = Tree(label=label if lines else label + " None", style="cyan")
493
493
  for line in lines:
494
494
  child_node = node.add(Text(line.content, style="yellow"))
495
495
  child_node.children = build_tree("", line.children).children
@@ -657,7 +657,7 @@ async def update_all_flows_async(options: FlowLiveUpdaterOptions) -> dict[str, _
657
657
 
658
658
  def _get_data_slice_annotation_type(data_slice_type: Type[DataSlice[T]]) -> Type[T] | None:
659
659
  type_args = get_args(data_slice_type)
660
- if data_slice_type is DataSlice:
660
+ if data_slice_type is inspect.Parameter.empty or data_slice_type is DataSlice:
661
661
  return None
662
662
  if get_origin(data_slice_type) != DataSlice or len(type_args) != 1:
663
663
  raise ValueError(f"Expect a DataSlice[T] type, but got {data_slice_type}")
cocoindex/lib.py CHANGED
@@ -1,19 +1,20 @@
1
1
  """
2
2
  Library level functions and states.
3
3
  """
4
- import sys
5
- import functools
6
- import inspect
4
+ import warnings
5
+ from typing import Callable, Any
7
6
 
8
- from typing import Callable
9
-
10
- from . import _engine
11
- from . import flow, query, cli, setting
7
+ from . import _engine, flow, query, setting
12
8
  from .convert import dump_engine_object
13
9
 
14
10
 
15
- def init(settings: setting.Settings):
16
- """Initialize the cocoindex library."""
11
+ def init(settings: setting.Settings | None = None):
12
+ """
13
+ Initialize the cocoindex library.
14
+
15
+ If the settings are not provided, they are loaded from the environment variables.
16
+ """
17
+ settings = settings or setting.Settings.from_env()
17
18
  _engine.init(dump_engine_object(settings))
18
19
  setting.set_app_namespace(settings.app_namespace)
19
20
 
@@ -29,50 +30,42 @@ def stop():
29
30
  _engine.stop()
30
31
 
31
32
  def main_fn(
32
- settings: setting.Settings | None = None,
33
- cocoindex_cmd: str = 'cocoindex',
33
+ settings: Any | None = None,
34
+ cocoindex_cmd: str | None = None,
34
35
  ) -> Callable[[Callable], Callable]:
35
36
  """
36
- A decorator to wrap the main function.
37
- If the python binary is called with the given command, it yields control to the cocoindex CLI.
37
+ DEPRECATED: The @cocoindex.main_fn() decorator is obsolete and has no effect.
38
+ It will be removed in a future version, which will cause an AttributeError.
38
39
 
39
- If the settings are not provided, they are loaded from the environment variables.
40
+ Please remove this decorator from your code and use the standalone 'cocoindex' CLI.
41
+ See the updated CLI usage examples in the warning message.
40
42
  """
41
-
42
- def _pre_init() -> None:
43
- effective_settings = settings or setting.Settings.from_env()
44
- init(effective_settings)
45
-
46
- def _should_run_cli() -> bool:
47
- return len(sys.argv) > 1 and sys.argv[1] == cocoindex_cmd
48
-
49
- def _run_cli():
50
- return cli.cli.main(sys.argv[2:], prog_name=f"{sys.argv[0]} {sys.argv[1]}")
43
+ warnings.warn(
44
+ "\n\n"
45
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
46
+ "CRITICAL DEPRECATION NOTICE from CocoIndex:\n"
47
+ "The @cocoindex.main_fn() decorator in your script is DEPRECATED and IGNORED.\n"
48
+ "It provides NO functionality and will be REMOVED entirely in a future version.\n"
49
+ "If not removed, your script will FAIL with an AttributeError in the future.\n\n"
50
+ "ACTION REQUIRED: Please REMOVE @cocoindex.main_fn() from your Python script.\n\n"
51
+ "To use CocoIndex, invoke the standalone 'cocoindex' CLI."
52
+ " Examples of new CLI usage:\n\n"
53
+ " To list flows from 'main.py' (previously 'python main.py cocoindex ls'):\n"
54
+ " cocoindex ls main.py\n\n"
55
+ " To list all persisted flows (previously 'python main.py cocoindex ls --all'):\n"
56
+ " cocoindex ls\n\n"
57
+ " To show 'MyFlow' defined in 'main.py' (previously 'python main.py cocoindex show MyFlow'):\n"
58
+ " cocoindex show main.py:MyFlow\n\n"
59
+ " To update all flows in 'my_package.flows_module':\n"
60
+ " cocoindex update my_package.flows_module\n\n"
61
+ " To update 'SpecificFlow' in 'my_package.flows_module':\n"
62
+ " cocoindex update my_package.flows_module:SpecificFlow\n\n"
63
+ "See cocoindex <command> --help for more details.\n"
64
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n",
65
+ DeprecationWarning,
66
+ stacklevel=2
67
+ )
51
68
 
52
69
  def _main_wrapper(fn: Callable) -> Callable:
53
- if inspect.iscoroutinefunction(fn):
54
- @functools.wraps(fn)
55
- async def _inner(*args, **kwargs):
56
- _pre_init()
57
- try:
58
- if _should_run_cli():
59
- # Schedule to a separate thread as it invokes nested event loop.
60
- # return await asyncio.to_thread(_run_cli)
61
- return _run_cli()
62
- return await fn(*args, **kwargs)
63
- finally:
64
- stop()
65
- return _inner
66
- else:
67
- @functools.wraps(fn)
68
- def _inner(*args, **kwargs):
69
- _pre_init()
70
- try:
71
- if _should_run_cli():
72
- return _run_cli()
73
- return fn(*args, **kwargs)
74
- finally:
75
- stop()
76
- return _inner
77
-
70
+ return fn
78
71
  return _main_wrapper
cocoindex/typing.py CHANGED
@@ -168,6 +168,8 @@ def analyze_type_info(t) -> AnalyzedTypeInfo:
168
168
  kind = 'Time'
169
169
  elif t is datetime.datetime:
170
170
  kind = 'OffsetDateTime'
171
+ elif t is datetime.timedelta:
172
+ kind = 'TimeDelta'
171
173
  else:
172
174
  raise ValueError(f"type unsupported yet: {t}")
173
175
 
@@ -1,9 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cocoindex
3
- Version: 0.1.41
3
+ Version: 0.1.42
4
4
  Requires-Dist: sentence-transformers>=3.3.1
5
5
  Requires-Dist: click>=8.1.8
6
6
  Requires-Dist: rich>=14.0.0
7
+ Requires-Dist: python-dotenv>=1.1.0
7
8
  Requires-Dist: pytest ; extra == 'test'
8
9
  Provides-Extra: test
9
10
  License-File: LICENSE
@@ -1,15 +1,16 @@
1
- cocoindex-0.1.41.dist-info/METADATA,sha256=_wnwGo7A-7PLOp6HuG18kFtC-ld4QEP4RtKwVEWdpQ4,9790
2
- cocoindex-0.1.41.dist-info/WHEEL,sha256=UPfJ7S-gMCqCJ6cj5sliE010L87pWgiShNoxmys5TN4,104
3
- cocoindex-0.1.41.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
1
+ cocoindex-0.1.42.dist-info/METADATA,sha256=-ukEQ2NadNRsjsH5WOnIQ4RuPN8oDBlLc2DJ4NyLilg,9826
2
+ cocoindex-0.1.42.dist-info/WHEEL,sha256=UPfJ7S-gMCqCJ6cj5sliE010L87pWgiShNoxmys5TN4,104
3
+ cocoindex-0.1.42.dist-info/entry_points.txt,sha256=_NretjYVzBdNTn7dK-zgwr7YfG2afz1u1uSE-5bZXF8,46
4
+ cocoindex-0.1.42.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
4
5
  cocoindex/__init__.py,sha256=0iO3lM8w35zhVDPsvCGBAXwUQZsCWRRqjt2wFdtJhME,834
5
- cocoindex/_engine.cpython-313-darwin.so,sha256=boDiMX3DQ8TsLMtLlxn2E0MSVeFNS4ER9d3AVaBlazQ,56853280
6
+ cocoindex/_engine.cpython-313-darwin.so,sha256=3J5U0Twf2TYRTTyXAHc5ELPlxHgaZw-7mUCy5yAOJNs,56872480
6
7
  cocoindex/auth_registry.py,sha256=NsALZ3SKsDG9cPdrlTlalIqUvgbgFOaFGAbWJNedtJE,692
7
- cocoindex/cli.py,sha256=Ac3ybnQW-HGVGJeUwIOHd1qhjs0KC5wCsemWuyouEfU,8999
8
+ cocoindex/cli.py,sha256=1FvpeLjLHSu4EC3Ds7eeSDjBNTtAc06nsuORkNjv_NQ,16569
8
9
  cocoindex/convert.py,sha256=75HSBie7DokM0RJyUBqeveZRl5y_Fl8lzByoRF0yb2M,6915
9
- cocoindex/flow.py,sha256=FJGuw71q8EPd37DUbveiMV-LTpqRG2ckQ7jM5bVMJcw,28356
10
+ cocoindex/flow.py,sha256=0otFFqO8XVv-uc1Qbya9BBCK1orv7CGI52TiB5v63xk,28409
10
11
  cocoindex/functions.py,sha256=F79dNmGE127LaU67kF5Oqtf_tIzebFQH7MkyceMX4-s,1830
11
12
  cocoindex/index.py,sha256=LssEOuZi6AqhwKtZM3QFeQpa9T-0ELi8G5DsrYKECvc,534
12
- cocoindex/lib.py,sha256=OqTMuOHicdyX9PRA7fmTzznK8HZMrzxpUDbqxAEF--Q,2383
13
+ cocoindex/lib.py,sha256=JiLIV5Fi-FNKMlH-6JMoe-UQsvirnFkkOLcoEQk9WkE,2903
13
14
  cocoindex/llm.py,sha256=_3rtahuKcqcEHPkFSwhXOSrekZyGxVApPoYtlU_chcA,348
14
15
  cocoindex/op.py,sha256=yyB3gYYj6uIeoGW9FXuj9Ludaz50QYDeqGgi3dKG1_I,10739
15
16
  cocoindex/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,6 +22,6 @@ cocoindex/sources.py,sha256=7lpwYLsFCRfbURKf79Vu0JZZoXjAYY0DxNHzUb-VHBY,1327
21
22
  cocoindex/storages.py,sha256=MFMsfyOCYMggTWeWrOi82miqOXQmiUuqq828x5htBr0,2207
22
23
  cocoindex/tests/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
23
24
  cocoindex/tests/test_convert.py,sha256=7jc--I3frrg7DB5MPr4JFzE7DSCznJuWyHdlDLQJ_fM,15516
24
- cocoindex/typing.py,sha256=369ABRtnpbaVSQVIBc2ZDutXW8jUmncvNJd9CHEWT3Q,8962
25
+ cocoindex/typing.py,sha256=RMJXdey5_vd7JbPTEdUw1DeQ4Uu4MicdiaXjzQHFNRA,9031
25
26
  cocoindex/utils.py,sha256=eClhMdjBjcXaDkah-rPUmE7Y5Ncd7S1goUe2qTETR08,456
26
- cocoindex-0.1.41.dist-info/RECORD,,
27
+ cocoindex-0.1.42.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cocoindex=cocoindex.cli:cli