golf-mcp 0.1.20__py3-none-any.whl → 0.2.1__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.
Potentially problematic release.
This version of golf-mcp might be problematic. Click here for more details.
- golf/__init__.py +9 -1
- golf/_endpoints.py +6 -0
- golf/_endpoints_fallback.py +10 -0
- golf/auth/__init__.py +235 -83
- golf/auth/api_key.py +6 -14
- golf/auth/factory.py +358 -0
- golf/auth/helpers.py +12 -42
- golf/auth/providers.py +446 -0
- golf/auth/registry.py +256 -0
- golf/cli/branding.py +192 -0
- golf/cli/main.py +28 -69
- golf/commands/__init__.py +2 -0
- golf/commands/build.py +4 -7
- golf/commands/init.py +30 -53
- golf/commands/run.py +50 -20
- golf/core/builder.py +355 -414
- golf/core/builder_auth.py +63 -144
- golf/core/builder_telemetry.py +26 -3
- golf/core/config.py +38 -59
- golf/core/parser.py +132 -139
- golf/core/platform.py +12 -10
- golf/core/telemetry.py +11 -19
- golf/core/transformer.py +38 -15
- golf/examples/__pycache__/__init__.cpython-311.pyc +0 -0
- golf/examples/basic/.coverage +0 -0
- golf/examples/basic/.env.example +8 -4
- golf/examples/basic/README.md +117 -45
- golf/examples/basic/__pycache__/auth.cpython-311.pyc +0 -0
- golf/examples/basic/auth.py +76 -0
- golf/examples/basic/golf.json +2 -5
- golf/examples/basic/htmlcov/.gitignore +2 -0
- golf/examples/basic/htmlcov/class_index.html +547 -0
- golf/examples/basic/htmlcov/coverage_html_cb_6fb7b396.js +733 -0
- golf/examples/basic/htmlcov/favicon_32_cb_58284776.png +0 -0
- golf/examples/basic/htmlcov/function_index.html +2091 -0
- golf/examples/basic/htmlcov/index.html +349 -0
- golf/examples/basic/htmlcov/keybd_closed_cb_ce680311.png +0 -0
- golf/examples/basic/htmlcov/status.json +1 -0
- golf/examples/basic/htmlcov/style_cb_8e611ae1.css +337 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496___init___py.html +323 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_api_key_py.html +170 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_factory_py.html +430 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_helpers_py.html +288 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_providers_py.html +493 -0
- golf/examples/basic/htmlcov/z_1c9a91c0e91c8496_registry_py.html +353 -0
- golf/examples/basic/htmlcov/z_3ec3b3f490dc0950___init___py.html +120 -0
- golf/examples/basic/htmlcov/z_3ec3b3f490dc0950_instrumentation_py.html +1535 -0
- golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db___init___py.html +98 -0
- golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_branding_py.html +289 -0
- golf/examples/basic/htmlcov/z_4b8b9dd4ccccc5db_main_py.html +476 -0
- golf/examples/basic/htmlcov/z_5a6c4e6bcc86fb2f___init___py.html +97 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d___init___py.html +102 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d_build_py.html +178 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d_init_py.html +387 -0
- golf/examples/basic/htmlcov/z_6cadab9ec0df475d_run_py.html +222 -0
- golf/examples/basic/htmlcov/z_6fcdee0582ba84e4___init___py.html +106 -0
- golf/examples/basic/htmlcov/z_6fcdee0582ba84e4__endpoints_fallback_py.html +107 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217___init___py.html +98 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_auth_py.html +306 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_metrics_py.html +329 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_py.html +1471 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_builder_telemetry_py.html +186 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_config_py.html +315 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_parser_py.html +1149 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_platform_py.html +279 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_telemetry_py.html +589 -0
- golf/examples/basic/htmlcov/z_7ba499ed22986217_transformer_py.html +286 -0
- golf/examples/basic/htmlcov/z_7d7da37693a43688___init___py.html +107 -0
- golf/examples/basic/htmlcov/z_7d7da37693a43688_collector_py.html +417 -0
- golf/examples/basic/htmlcov/z_7d7da37693a43688_registry_py.html +109 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e___init___py.html +109 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e_context_py.html +150 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e_elicitation_py.html +267 -0
- golf/examples/basic/htmlcov/z_abe733142b40ad4e_sampling_py.html +318 -0
- golf/examples/basic/prompts/__pycache__/welcome.cpython-311.pyc +0 -0
- golf/examples/basic/prompts/welcome.py +3 -5
- golf/examples/basic/resources/__pycache__/current_time.cpython-311.pyc +0 -0
- golf/examples/basic/resources/__pycache__/info.cpython-311.pyc +0 -0
- golf/examples/basic/resources/current_time.py +5 -13
- golf/examples/basic/resources/weather/__pycache__/common.cpython-311.pyc +0 -0
- golf/examples/basic/resources/weather/__pycache__/current.cpython-311.pyc +0 -0
- golf/examples/basic/resources/weather/__pycache__/forecast.cpython-311.pyc +0 -0
- golf/examples/basic/resources/weather/city.py +46 -0
- golf/examples/basic/resources/weather/common.py +4 -11
- golf/examples/basic/resources/weather/current.py +5 -5
- golf/examples/basic/resources/weather/forecast.py +5 -5
- golf/examples/basic/tools/__pycache__/calculator.cpython-311.pyc +0 -0
- golf/examples/basic/tools/calculator.py +94 -0
- golf/examples/basic/tools/say/__pycache__/hello.cpython-311.pyc +0 -0
- golf/examples/basic/tools/say/hello.py +65 -0
- golf/metrics/collector.py +100 -19
- golf/telemetry/__init__.py +4 -0
- golf/telemetry/instrumentation.py +484 -178
- golf/utilities/__init__.py +12 -0
- golf/utilities/context.py +53 -0
- golf/utilities/elicitation.py +170 -0
- golf/utilities/sampling.py +221 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/METADATA +51 -104
- golf_mcp-0.2.1.dist-info/RECORD +110 -0
- golf/auth/oauth.py +0 -861
- golf/auth/provider.py +0 -115
- golf/examples/api_key/.env +0 -2
- golf/examples/api_key/.env.example +0 -1
- golf/examples/api_key/README.md +0 -84
- golf/examples/api_key/golf.json +0 -8
- golf/examples/api_key/pre_build.py +0 -11
- golf/examples/api_key/tools/issues/create.py +0 -93
- golf/examples/api_key/tools/issues/list.py +0 -92
- golf/examples/api_key/tools/repos/list.py +0 -111
- golf/examples/api_key/tools/search/code.py +0 -106
- golf/examples/api_key/tools/users/get.py +0 -82
- golf/examples/basic/.env +0 -5
- golf/examples/basic/pre_build.py +0 -28
- golf/examples/basic/tools/github_user.py +0 -65
- golf/examples/basic/tools/hello.py +0 -34
- golf/examples/basic/tools/payments/charge.py +0 -70
- golf/examples/basic/tools/payments/common.py +0 -36
- golf/examples/basic/tools/payments/refund.py +0 -61
- golf_mcp-0.1.20.dist-info/RECORD +0 -60
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/WHEEL +0 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/entry_points.txt +0 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {golf_mcp-0.1.20.dist-info → golf_mcp-0.2.1.dist-info}/top_level.txt +0 -0
golf/core/parser.py
CHANGED
|
@@ -58,9 +58,7 @@ class AstParser:
|
|
|
58
58
|
|
|
59
59
|
for file_path in directory.glob("**/*.py"):
|
|
60
60
|
# Skip __pycache__ and other hidden directories
|
|
61
|
-
if "__pycache__" in file_path.parts or any(
|
|
62
|
-
part.startswith(".") for part in file_path.parts
|
|
63
|
-
):
|
|
61
|
+
if "__pycache__" in file_path.parts or any(part.startswith(".") for part in file_path.parts):
|
|
64
62
|
continue
|
|
65
63
|
|
|
66
64
|
try:
|
|
@@ -68,9 +66,7 @@ class AstParser:
|
|
|
68
66
|
components.extend(file_components)
|
|
69
67
|
except Exception as e:
|
|
70
68
|
relative_path = file_path.relative_to(self.project_root)
|
|
71
|
-
console.print(
|
|
72
|
-
f"[bold red]Error parsing {relative_path}:[/bold red] {e}"
|
|
73
|
-
)
|
|
69
|
+
console.print(f"[bold red]Error parsing {relative_path}:[/bold red] {e}")
|
|
74
70
|
|
|
75
71
|
return components
|
|
76
72
|
|
|
@@ -123,10 +119,9 @@ class AstParser:
|
|
|
123
119
|
for node in tree.body:
|
|
124
120
|
if isinstance(node, ast.Assign):
|
|
125
121
|
for target in node.targets:
|
|
126
|
-
if isinstance(target, ast.Name) and target.id == "export":
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
break
|
|
122
|
+
if isinstance(target, ast.Name) and target.id == "export" and isinstance(node.value, ast.Name):
|
|
123
|
+
export_target = node.value.id
|
|
124
|
+
break
|
|
130
125
|
|
|
131
126
|
# Find all top-level functions
|
|
132
127
|
functions = []
|
|
@@ -145,9 +140,7 @@ class AstParser:
|
|
|
145
140
|
|
|
146
141
|
# If we have an export but didn't find the target function, warn
|
|
147
142
|
if export_target and not entry_function:
|
|
148
|
-
console.print(
|
|
149
|
-
f"[yellow]Warning: Export target '{export_target}' not found in {file_path}[/yellow]"
|
|
150
|
-
)
|
|
143
|
+
console.print(f"[yellow]Warning: Export target '{export_target}' not found in {file_path}[/yellow]")
|
|
151
144
|
|
|
152
145
|
# Use the export target function if found, otherwise fall back to run
|
|
153
146
|
entry_function = entry_function or run_function
|
|
@@ -163,8 +156,7 @@ class AstParser:
|
|
|
163
156
|
file_path=file_path,
|
|
164
157
|
module_path=file_path.relative_to(self.project_root).as_posix(),
|
|
165
158
|
docstring=module_docstring,
|
|
166
|
-
entry_function=export_target
|
|
167
|
-
or "run", # Store the name of the entry function
|
|
159
|
+
entry_function=export_target or "run", # Store the name of the entry function
|
|
168
160
|
)
|
|
169
161
|
|
|
170
162
|
# Process the entry function
|
|
@@ -183,9 +175,7 @@ class AstParser:
|
|
|
183
175
|
|
|
184
176
|
# Set parent module if it's in a nested structure
|
|
185
177
|
if len(rel_path.parts) > 2: # More than just "tools/file.py"
|
|
186
|
-
parent_parts = rel_path.parts[
|
|
187
|
-
1:-1
|
|
188
|
-
] # Skip the root category and the file itself
|
|
178
|
+
parent_parts = rel_path.parts[1:-1] # Skip the root category and the file itself
|
|
189
179
|
if parent_parts:
|
|
190
180
|
component.parent_module = ".".join(parent_parts)
|
|
191
181
|
|
|
@@ -201,9 +191,7 @@ class AstParser:
|
|
|
201
191
|
"""Process the entry function to extract parameters and return type."""
|
|
202
192
|
# Check for return annotation - STRICT requirement
|
|
203
193
|
if func_node.returns is None:
|
|
204
|
-
raise ValueError(
|
|
205
|
-
f"Missing return annotation for {func_node.name} function in {file_path}"
|
|
206
|
-
)
|
|
194
|
+
raise ValueError(f"Missing return annotation for {func_node.name} function in {file_path}")
|
|
207
195
|
|
|
208
196
|
# Extract parameter names for basic info
|
|
209
197
|
parameters = []
|
|
@@ -219,15 +207,12 @@ class AstParser:
|
|
|
219
207
|
try:
|
|
220
208
|
self._extract_schemas_at_runtime(component, file_path)
|
|
221
209
|
except Exception as e:
|
|
222
|
-
console.print(
|
|
223
|
-
f"[yellow]Warning: Could not extract schemas from {file_path}: {e}[/yellow]"
|
|
224
|
-
)
|
|
210
|
+
console.print(f"[yellow]Warning: Could not extract schemas from {file_path}: {e}[/yellow]")
|
|
225
211
|
# Continue without schemas - better than failing the build
|
|
226
212
|
|
|
227
|
-
def _extract_schemas_at_runtime(
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
"""Extract input/output schemas by importing and inspecting the actual function."""
|
|
213
|
+
def _extract_schemas_at_runtime(self, component: ParsedComponent, file_path: Path) -> None:
|
|
214
|
+
"""Extract input/output schemas by importing and inspecting the
|
|
215
|
+
actual function."""
|
|
231
216
|
import importlib.util
|
|
232
217
|
import sys
|
|
233
218
|
|
|
@@ -269,7 +254,7 @@ class AstParser:
|
|
|
269
254
|
if cleanup_path and project_root_str in sys.path:
|
|
270
255
|
sys.path.remove(project_root_str)
|
|
271
256
|
|
|
272
|
-
def _extract_input_schema(self, func) -> dict[str, Any] | None:
|
|
257
|
+
def _extract_input_schema(self, func: Any) -> dict[str, Any] | None:
|
|
273
258
|
"""Extract input schema from function signature using runtime inspection."""
|
|
274
259
|
import inspect
|
|
275
260
|
from typing import get_type_hints
|
|
@@ -293,9 +278,7 @@ class AstParser:
|
|
|
293
278
|
type_hint = type_hints[param_name]
|
|
294
279
|
|
|
295
280
|
# Extract schema for this parameter
|
|
296
|
-
param_schema = self._extract_param_schema_from_hint(
|
|
297
|
-
type_hint, param_name
|
|
298
|
-
)
|
|
281
|
+
param_schema = self._extract_param_schema_from_hint(type_hint, param_name)
|
|
299
282
|
if param_schema:
|
|
300
283
|
# Clean the schema to remove problematic objects
|
|
301
284
|
cleaned_schema = self._clean_schema(param_schema)
|
|
@@ -314,13 +297,11 @@ class AstParser:
|
|
|
314
297
|
}
|
|
315
298
|
|
|
316
299
|
except Exception as e:
|
|
317
|
-
console.print(
|
|
318
|
-
f"[yellow]Warning: Could not extract input schema: {e}[/yellow]"
|
|
319
|
-
)
|
|
300
|
+
console.print(f"[yellow]Warning: Could not extract input schema: {e}[/yellow]")
|
|
320
301
|
|
|
321
302
|
return None
|
|
322
303
|
|
|
323
|
-
def _extract_output_schema(self, func) -> dict[str, Any] | None:
|
|
304
|
+
def _extract_output_schema(self, func: Any) -> dict[str, Any] | None:
|
|
324
305
|
"""Extract output schema from return type annotation."""
|
|
325
306
|
from typing import get_type_hints
|
|
326
307
|
|
|
@@ -339,13 +320,11 @@ class AstParser:
|
|
|
339
320
|
return self._type_to_schema(return_type)
|
|
340
321
|
|
|
341
322
|
except Exception as e:
|
|
342
|
-
console.print(
|
|
343
|
-
f"[yellow]Warning: Could not extract output schema: {e}[/yellow]"
|
|
344
|
-
)
|
|
323
|
+
console.print(f"[yellow]Warning: Could not extract output schema: {e}[/yellow]")
|
|
345
324
|
|
|
346
325
|
return None
|
|
347
326
|
|
|
348
|
-
def _extract_pydantic_model_schema(self, model_class) -> dict[str, Any]:
|
|
327
|
+
def _extract_pydantic_model_schema(self, model_class: Any) -> dict[str, Any]:
|
|
349
328
|
"""Extract schema from Pydantic model by inspecting fields directly."""
|
|
350
329
|
try:
|
|
351
330
|
schema = {"type": "object", "properties": {}, "required": []}
|
|
@@ -353,29 +332,19 @@ class AstParser:
|
|
|
353
332
|
if hasattr(model_class, "model_fields"):
|
|
354
333
|
for field_name, field_info in model_class.model_fields.items():
|
|
355
334
|
# Extract field type
|
|
356
|
-
field_type = (
|
|
357
|
-
field_info.annotation
|
|
358
|
-
if hasattr(field_info, "annotation")
|
|
359
|
-
else None
|
|
360
|
-
)
|
|
335
|
+
field_type = field_info.annotation if hasattr(field_info, "annotation") else None
|
|
361
336
|
if field_type:
|
|
362
337
|
field_schema = self._type_to_schema(field_type)
|
|
363
338
|
|
|
364
339
|
# Add description if available
|
|
365
|
-
if (
|
|
366
|
-
hasattr(field_info, "description")
|
|
367
|
-
and field_info.description
|
|
368
|
-
):
|
|
340
|
+
if hasattr(field_info, "description") and field_info.description:
|
|
369
341
|
field_schema["description"] = field_info.description
|
|
370
342
|
|
|
371
343
|
# Add title
|
|
372
344
|
field_schema["title"] = field_name.replace("_", " ").title()
|
|
373
345
|
|
|
374
346
|
# Add default if available
|
|
375
|
-
if (
|
|
376
|
-
hasattr(field_info, "default")
|
|
377
|
-
and field_info.default is not None
|
|
378
|
-
):
|
|
347
|
+
if hasattr(field_info, "default") and field_info.default is not None:
|
|
379
348
|
try:
|
|
380
349
|
# Only add if it's JSON serializable
|
|
381
350
|
import json
|
|
@@ -388,31 +357,23 @@ class AstParser:
|
|
|
388
357
|
schema["properties"][field_name] = field_schema
|
|
389
358
|
|
|
390
359
|
# Check if required
|
|
391
|
-
if (
|
|
392
|
-
hasattr(field_info, "is_required")
|
|
393
|
-
and field_info.is_required()
|
|
394
|
-
):
|
|
360
|
+
if hasattr(field_info, "is_required") and field_info.is_required():
|
|
395
361
|
schema["required"].append(field_name)
|
|
396
|
-
elif (
|
|
397
|
-
not hasattr(field_info, "default")
|
|
398
|
-
or field_info.default is None
|
|
399
|
-
):
|
|
362
|
+
elif not hasattr(field_info, "default") or field_info.default is None:
|
|
400
363
|
# Assume required if no default
|
|
401
364
|
schema["required"].append(field_name)
|
|
402
365
|
|
|
403
366
|
return schema
|
|
404
367
|
|
|
405
368
|
except Exception as e:
|
|
406
|
-
console.print(
|
|
407
|
-
f"[yellow]Warning: Could not extract Pydantic model schema: {e}[/yellow]"
|
|
408
|
-
)
|
|
369
|
+
console.print(f"[yellow]Warning: Could not extract Pydantic model schema: {e}[/yellow]")
|
|
409
370
|
return {"type": "object"}
|
|
410
371
|
|
|
411
|
-
def _clean_schema(self, schema) -> dict[str, Any]:
|
|
372
|
+
def _clean_schema(self, schema: Any) -> dict[str, Any]:
|
|
412
373
|
"""Clean up a schema to remove non-JSON-serializable objects."""
|
|
413
374
|
import json
|
|
414
375
|
|
|
415
|
-
def clean_object(obj):
|
|
376
|
+
def clean_object(obj: Any) -> Any:
|
|
416
377
|
if obj is None:
|
|
417
378
|
return None
|
|
418
379
|
elif isinstance(obj, (str, int, float, bool)):
|
|
@@ -455,9 +416,7 @@ class AstParser:
|
|
|
455
416
|
cleaned = clean_object(schema)
|
|
456
417
|
return cleaned if cleaned else {"type": "object"}
|
|
457
418
|
|
|
458
|
-
def _extract_param_schema_from_hint(
|
|
459
|
-
self, type_hint, param_name: str
|
|
460
|
-
) -> dict[str, Any] | None:
|
|
419
|
+
def _extract_param_schema_from_hint(self, type_hint: Any, param_name: str) -> dict[str, Any] | None:
|
|
461
420
|
"""Extract parameter schema from type hint (including Annotated types)."""
|
|
462
421
|
from typing import get_args, get_origin
|
|
463
422
|
|
|
@@ -467,11 +426,7 @@ class AstParser:
|
|
|
467
426
|
args = get_args(type_hint)
|
|
468
427
|
|
|
469
428
|
# Check for Annotated[Type, Field(...)]
|
|
470
|
-
if (
|
|
471
|
-
hasattr(origin, "__name__")
|
|
472
|
-
and origin.__name__ == "Annotated"
|
|
473
|
-
and len(args) >= 2
|
|
474
|
-
):
|
|
429
|
+
if hasattr(origin, "__name__") and origin.__name__ == "Annotated" and len(args) >= 2:
|
|
475
430
|
base_type = args[0]
|
|
476
431
|
metadata = args[1:]
|
|
477
432
|
|
|
@@ -493,7 +448,7 @@ class AstParser:
|
|
|
493
448
|
# For non-Annotated types, just convert the type
|
|
494
449
|
return self._type_to_schema(type_hint)
|
|
495
450
|
|
|
496
|
-
def _type_to_schema(self, type_hint) -> dict[str, Any]:
|
|
451
|
+
def _type_to_schema(self, type_hint: object) -> dict[str, Any]:
|
|
497
452
|
"""Convert a Python type to JSON schema."""
|
|
498
453
|
from typing import get_args, get_origin
|
|
499
454
|
import types
|
|
@@ -577,9 +532,7 @@ class AstParser:
|
|
|
577
532
|
# Check if it inherits from BaseModel
|
|
578
533
|
for base in input_class.bases:
|
|
579
534
|
if isinstance(base, ast.Name) and base.id == "BaseModel":
|
|
580
|
-
component.input_schema = self._extract_pydantic_schema_from_ast(
|
|
581
|
-
input_class
|
|
582
|
-
)
|
|
535
|
+
component.input_schema = self._extract_pydantic_schema_from_ast(input_class)
|
|
583
536
|
break
|
|
584
537
|
|
|
585
538
|
# Process Output class if found
|
|
@@ -587,9 +540,7 @@ class AstParser:
|
|
|
587
540
|
# Check if it inherits from BaseModel
|
|
588
541
|
for base in output_class.bases:
|
|
589
542
|
if isinstance(base, ast.Name) and base.id == "BaseModel":
|
|
590
|
-
component.output_schema = self._extract_pydantic_schema_from_ast(
|
|
591
|
-
output_class
|
|
592
|
-
)
|
|
543
|
+
component.output_schema = self._extract_pydantic_schema_from_ast(output_class)
|
|
593
544
|
break
|
|
594
545
|
|
|
595
546
|
# Store annotations if found
|
|
@@ -602,24 +553,25 @@ class AstParser:
|
|
|
602
553
|
for node in tree.body:
|
|
603
554
|
if isinstance(node, ast.Assign):
|
|
604
555
|
for target in node.targets:
|
|
605
|
-
if
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
556
|
+
if (
|
|
557
|
+
isinstance(target, ast.Name)
|
|
558
|
+
and target.id == "resource_uri"
|
|
559
|
+
and isinstance(node.value, ast.Constant)
|
|
560
|
+
):
|
|
561
|
+
uri_template = node.value.value
|
|
562
|
+
component.uri_template = uri_template
|
|
609
563
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
564
|
+
# Extract URI parameters (parts in {})
|
|
565
|
+
uri_params = re.findall(r"{([^}]+)}", uri_template)
|
|
566
|
+
if uri_params:
|
|
567
|
+
component.parameters = uri_params
|
|
568
|
+
break
|
|
615
569
|
|
|
616
570
|
def _process_prompt(self, component: ParsedComponent, tree: ast.Module) -> None:
|
|
617
571
|
"""Process a prompt component (no special processing needed)."""
|
|
618
572
|
pass
|
|
619
573
|
|
|
620
|
-
def _derive_component_name(
|
|
621
|
-
self, file_path: Path, component_type: ComponentType
|
|
622
|
-
) -> str:
|
|
574
|
+
def _derive_component_name(self, file_path: Path, component_type: ComponentType) -> str:
|
|
623
575
|
"""Derive a component name from its file path according to the spec.
|
|
624
576
|
|
|
625
577
|
Following the spec: <filename> + ("_" + "_".join(PathRev) if PathRev else "")
|
|
@@ -652,9 +604,7 @@ class AstParser:
|
|
|
652
604
|
else:
|
|
653
605
|
return filename
|
|
654
606
|
|
|
655
|
-
def _extract_pydantic_schema_from_ast(
|
|
656
|
-
self, class_node: ast.ClassDef
|
|
657
|
-
) -> dict[str, Any]:
|
|
607
|
+
def _extract_pydantic_schema_from_ast(self, class_node: ast.ClassDef) -> dict[str, Any]:
|
|
658
608
|
"""Extract a JSON schema from an AST class definition.
|
|
659
609
|
|
|
660
610
|
This is a simplified version that extracts basic field information.
|
|
@@ -711,10 +661,7 @@ class AstParser:
|
|
|
711
661
|
):
|
|
712
662
|
# Field object - extract its parameters
|
|
713
663
|
for keyword in node.value.keywords:
|
|
714
|
-
if
|
|
715
|
-
keyword.arg == "default"
|
|
716
|
-
or keyword.arg == "default_factory"
|
|
717
|
-
):
|
|
664
|
+
if keyword.arg == "default" or keyword.arg == "default_factory":
|
|
718
665
|
if isinstance(keyword.value, ast.Constant):
|
|
719
666
|
prop["default"] = keyword.value.value
|
|
720
667
|
elif keyword.arg == "description":
|
|
@@ -724,14 +671,11 @@ class AstParser:
|
|
|
724
671
|
if isinstance(keyword.value, ast.Constant):
|
|
725
672
|
prop["title"] = keyword.value.value
|
|
726
673
|
|
|
727
|
-
# Check for position default argument
|
|
674
|
+
# Check for position default argument
|
|
675
|
+
# (Field(..., "description"))
|
|
728
676
|
if node.value.args:
|
|
729
677
|
for i, arg in enumerate(node.value.args):
|
|
730
|
-
if (
|
|
731
|
-
i == 0
|
|
732
|
-
and isinstance(arg, ast.Constant)
|
|
733
|
-
and arg.value != Ellipsis
|
|
734
|
-
):
|
|
678
|
+
if i == 0 and isinstance(arg, ast.Constant) and arg.value != Ellipsis:
|
|
735
679
|
prop["default"] = arg.value
|
|
736
680
|
elif i == 1 and isinstance(arg, ast.Constant):
|
|
737
681
|
prop["description"] = arg.value
|
|
@@ -749,20 +693,16 @@ class AstParser:
|
|
|
749
693
|
and isinstance(node.value.func, ast.Name)
|
|
750
694
|
and node.value.func.id == "Field"
|
|
751
695
|
):
|
|
752
|
-
# Field has default if it doesn't use ... or if it has a
|
|
696
|
+
# Field has default if it doesn't use ... or if it has a
|
|
697
|
+
# default keyword
|
|
753
698
|
has_ellipsis = False
|
|
754
699
|
has_default = False
|
|
755
700
|
|
|
756
|
-
if node.value.args and isinstance(
|
|
757
|
-
node.value.args[0], ast.Constant
|
|
758
|
-
):
|
|
701
|
+
if node.value.args and isinstance(node.value.args[0], ast.Constant):
|
|
759
702
|
has_ellipsis = node.value.args[0].value is Ellipsis
|
|
760
703
|
|
|
761
704
|
for keyword in node.value.keywords:
|
|
762
|
-
if
|
|
763
|
-
keyword.arg == "default"
|
|
764
|
-
or keyword.arg == "default_factory"
|
|
765
|
-
):
|
|
705
|
+
if keyword.arg == "default" or keyword.arg == "default_factory":
|
|
766
706
|
has_default = True
|
|
767
707
|
|
|
768
708
|
is_required = has_ellipsis and not has_default
|
|
@@ -863,16 +803,12 @@ class AstParser:
|
|
|
863
803
|
result[key_str] = value.s
|
|
864
804
|
elif isinstance(value, ast.Num): # For older Python versions
|
|
865
805
|
result[key_str] = value.n
|
|
866
|
-
elif isinstance(
|
|
867
|
-
value, ast.NameConstant
|
|
868
|
-
): # For older Python versions (True/False/None)
|
|
806
|
+
elif isinstance(value, ast.NameConstant): # For older Python versions (True/False/None)
|
|
869
807
|
result[key_str] = value.value
|
|
870
808
|
elif isinstance(value, ast.Name):
|
|
871
809
|
# Handle True/False/None as names
|
|
872
810
|
if value.id in ("True", "False", "None"):
|
|
873
|
-
result[key_str] = {"True": True, "False": False, "None": None}[
|
|
874
|
-
value.id
|
|
875
|
-
]
|
|
811
|
+
result[key_str] = {"True": True, "False": False, "None": None}[value.id]
|
|
876
812
|
# We could add more complex value handling here if needed
|
|
877
813
|
|
|
878
814
|
return result
|
|
@@ -937,9 +873,7 @@ class AstParser:
|
|
|
937
873
|
type_str = ast.unparse(subscript.slice)
|
|
938
874
|
return {"type": self._type_hint_to_json_type(type_str)}
|
|
939
875
|
|
|
940
|
-
def _is_parameter_required(
|
|
941
|
-
self, position: int, defaults: list, total_args: int
|
|
942
|
-
) -> bool:
|
|
876
|
+
def _is_parameter_required(self, position: int, defaults: list, total_args: int) -> bool:
|
|
943
877
|
"""Check if a function parameter is required (has no default value)."""
|
|
944
878
|
if position >= total_args or position < 0:
|
|
945
879
|
return True # Default to required if position is out of range
|
|
@@ -953,12 +887,11 @@ class AstParser:
|
|
|
953
887
|
args_with_defaults = len(defaults)
|
|
954
888
|
first_default_position = total_args - args_with_defaults
|
|
955
889
|
|
|
956
|
-
# If this parameter's position is before the first default position,
|
|
890
|
+
# If this parameter's position is before the first default position,
|
|
891
|
+
# it's required
|
|
957
892
|
return position < first_default_position
|
|
958
893
|
|
|
959
|
-
def _extract_return_type_schema(
|
|
960
|
-
self, return_annotation: ast.AST, tree: ast.Module
|
|
961
|
-
) -> dict[str, Any] | None:
|
|
894
|
+
def _extract_return_type_schema(self, return_annotation: ast.AST, tree: ast.Module) -> dict[str, Any] | None:
|
|
962
895
|
"""Extract schema from function return type annotation."""
|
|
963
896
|
if isinstance(return_annotation, ast.Name):
|
|
964
897
|
# Simple type like str, int, or a class name
|
|
@@ -977,9 +910,7 @@ class AstParser:
|
|
|
977
910
|
type_str = ast.unparse(return_annotation)
|
|
978
911
|
return {"type": self._type_hint_to_json_type(type_str)}
|
|
979
912
|
|
|
980
|
-
def _find_class_schema(
|
|
981
|
-
self, class_name: str, tree: ast.Module
|
|
982
|
-
) -> dict[str, Any] | None:
|
|
913
|
+
def _find_class_schema(self, class_name: str, tree: ast.Module) -> dict[str, Any] | None:
|
|
983
914
|
"""Find a class definition in the module and extract its schema."""
|
|
984
915
|
for node in tree.body:
|
|
985
916
|
if isinstance(node, ast.ClassDef) and node.name == class_name:
|
|
@@ -1010,18 +941,14 @@ def parse_project(project_path: Path) -> dict[ComponentType, list[ParsedComponen
|
|
|
1010
941
|
dir_path = project_path / dir_name
|
|
1011
942
|
if dir_path.exists() and dir_path.is_dir():
|
|
1012
943
|
dir_components = parser.parse_directory(dir_path)
|
|
1013
|
-
components[comp_type].extend(
|
|
1014
|
-
[c for c in dir_components if c.type == comp_type]
|
|
1015
|
-
)
|
|
944
|
+
components[comp_type].extend([c for c in dir_components if c.type == comp_type])
|
|
1016
945
|
|
|
1017
946
|
# Check for ID collisions
|
|
1018
947
|
all_ids = []
|
|
1019
948
|
for comp_type, comps in components.items():
|
|
1020
949
|
for comp in comps:
|
|
1021
950
|
if comp.name in all_ids:
|
|
1022
|
-
raise ValueError(
|
|
1023
|
-
f"ID collision detected: {comp.name} is used by multiple components"
|
|
1024
|
-
)
|
|
951
|
+
raise ValueError(f"ID collision detected: {comp.name} is used by multiple components")
|
|
1025
952
|
all_ids.append(comp.name)
|
|
1026
953
|
|
|
1027
954
|
return components
|
|
@@ -1047,9 +974,7 @@ def parse_common_files(project_path: Path) -> dict[str, Path]:
|
|
|
1047
974
|
# Find all common.py files (recursively)
|
|
1048
975
|
for common_file in base_dir.glob("**/common.py"):
|
|
1049
976
|
# Skip files in __pycache__ or other hidden directories
|
|
1050
|
-
if "__pycache__" in common_file.parts or any(
|
|
1051
|
-
part.startswith(".") for part in common_file.parts
|
|
1052
|
-
):
|
|
977
|
+
if "__pycache__" in common_file.parts or any(part.startswith(".") for part in common_file.parts):
|
|
1053
978
|
continue
|
|
1054
979
|
|
|
1055
980
|
# Get the parent directory as the module path
|
|
@@ -1057,3 +982,71 @@ def parse_common_files(project_path: Path) -> dict[str, Path]:
|
|
|
1057
982
|
common_files[module_path] = common_file
|
|
1058
983
|
|
|
1059
984
|
return common_files
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
def _is_golf_component_file(file_path: Path) -> bool:
|
|
988
|
+
"""Check if a Python file is a Golf component (has export or resource_uri).
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
file_path: Path to the Python file to check
|
|
992
|
+
|
|
993
|
+
Returns:
|
|
994
|
+
True if the file appears to be a Golf component, False otherwise
|
|
995
|
+
"""
|
|
996
|
+
try:
|
|
997
|
+
with open(file_path, encoding="utf-8") as f:
|
|
998
|
+
content = f.read()
|
|
999
|
+
|
|
1000
|
+
# Parse the file to check for Golf component patterns
|
|
1001
|
+
tree = ast.parse(content)
|
|
1002
|
+
|
|
1003
|
+
# Look for 'export' or 'resource_uri' variable assignments
|
|
1004
|
+
for node in ast.walk(tree):
|
|
1005
|
+
if isinstance(node, ast.Assign):
|
|
1006
|
+
for target in node.targets:
|
|
1007
|
+
if isinstance(target, ast.Name):
|
|
1008
|
+
if target.id in ("export", "resource_uri"):
|
|
1009
|
+
return True
|
|
1010
|
+
|
|
1011
|
+
return False
|
|
1012
|
+
|
|
1013
|
+
except (SyntaxError, OSError, UnicodeDecodeError):
|
|
1014
|
+
# If we can't parse the file, assume it's not a component
|
|
1015
|
+
return False
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def parse_shared_files(project_path: Path) -> dict[str, Path]:
|
|
1019
|
+
"""Find all shared Python files in the project (non-component .py files).
|
|
1020
|
+
|
|
1021
|
+
Args:
|
|
1022
|
+
project_path: Path to the project root
|
|
1023
|
+
|
|
1024
|
+
Returns:
|
|
1025
|
+
Dictionary mapping module paths to shared file paths
|
|
1026
|
+
"""
|
|
1027
|
+
shared_files = {}
|
|
1028
|
+
|
|
1029
|
+
# Search for all .py files in tools, resources, and prompts directories
|
|
1030
|
+
for dir_name in ["tools", "resources", "prompts"]:
|
|
1031
|
+
base_dir = project_path / dir_name
|
|
1032
|
+
if not base_dir.exists() or not base_dir.is_dir():
|
|
1033
|
+
continue
|
|
1034
|
+
|
|
1035
|
+
# Find all .py files (recursively)
|
|
1036
|
+
for py_file in base_dir.glob("**/*.py"):
|
|
1037
|
+
# Skip files in __pycache__ or other hidden directories
|
|
1038
|
+
if "__pycache__" in py_file.parts or any(part.startswith(".") for part in py_file.parts):
|
|
1039
|
+
continue
|
|
1040
|
+
|
|
1041
|
+
# Skip files that are Golf components (have export or resource_uri)
|
|
1042
|
+
if _is_golf_component_file(py_file):
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
# Calculate the module path for this shared file
|
|
1046
|
+
# For example: tools/weather/helpers.py -> tools/weather/helpers
|
|
1047
|
+
relative_path = py_file.relative_to(project_path)
|
|
1048
|
+
module_path = str(relative_path.with_suffix("")) # Remove .py extension
|
|
1049
|
+
|
|
1050
|
+
shared_files[module_path] = py_file
|
|
1051
|
+
|
|
1052
|
+
return shared_files
|
golf/core/platform.py
CHANGED
|
@@ -12,6 +12,14 @@ from golf import __version__
|
|
|
12
12
|
from golf.core.config import Settings
|
|
13
13
|
from golf.core.parser import ComponentType, ParsedComponent
|
|
14
14
|
|
|
15
|
+
# Import endpoints with fallback for dev mode
|
|
16
|
+
try:
|
|
17
|
+
# In built wheels, this exists (generated from _endpoints.py.in)
|
|
18
|
+
from golf import _endpoints # type: ignore
|
|
19
|
+
except ImportError:
|
|
20
|
+
# In editable/dev installs, fall back to env-based values
|
|
21
|
+
from golf import _endpoints_fallback as _endpoints # type: ignore
|
|
22
|
+
|
|
15
23
|
console = Console()
|
|
16
24
|
|
|
17
25
|
|
|
@@ -65,7 +73,7 @@ async def register_project_with_platform(
|
|
|
65
73
|
try:
|
|
66
74
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
67
75
|
response = await client.post(
|
|
68
|
-
|
|
76
|
+
_endpoints.PLATFORM_API_URL,
|
|
69
77
|
json=metadata,
|
|
70
78
|
headers={
|
|
71
79
|
"X-Golf-Key": api_key,
|
|
@@ -83,17 +91,11 @@ async def register_project_with_platform(
|
|
|
83
91
|
return False
|
|
84
92
|
except httpx.HTTPStatusError as e:
|
|
85
93
|
if e.response.status_code == 401:
|
|
86
|
-
console.print(
|
|
87
|
-
"[yellow]Warning: Platform registration failed - invalid API key[/yellow]"
|
|
88
|
-
)
|
|
94
|
+
console.print("[yellow]Warning: Platform registration failed - invalid API key[/yellow]")
|
|
89
95
|
elif e.response.status_code == 403:
|
|
90
|
-
console.print(
|
|
91
|
-
"[yellow]Warning: Platform registration failed - access denied[/yellow]"
|
|
92
|
-
)
|
|
96
|
+
console.print("[yellow]Warning: Platform registration failed - access denied[/yellow]")
|
|
93
97
|
else:
|
|
94
|
-
console.print(
|
|
95
|
-
f"[yellow]Warning: Platform registration failed - HTTP {e.response.status_code}[/yellow]"
|
|
96
|
-
)
|
|
98
|
+
console.print(f"[yellow]Warning: Platform registration failed - HTTP {e.response.status_code}[/yellow]")
|
|
97
99
|
return False
|
|
98
100
|
except Exception as e:
|
|
99
101
|
console.print(f"[yellow]Warning: Platform registration failed: {e}[/yellow]")
|
golf/core/telemetry.py
CHANGED
|
@@ -82,7 +82,7 @@ def is_telemetry_enabled() -> bool:
|
|
|
82
82
|
2. GOLF_TEST_MODE environment variable (always disabled in test mode)
|
|
83
83
|
3. GOLF_TELEMETRY environment variable
|
|
84
84
|
4. Persistent preference file
|
|
85
|
-
5. Default to
|
|
85
|
+
5. Default to False (opt-in model)
|
|
86
86
|
"""
|
|
87
87
|
global _telemetry_enabled
|
|
88
88
|
|
|
@@ -144,14 +144,11 @@ def get_anonymous_id() -> str:
|
|
|
144
144
|
if id_file.exists():
|
|
145
145
|
try:
|
|
146
146
|
_anonymous_id = id_file.read_text().strip()
|
|
147
|
-
# Check if ID is in the old format (no hyphen between hash and
|
|
147
|
+
# Check if ID is in the old format (no hyphen between hash and
|
|
148
|
+
# random component)
|
|
148
149
|
# Old format: golf-[8 chars hash][8 chars random]
|
|
149
150
|
# New format: golf-[8 chars hash]-[8 chars random]
|
|
150
|
-
if (
|
|
151
|
-
_anonymous_id
|
|
152
|
-
and _anonymous_id.startswith("golf-")
|
|
153
|
-
and len(_anonymous_id) == 21
|
|
154
|
-
):
|
|
151
|
+
if _anonymous_id and _anonymous_id.startswith("golf-") and len(_anonymous_id) == 21:
|
|
155
152
|
# This is likely the old format, regenerate
|
|
156
153
|
_anonymous_id = None
|
|
157
154
|
elif _anonymous_id:
|
|
@@ -163,9 +160,7 @@ def get_anonymous_id() -> str:
|
|
|
163
160
|
# Use only non-identifying system information
|
|
164
161
|
|
|
165
162
|
# Combine non-identifying factors for uniqueness
|
|
166
|
-
machine_data = (
|
|
167
|
-
f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
|
|
168
|
-
)
|
|
163
|
+
machine_data = f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
|
|
169
164
|
machine_hash = hashlib.sha256(machine_data.encode()).hexdigest()[:8]
|
|
170
165
|
|
|
171
166
|
# Add a random component to ensure uniqueness
|
|
@@ -264,7 +259,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
264
259
|
"$set": {
|
|
265
260
|
"golf_version": __version__,
|
|
266
261
|
"os": platform.system(),
|
|
267
|
-
"python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
|
|
262
|
+
"python_version": (f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}"),
|
|
268
263
|
}
|
|
269
264
|
}
|
|
270
265
|
|
|
@@ -284,7 +279,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
|
|
|
284
279
|
# Only include minimal, non-identifying properties
|
|
285
280
|
safe_properties = {
|
|
286
281
|
"golf_version": __version__,
|
|
287
|
-
"python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
|
|
282
|
+
"python_version": (f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}"),
|
|
288
283
|
"os": platform.system(),
|
|
289
284
|
# Explicitly disable IP tracking and GeoIP enrichment
|
|
290
285
|
"$ip": "0", # Override IP to prevent collection
|
|
@@ -331,7 +326,8 @@ def track_command(
|
|
|
331
326
|
Args:
|
|
332
327
|
command: The command being executed (e.g., "init", "build", "run")
|
|
333
328
|
success: Whether the command was successful
|
|
334
|
-
error_type: Type of error if command failed (e.g., "ValueError",
|
|
329
|
+
error_type: Type of error if command failed (e.g., "ValueError",
|
|
330
|
+
"FileNotFoundError")
|
|
335
331
|
error_message: Sanitized error message (no sensitive data)
|
|
336
332
|
"""
|
|
337
333
|
properties = {"success": success}
|
|
@@ -409,9 +405,7 @@ def track_detailed_error(
|
|
|
409
405
|
|
|
410
406
|
# Add system context for debugging
|
|
411
407
|
try:
|
|
412
|
-
properties["python_executable"] = _sanitize_error_message(
|
|
413
|
-
platform.python_implementation()
|
|
414
|
-
)
|
|
408
|
+
properties["python_executable"] = _sanitize_error_message(platform.python_implementation())
|
|
415
409
|
properties["platform_detail"] = platform.platform()[:50] # Limit length
|
|
416
410
|
except Exception:
|
|
417
411
|
pass
|
|
@@ -459,9 +453,7 @@ def _sanitize_error_message(message: str) -> str:
|
|
|
459
453
|
message = re.sub(r"Bearer\s+[a-zA-Z0-9_.-]+", "Bearer [REDACTED]", message)
|
|
460
454
|
|
|
461
455
|
# Remove email addresses
|
|
462
|
-
message = re.sub(
|
|
463
|
-
r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message
|
|
464
|
-
)
|
|
456
|
+
message = re.sub(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message)
|
|
465
457
|
|
|
466
458
|
# Remove IP addresses
|
|
467
459
|
message = re.sub(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", "[IP]", message)
|