cocoindex 0.1.41__cp311-cp311-macosx_11_0_arm64.whl → 0.1.43__cp311-cp311-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.
cocoindex/__init__.py CHANGED
@@ -5,7 +5,7 @@ from . import functions, query, sources, storages, cli, utils
5
5
 
6
6
  from .auth_registry import AuthEntryReference, add_auth_entry, ref_auth_entry
7
7
  from .flow import FlowBuilder, DataScope, DataSlice, Flow, transform_flow
8
- from .flow import flow_def, flow_def as flow
8
+ from .flow import flow_def
9
9
  from .flow import EvaluateAndDumpOptions, GeneratedField
10
10
  from .flow import update_all_flows_async, FlowLiveUpdater, FlowLiveUpdaterOptions
11
11
  from .lib import init, start_server, stop, main_fn
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,127 @@ 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
+ \b
255
+ Modes of operation:
256
+ 1. Drop ALL persisted setups: `cocoindex drop --all`
257
+ 2. Drop all flows defined in an app: `cocoindex drop <APP_TARGET>`
258
+ 3. Drop specific named flows: `cocoindex drop <APP_TARGET> [FLOW_NAME...]`
107
259
  """
260
+ app_ref = None
261
+ flow_names = []
262
+
108
263
  if drop_all:
264
+ if app_target or flow_name:
265
+ click.echo("Warning: When --all is used, APP_TARGET and any individual flow names are ignored.", err=True)
109
266
  flow_names = flow_names_with_setup()
110
- elif len(flow_name) == 0:
111
- flow_names = flow.flow_names()
267
+ elif app_target:
268
+ app_ref = _get_app_ref_from_specifier(app_target)
269
+ _load_user_app(app_ref)
270
+ if flow_name:
271
+ flow_names = list(flow_name)
272
+ click.echo(f"Preparing to drop specified flows: {', '.join(flow_names)} (in '{app_ref}').", err=True)
273
+ else:
274
+ flow_names = flow.flow_names()
275
+ if not flow_names:
276
+ click.echo(f"No flows found defined in '{app_ref}' to drop.")
277
+ return
278
+ click.echo(f"Preparing to drop all flows defined in '{app_ref}': {', '.join(flow_names)}.", err=True)
112
279
  else:
113
- flow_names = list(flow_name)
280
+ raise click.UsageError(
281
+ "Missing arguments. You must either provide an APP_TARGET (to target app-specific flows) "
282
+ "or use the --all flag."
283
+ )
284
+
285
+ if not flow_names:
286
+ click.echo("No flows identified for the drop operation.")
287
+ return
288
+
114
289
  setup_status = drop_setup(flow_names)
115
290
  click.echo(setup_status)
116
291
  if setup_status.is_up_to_date():
117
292
  click.echo("No flows need to be dropped.")
118
293
  return
119
294
  if not click.confirm(
120
- "Changes need to be pushed. Continue? [yes/N]", default=False, show_default=False):
295
+ f"\nThis will apply changes to drop setup for: {', '.join(flow_names)}. Continue? [yes/N]",
296
+ default=False, show_default=False):
297
+ click.echo("Drop operation aborted by user.")
121
298
  return
122
299
  apply_setup_changes(setup_status)
123
300
 
124
301
  @cli.command()
125
- @click.argument("flow_name", type=str, required=False)
302
+ @click.argument("app_flow_specifier", type=str)
126
303
  @click.option(
127
304
  "-L", "--live", is_flag=True, show_default=True, default=False,
128
305
  help="Continuously watch changes from data sources and apply to the target index.")
129
306
  @click.option(
130
307
  "-q", "--quiet", is_flag=True, show_default=True, default=False,
131
308
  help="Avoid printing anything to the standard output, e.g. statistics.")
132
- def update(flow_name: str | None, live: bool, quiet: bool):
309
+ def update(app_flow_specifier: str, live: bool, quiet: bool):
133
310
  """
134
311
  Update the index to reflect the latest data from data sources.
312
+
313
+ APP_FLOW_SPECIFIER: path/to/app.py, module, path/to/app.py:FlowName, or module:FlowName.
314
+ If :FlowName is omitted, updates all flows.
135
315
  """
316
+ app_ref, flow_ref = _parse_app_flow_specifier(app_flow_specifier)
317
+ _load_user_app(app_ref)
318
+
136
319
  options = flow.FlowLiveUpdaterOptions(live_mode=live, print_stats=not quiet)
137
- if flow_name is None:
320
+ if flow_ref is None:
138
321
  return flow.update_all_flows(options)
139
322
  else:
140
- with flow.FlowLiveUpdater(_flow_by_name(flow_name), options) as updater:
323
+ with flow.FlowLiveUpdater(_flow_by_name(flow_ref), options) as updater:
141
324
  updater.wait()
142
325
  return updater.update_stats()
143
326
 
144
327
  @cli.command()
145
- @click.argument("flow_name", type=str, required=False)
328
+ @click.argument("app_flow_specifier", type=str)
146
329
  @click.option(
147
330
  "-o", "--output-dir", type=str, required=False,
148
331
  help="The directory to dump the output to.")
149
332
  @click.option(
150
333
  "--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):
334
+ help="Use already-cached intermediate data if available.")
335
+ def evaluate(app_flow_specifier: str, output_dir: str | None, cache: bool = True):
155
336
  """
156
337
  Evaluate the flow and dump flow outputs to files.
157
338
 
158
339
  Instead of updating the index, it dumps what should be indexed to files.
159
340
  Mainly used for evaluation purpose.
341
+
342
+ \b
343
+ APP_FLOW_SPECIFIER: Specifies the application and optionally the target flow.
344
+ Can be one of the following formats:
345
+ - path/to/your_app.py
346
+ - an_installed.module_name
347
+ - path/to/your_app.py:SpecificFlowName
348
+ - an_installed.module_name:SpecificFlowName
349
+
350
+ :SpecificFlowName can be omitted only if the application defines a single flow.
160
351
  """
161
- fl = _flow_by_name(flow_name)
352
+ app_ref, flow_ref = _parse_app_flow_specifier(app_flow_specifier)
353
+ _load_user_app(app_ref)
354
+
355
+ fl = _flow_by_name(flow_ref)
162
356
  if output_dir is None:
163
357
  output_dir = f"eval_{setting.get_app_namespace(trailing_delimiter='_')}{flow_name}_{datetime.datetime.now().strftime('%y%m%d_%H%M%S')}"
164
358
  options = flow.EvaluateAndDumpOptions(output_dir=output_dir, use_cache=cache)
165
359
  fl.evaluate_and_dump(options)
166
360
 
167
- # Create ServerSettings lazily upon first call, as environment variables may be loaded from files, etc.
168
- COCOINDEX_HOST = 'https://cocoindex.io'
169
-
170
361
  @cli.command()
362
+ @click.argument("app_target", type=str)
171
363
  @click.option(
172
364
  "-a", "--address", type=str,
173
365
  help="The address to bind the server to, in the format of IP:PORT. "
@@ -190,13 +382,18 @@ COCOINDEX_HOST = 'https://cocoindex.io'
190
382
  @click.option(
191
383
  "-q", "--quiet", is_flag=True, show_default=True, default=False,
192
384
  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):
385
+ def server(app_target: str, address: str | None, live_update: bool, quiet: bool,
386
+ cors_origin: str | None, cors_cocoindex: bool, cors_local: int | None):
195
387
  """
196
388
  Start a HTTP server providing REST APIs.
197
389
 
198
390
  It will allow tools like CocoInsight to access the server.
391
+
392
+ APP_TARGET: path/to/app.py or installed_module.
199
393
  """
394
+ app_ref = _get_app_ref_from_specifier(app_target)
395
+ _load_user_app(app_ref)
396
+
200
397
  server_settings = setting.ServerSettings.from_env()
201
398
  cors_origins: set[str] = set(server_settings.cors_origins or [])
202
399
  if cors_origin is not None:
@@ -223,16 +420,20 @@ def server(address: str | None, live_update: bool, quiet: bool, cors_origin: str
223
420
 
224
421
  def _flow_name(name: str | None) -> str:
225
422
  names = flow.flow_names()
423
+ available = ', '.join(sorted(names))
226
424
  if name is not None:
227
425
  if name not in names:
228
- raise click.BadParameter(f"Flow {name} not found")
426
+ raise click.BadParameter(f"Flow '{name}' not found.\nAvailable: {available if names else 'None'}")
229
427
  return name
230
428
  if len(names) == 0:
231
- raise click.UsageError("No flows available")
429
+ raise click.UsageError("No flows available in the loaded application.")
232
430
  elif len(names) == 1:
233
431
  return names[0]
234
432
  else:
235
- raise click.UsageError("Multiple flows available, please specify --name")
433
+ raise click.UsageError(f"Multiple flows available, please specify which flow to target by appending :FlowName to the APP_TARGET.\nAvailable: {available}")
236
434
 
237
435
  def _flow_by_name(name: str | None) -> flow.Flow:
238
436
  return flow.flow_by_name(_flow_name(name))
437
+
438
+ if __name__ == "__main__":
439
+ 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.43
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
@@ -154,7 +155,7 @@ It defines an index flow like this:
154
155
  | [Embeddings to Qdrant](examples/text_embedding_qdrant) | Index documents in a Qdrant collection for semantic search |
155
156
  | [FastAPI Server with Docker](examples/fastapi_server_docker) | Run the semantic search server in a Dockerized FastAPI setup |
156
157
  | [Product Recommendation](examples/product_recommendation) | Build real-time product recommendations with LLM and graph database|
157
- | [Image Search with Vision API](examples/image_search_example) | Generates detailed captions for images using a vision model, embeds them, enables live-updating semantic search via FastAPI and served on a React frontend|
158
+ | [Image Search with Vision API](examples/image_search) | Generates detailed captions for images using a vision model, embeds them, enables live-updating semantic search via FastAPI and served on a React frontend|
158
159
 
159
160
  More coming and stay tuned 👀!
160
161
 
@@ -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=p_tvkyHH2UmMBrR2Gemb1ahXJMM2SXUIsCLrWZgJvB8,104
3
- cocoindex-0.1.41.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
4
- cocoindex/__init__.py,sha256=0iO3lM8w35zhVDPsvCGBAXwUQZsCWRRqjt2wFdtJhME,834
5
- cocoindex/_engine.cpython-311-darwin.so,sha256=rloTA56_Z9ab9o5O9K-AF5h1MRql7xPVR5kB5-Ez2Ig,56867248
1
+ cocoindex-0.1.43.dist-info/METADATA,sha256=2n3yADdVbrbv-m-x7qtZX8l8D_2h37MEP2tY5OqTyBo,9818
2
+ cocoindex-0.1.43.dist-info/WHEEL,sha256=p_tvkyHH2UmMBrR2Gemb1ahXJMM2SXUIsCLrWZgJvB8,104
3
+ cocoindex-0.1.43.dist-info/entry_points.txt,sha256=_NretjYVzBdNTn7dK-zgwr7YfG2afz1u1uSE-5bZXF8,46
4
+ cocoindex-0.1.43.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
5
+ cocoindex/__init__.py,sha256=ztuMoVHbGsyhgnN_CHBnY4yQtcsHi4O-u-Q3eUFaB5Y,816
6
+ cocoindex/_engine.cpython-311-darwin.so,sha256=yTewruRgB6wQCP00OjCDNCILMVk0dEInGPHF3jhCrRg,56886416
6
7
  cocoindex/auth_registry.py,sha256=NsALZ3SKsDG9cPdrlTlalIqUvgbgFOaFGAbWJNedtJE,692
7
- cocoindex/cli.py,sha256=Ac3ybnQW-HGVGJeUwIOHd1qhjs0KC5wCsemWuyouEfU,8999
8
+ cocoindex/cli.py,sha256=Gbn6vz37Gi5ckZYdRdJkcbNcokLbtDhH9wrZswISeRc,16554
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.43.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cocoindex=cocoindex.cli:cli