provide-foundation 0.0.0.dev0__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.
Files changed (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,174 @@
1
+ """
2
+ Query logs command for Foundation CLI.
3
+ """
4
+
5
+ try:
6
+ import click
7
+
8
+ _HAS_CLICK = True
9
+ except ImportError:
10
+ click = None
11
+ _HAS_CLICK = False
12
+
13
+ from provide.foundation.logger import get_logger
14
+
15
+ log = get_logger(__name__)
16
+
17
+
18
+ if _HAS_CLICK:
19
+
20
+ @click.command("query")
21
+ @click.option(
22
+ "--sql",
23
+ help="SQL query to execute (if not provided, builds from other options)",
24
+ )
25
+ @click.option(
26
+ "--current-trace",
27
+ is_flag=True,
28
+ help="Query logs for the current active trace",
29
+ )
30
+ @click.option(
31
+ "--trace-id",
32
+ help="Query logs for a specific trace ID",
33
+ )
34
+ @click.option(
35
+ "--level",
36
+ type=click.Choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]),
37
+ help="Filter by log level",
38
+ )
39
+ @click.option(
40
+ "--service",
41
+ help="Filter by service name",
42
+ )
43
+ @click.option(
44
+ "--last",
45
+ help="Time range (e.g., 1h, 30m, 5m)",
46
+ default="1h",
47
+ )
48
+ @click.option(
49
+ "--stream",
50
+ default="default",
51
+ help="Stream to query",
52
+ )
53
+ @click.option(
54
+ "--size",
55
+ "-n",
56
+ type=int,
57
+ default=100,
58
+ help="Number of results",
59
+ )
60
+ @click.option(
61
+ "--format",
62
+ "-f",
63
+ type=click.Choice(["json", "log", "table", "csv", "summary"]),
64
+ default="log",
65
+ help="Output format",
66
+ )
67
+ @click.pass_context
68
+ def query_command(
69
+ ctx, sql, current_trace, trace_id, level, service, last, stream, size, format
70
+ ):
71
+ """Query logs from OpenObserve.
72
+
73
+ Examples:
74
+ # Query recent logs
75
+ foundation logs query --last 30m
76
+
77
+ # Query errors
78
+ foundation logs query --level ERROR --last 1h
79
+
80
+ # Query by current trace
81
+ foundation logs query --current-trace
82
+
83
+ # Query by specific trace
84
+ foundation logs query --trace-id abc123def456
85
+
86
+ # Query by service
87
+ foundation logs query --service auth-service --last 15m
88
+
89
+ # Custom SQL query
90
+ foundation logs query --sql "SELECT * FROM default WHERE duration_ms > 1000"
91
+ """
92
+ from provide.foundation.observability.openobserve import (
93
+ format_output,
94
+ search_logs,
95
+ )
96
+
97
+ client = ctx.obj.get("client")
98
+ if not client:
99
+ click.echo("Error: OpenObserve not configured.", err=True)
100
+ return 1
101
+
102
+ # Build SQL query if not provided
103
+ if not sql:
104
+ # Handle current trace
105
+ if current_trace:
106
+ try:
107
+ # Try OpenTelemetry first
108
+ from opentelemetry import trace
109
+
110
+ current_span = trace.get_current_span()
111
+ if current_span and current_span.is_recording():
112
+ span_context = current_span.get_span_context()
113
+ trace_id = f"{span_context.trace_id:032x}"
114
+ else:
115
+ # Try Foundation tracer
116
+ from provide.foundation.tracer.context import (
117
+ get_current_trace_id,
118
+ )
119
+
120
+ trace_id = get_current_trace_id()
121
+ if not trace_id:
122
+ click.echo("No active trace found.", err=True)
123
+ return 1
124
+ except ImportError:
125
+ click.echo("Tracing not available.", err=True)
126
+ return 1
127
+
128
+ # Build WHERE clause
129
+ conditions = []
130
+ if trace_id:
131
+ conditions.append(f"trace_id = '{trace_id}'")
132
+ if level:
133
+ conditions.append(f"level = '{level}'")
134
+ if service:
135
+ conditions.append(f"service = '{service}'")
136
+
137
+ where_clause = f"WHERE {' AND '.join(conditions)}" if conditions else ""
138
+ sql = f"SELECT * FROM {stream} {where_clause} ORDER BY _timestamp DESC LIMIT {size}"
139
+
140
+ # Execute query
141
+ try:
142
+ response = search_logs(
143
+ sql=sql,
144
+ start_time=f"-{last}" if last else "-1h",
145
+ end_time="now",
146
+ size=size,
147
+ client=client,
148
+ )
149
+
150
+ # Format and display results
151
+ if response.total == 0:
152
+ click.echo("No logs found matching the query.")
153
+ else:
154
+ output = format_output(response, format_type=format)
155
+ click.echo(output)
156
+
157
+ # Show summary for non-summary formats
158
+ if format != "summary":
159
+ click.echo(
160
+ f"\n📊 Found {response.total} logs, showing {len(response.hits)}"
161
+ )
162
+
163
+ except Exception as e:
164
+ click.echo(f"Query failed: {e}", err=True)
165
+ return 1
166
+
167
+ else:
168
+
169
+ def query_command(*args, **kwargs):
170
+ """Query command stub when click is not available."""
171
+ raise ImportError(
172
+ "CLI commands require optional dependencies. "
173
+ "Install with: pip install 'provide-foundation[cli]'"
174
+ )
@@ -0,0 +1,166 @@
1
+ """
2
+ Send logs command for Foundation CLI.
3
+ """
4
+
5
+ import json
6
+ import sys
7
+
8
+ try:
9
+ import click
10
+
11
+ _HAS_CLICK = True
12
+ except ImportError:
13
+ click = None
14
+ _HAS_CLICK = False
15
+
16
+ from provide.foundation.logger import get_logger
17
+
18
+ log = get_logger(__name__)
19
+
20
+
21
+ if _HAS_CLICK:
22
+
23
+ @click.command("send")
24
+ @click.option(
25
+ "--message",
26
+ "-m",
27
+ help="Log message to send (reads from stdin if not provided)",
28
+ )
29
+ @click.option(
30
+ "--level",
31
+ "-l",
32
+ type=click.Choice(["TRACE", "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"]),
33
+ default="INFO",
34
+ help="Log level",
35
+ )
36
+ @click.option(
37
+ "--service",
38
+ "-s",
39
+ help="Service name (uses config default if not provided)",
40
+ )
41
+ @click.option(
42
+ "--json",
43
+ "-j",
44
+ "json_attrs",
45
+ help="Additional attributes as JSON",
46
+ )
47
+ @click.option(
48
+ "--attr",
49
+ "-a",
50
+ multiple=True,
51
+ help="Additional attributes as key=value pairs",
52
+ )
53
+ @click.option(
54
+ "--trace-id",
55
+ help="Explicit trace ID to use",
56
+ )
57
+ @click.option(
58
+ "--span-id",
59
+ help="Explicit span ID to use",
60
+ )
61
+ @click.option(
62
+ "--otlp/--bulk",
63
+ "use_otlp",
64
+ default=True,
65
+ help="Use OTLP (default) or bulk API",
66
+ )
67
+ @click.pass_context
68
+ def send_command(
69
+ ctx, message, level, service, json_attrs, attr, trace_id, span_id, use_otlp
70
+ ):
71
+ """Send a log entry to OpenObserve.
72
+
73
+ Examples:
74
+ # Send a simple log
75
+ foundation logs send -m "User logged in" -l INFO
76
+
77
+ # Send with attributes
78
+ foundation logs send -m "Payment processed" --attr user_id=123 --attr amount=99.99
79
+
80
+ # Send from stdin
81
+ echo "Application started" | foundation logs send -l INFO
82
+
83
+ # Send with JSON attributes
84
+ foundation logs send -m "Error occurred" -j '{"error_code": 500, "path": "/api/users"}'
85
+ """
86
+ from provide.foundation.observability.openobserve.otlp import send_log
87
+
88
+ # Get message from stdin if not provided
89
+ if not message:
90
+ if sys.stdin.isatty():
91
+ click.echo(
92
+ "Error: No message provided. Use -m or pipe input.", err=True
93
+ )
94
+ return 1
95
+ message = sys.stdin.read().strip()
96
+ if not message:
97
+ click.echo("Error: Empty message from stdin.", err=True)
98
+ return 1
99
+
100
+ # Build attributes
101
+ attributes = {}
102
+
103
+ # Add JSON attributes
104
+ if json_attrs:
105
+ try:
106
+ attributes.update(json.loads(json_attrs))
107
+ except json.JSONDecodeError as e:
108
+ click.echo(f"Error: Invalid JSON attributes: {e}", err=True)
109
+ return 1
110
+
111
+ # Add key=value attributes
112
+ for kv in attr:
113
+ if "=" not in kv:
114
+ click.echo(
115
+ f"Error: Invalid attribute format '{kv}'. Use key=value.", err=True
116
+ )
117
+ return 1
118
+ key, value = kv.split("=", 1)
119
+ # Try to parse value as number
120
+ try:
121
+ if "." in value:
122
+ value = float(value)
123
+ else:
124
+ value = int(value)
125
+ except ValueError:
126
+ pass # Keep as string
127
+ attributes[key] = value
128
+
129
+ # Add explicit trace/span IDs if provided
130
+ if trace_id:
131
+ attributes["trace_id"] = trace_id
132
+ if span_id:
133
+ attributes["span_id"] = span_id
134
+
135
+ # Send the log
136
+ try:
137
+ client = ctx.obj.get("client")
138
+ success = send_log(
139
+ message=message,
140
+ level=level,
141
+ service=service,
142
+ attributes=attributes if attributes else None,
143
+ prefer_otlp=use_otlp,
144
+ client=client,
145
+ )
146
+
147
+ if success:
148
+ click.echo(
149
+ f"✅ Log sent successfully via {'OTLP' if use_otlp else 'bulk API'}"
150
+ )
151
+ else:
152
+ click.echo("❌ Failed to send log", err=True)
153
+ return 1
154
+
155
+ except Exception as e:
156
+ click.echo(f"Error: {e}", err=True)
157
+ return 1
158
+
159
+ else:
160
+
161
+ def send_command(*args, **kwargs):
162
+ """Send command stub when click is not available."""
163
+ raise ImportError(
164
+ "CLI commands require optional dependencies. "
165
+ "Install with: pip install 'provide-foundation[cli]'"
166
+ )
@@ -0,0 +1,112 @@
1
+ """
2
+ Tail logs command for Foundation CLI.
3
+ """
4
+
5
+ try:
6
+ import click
7
+
8
+ _HAS_CLICK = True
9
+ except ImportError:
10
+ click = None
11
+ _HAS_CLICK = False
12
+
13
+ from provide.foundation.logger import get_logger
14
+
15
+ log = get_logger(__name__)
16
+
17
+
18
+ if _HAS_CLICK:
19
+
20
+ @click.command("tail")
21
+ @click.option(
22
+ "--stream",
23
+ "-s",
24
+ default="default",
25
+ help="Stream to tail",
26
+ )
27
+ @click.option(
28
+ "--filter",
29
+ "-f",
30
+ "filter_sql",
31
+ help="SQL WHERE clause for filtering",
32
+ )
33
+ @click.option(
34
+ "--lines",
35
+ "-n",
36
+ type=int,
37
+ default=10,
38
+ help="Number of initial lines to show",
39
+ )
40
+ @click.option(
41
+ "--follow/--no-follow",
42
+ "-F/-N",
43
+ default=True,
44
+ help="Follow mode (like tail -f)",
45
+ )
46
+ @click.option(
47
+ "--format",
48
+ type=click.Choice(["log", "json"]),
49
+ default="log",
50
+ help="Output format",
51
+ )
52
+ @click.pass_context
53
+ def tail_command(ctx, stream, filter_sql, lines, follow, format):
54
+ """Tail logs in real-time (like 'tail -f').
55
+
56
+ Examples:
57
+ # Tail all logs
58
+ foundation logs tail
59
+
60
+ # Tail error logs only
61
+ foundation logs tail --filter "level='ERROR'"
62
+
63
+ # Tail specific service
64
+ foundation logs tail --filter "service='auth-service'"
65
+
66
+ # Show last 20 lines and exit
67
+ foundation logs tail -n 20 --no-follow
68
+
69
+ # Tail with JSON output
70
+ foundation logs tail --format json
71
+ """
72
+ from provide.foundation.observability.openobserve import (
73
+ format_output,
74
+ tail_logs,
75
+ )
76
+
77
+ client = ctx.obj.get("client")
78
+ if not client:
79
+ click.echo("Error: OpenObserve not configured.", err=True)
80
+ return 1
81
+
82
+ try:
83
+ click.echo(f"📡 Tailing logs from stream '{stream}'...")
84
+ if filter_sql:
85
+ click.echo(f" Filter: {filter_sql}")
86
+ click.echo(" Press Ctrl+C to stop\n")
87
+
88
+ # Tail logs
89
+ for log_entry in tail_logs(
90
+ stream=stream,
91
+ filter_sql=filter_sql,
92
+ follow=follow,
93
+ lines=lines,
94
+ client=client,
95
+ ):
96
+ output = format_output(log_entry, format_type=format)
97
+ click.echo(output)
98
+
99
+ except KeyboardInterrupt:
100
+ click.echo("\n✋ Stopped tailing logs.")
101
+ except Exception as e:
102
+ click.echo(f"Tail failed: {e}", err=True)
103
+ return 1
104
+
105
+ else:
106
+
107
+ def tail_command(*args, **kwargs):
108
+ """Tail command stub when click is not available."""
109
+ raise ImportError(
110
+ "CLI commands require optional dependencies. "
111
+ "Install with: pip install 'provide-foundation[cli]'"
112
+ )
@@ -0,0 +1,262 @@
1
+ """Standard CLI decorators for consistent option handling."""
2
+
3
+ from collections.abc import Callable
4
+ import functools
5
+ from pathlib import Path
6
+ import sys
7
+ from typing import Any, TypeVar
8
+
9
+ try:
10
+ import click
11
+ except ImportError:
12
+ click = None
13
+
14
+ from provide.foundation.context import Context
15
+
16
+ F = TypeVar("F", bound=Callable[..., Any])
17
+
18
+ # Standard log level choices
19
+ LOG_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
20
+
21
+
22
+ def logging_options(f: F) -> F:
23
+ """
24
+ Add standard logging options to a Click command.
25
+
26
+ Adds:
27
+ - --log-level/-l: Set logging verbosity (DEBUG, INFO, WARNING, ERROR, CRITICAL)
28
+ - --log-file: Write logs to file
29
+ - --log-format: Choose log output format (json, text, key_value)
30
+ """
31
+ f = click.option(
32
+ "--log-level",
33
+ "-l",
34
+ type=click.Choice(LOG_LEVELS, case_sensitive=False),
35
+ default=None,
36
+ envvar="PROVIDE_LOG_LEVEL",
37
+ help="Set the logging level",
38
+ )(f)
39
+ f = click.option(
40
+ "--log-file",
41
+ type=click.Path(dir_okay=False, writable=True, path_type=Path),
42
+ default=None,
43
+ envvar="PROVIDE_LOG_FILE",
44
+ help="Write logs to file",
45
+ )(f)
46
+ f = click.option(
47
+ "--log-format",
48
+ type=click.Choice(["json", "text", "key_value"], case_sensitive=False),
49
+ default="key_value",
50
+ envvar="PROVIDE_LOG_FORMAT",
51
+ help="Log output format",
52
+ )(f)
53
+ return f
54
+
55
+
56
+ def config_options(f: F) -> F:
57
+ """
58
+ Add configuration file options to a Click command.
59
+
60
+ Adds:
61
+ - --config/-c: Path to configuration file
62
+ - --profile/-p: Configuration profile to use
63
+ """
64
+ f = click.option(
65
+ "--config",
66
+ "-c",
67
+ type=click.Path(exists=True, dir_okay=False, path_type=Path),
68
+ default=None,
69
+ envvar="PROVIDE_CONFIG_FILE",
70
+ help="Path to configuration file",
71
+ )(f)
72
+ f = click.option(
73
+ "--profile",
74
+ "-p",
75
+ default=None,
76
+ envvar="PROVIDE_PROFILE",
77
+ help="Configuration profile to use",
78
+ )(f)
79
+ return f
80
+
81
+
82
+ def output_options(f: F) -> F:
83
+ """
84
+ Add output formatting options to a Click command.
85
+
86
+ Adds:
87
+ - --json: Output in JSON format
88
+ - --no-color: Disable colored output
89
+ - --no-emoji: Disable emoji in output
90
+ """
91
+ f = click.option(
92
+ "--json",
93
+ "json_output",
94
+ is_flag=True,
95
+ default=None,
96
+ envvar="PROVIDE_JSON_OUTPUT",
97
+ help="Output in JSON format",
98
+ )(f)
99
+ f = click.option(
100
+ "--no-color",
101
+ is_flag=True,
102
+ default=False,
103
+ envvar="PROVIDE_NO_COLOR",
104
+ help="Disable colored output",
105
+ )(f)
106
+ f = click.option(
107
+ "--no-emoji",
108
+ is_flag=True,
109
+ default=False,
110
+ envvar="PROVIDE_NO_EMOJI",
111
+ help="Disable emoji in output",
112
+ )(f)
113
+ return f
114
+
115
+
116
+ def flexible_options(f: F) -> F:
117
+ """
118
+ Apply flexible CLI options that can be used at any command level.
119
+
120
+ Combines logging_options and config_options for consistent
121
+ control at both group and command levels.
122
+ """
123
+ f = logging_options(f)
124
+ f = config_options(f)
125
+ return f
126
+
127
+
128
+ def standard_options(f: F) -> F:
129
+ """
130
+ Apply all standard CLI options.
131
+
132
+ Combines logging_options, config_options, and output_options.
133
+
134
+ Note: Consider using flexible_options for better granular control.
135
+ This decorator is maintained for backward compatibility.
136
+ """
137
+ f = logging_options(f)
138
+ f = config_options(f)
139
+ f = output_options(f)
140
+ return f
141
+
142
+
143
+ def error_handler(f: F) -> F:
144
+ """
145
+ Decorator to handle errors consistently in CLI commands.
146
+
147
+ Catches exceptions and formats them appropriately based on
148
+ debug mode and output format.
149
+ """
150
+
151
+ @functools.wraps(f)
152
+ def wrapper(*args, **kwargs):
153
+ click.get_current_context()
154
+ debug = kwargs.get("debug", False)
155
+ json_output = kwargs.get("json_output", False)
156
+
157
+ try:
158
+ return f(*args, **kwargs)
159
+ except click.ClickException:
160
+ # Let Click handle its own exceptions
161
+ raise
162
+ except KeyboardInterrupt:
163
+ if not json_output:
164
+ click.secho("\nInterrupted by user", fg="yellow", err=True)
165
+ sys.exit(130) # Standard exit code for SIGINT
166
+ except Exception as e:
167
+ if debug:
168
+ # In debug mode, show full traceback
169
+ raise
170
+
171
+ if json_output:
172
+ import json
173
+
174
+ error_data = {
175
+ "error": str(e),
176
+ "type": type(e).__name__,
177
+ }
178
+ click.echo(json.dumps(error_data), err=True)
179
+ else:
180
+ click.secho(f"Error: {e}", fg="red", err=True)
181
+
182
+ sys.exit(1)
183
+
184
+ return wrapper
185
+
186
+
187
+ def pass_context(f: F) -> F:
188
+ """
189
+ Decorator to pass the foundation Context to a command.
190
+
191
+ Creates or retrieves a Context from Click's context object
192
+ and passes it as the first argument to the decorated function.
193
+ """
194
+
195
+ @functools.wraps(f)
196
+ @click.pass_context
197
+ def wrapper(ctx: click.Context, *args, **kwargs):
198
+ # Get or create foundation context
199
+ if not hasattr(ctx, "obj") or ctx.obj is None:
200
+ ctx.obj = Context()
201
+ elif not isinstance(ctx.obj, Context):
202
+ # If obj exists but isn't a Context, wrap it
203
+ if isinstance(ctx.obj, dict):
204
+ ctx.obj = Context.from_dict(ctx.obj)
205
+ else:
206
+ # Store existing obj and create new Context
207
+ old_obj = ctx.obj
208
+ ctx.obj = Context()
209
+ ctx.obj._cli_data = old_obj
210
+
211
+ # Update context from command options
212
+ if kwargs.get("log_level"):
213
+ ctx.obj.log_level = kwargs["log_level"]
214
+ if kwargs.get("log_file"):
215
+ # Ensure log_file is a Path object, as expected by Context
216
+ ctx.obj.log_file = Path(kwargs["log_file"])
217
+ if "log_format" in kwargs and kwargs["log_format"] is not None:
218
+ ctx.obj.log_format = kwargs["log_format"]
219
+ if "json_output" in kwargs and kwargs["json_output"] is not None:
220
+ ctx.obj.json_output = kwargs["json_output"]
221
+ if "no_color" in kwargs and kwargs["no_color"] is not None:
222
+ ctx.obj.no_color = kwargs["no_color"]
223
+ if "no_emoji" in kwargs and kwargs["no_emoji"] is not None:
224
+ ctx.obj.no_emoji = kwargs["no_emoji"]
225
+ if kwargs.get("profile"):
226
+ ctx.obj.profile = kwargs["profile"]
227
+ if kwargs.get("config"):
228
+ ctx.obj.load_config(kwargs["config"])
229
+
230
+ # Remove these from kwargs to avoid duplicate arguments
231
+ for key in [
232
+ "log_level",
233
+ "log_file",
234
+ "log_format",
235
+ "json_output",
236
+ "no_color",
237
+ "no_emoji",
238
+ "profile",
239
+ "config",
240
+ ]:
241
+ kwargs.pop(key, None)
242
+
243
+ return f(ctx.obj, *args, **kwargs)
244
+
245
+ return wrapper
246
+
247
+
248
+ def version_option(version: str | None = None, prog_name: str | None = None):
249
+ """
250
+ Add a --version option to display version information.
251
+
252
+ Args:
253
+ version: Version string to display
254
+ prog_name: Program name to display
255
+ """
256
+
257
+ def decorator(f: F) -> F:
258
+ return click.version_option(
259
+ version=version, prog_name=prog_name, message="%(prog)s version %(version)s"
260
+ )(f)
261
+
262
+ return decorator