golf-mcp 0.1.10__py3-none-any.whl → 0.1.12__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.

Files changed (48) hide show
  1. golf/__init__.py +1 -1
  2. golf/auth/__init__.py +38 -26
  3. golf/auth/api_key.py +16 -23
  4. golf/auth/helpers.py +68 -54
  5. golf/auth/oauth.py +340 -277
  6. golf/auth/provider.py +58 -53
  7. golf/cli/__init__.py +1 -1
  8. golf/cli/main.py +202 -82
  9. golf/commands/__init__.py +1 -1
  10. golf/commands/build.py +31 -25
  11. golf/commands/init.py +119 -80
  12. golf/commands/run.py +14 -13
  13. golf/core/__init__.py +1 -1
  14. golf/core/builder.py +478 -353
  15. golf/core/builder_auth.py +115 -107
  16. golf/core/builder_telemetry.py +12 -9
  17. golf/core/config.py +62 -46
  18. golf/core/parser.py +174 -136
  19. golf/core/telemetry.py +169 -69
  20. golf/core/transformer.py +53 -55
  21. golf/examples/__init__.py +0 -1
  22. golf/examples/api_key/pre_build.py +2 -2
  23. golf/examples/api_key/tools/issues/create.py +35 -36
  24. golf/examples/api_key/tools/issues/list.py +42 -37
  25. golf/examples/api_key/tools/repos/list.py +50 -29
  26. golf/examples/api_key/tools/search/code.py +50 -37
  27. golf/examples/api_key/tools/users/get.py +21 -20
  28. golf/examples/basic/pre_build.py +4 -4
  29. golf/examples/basic/prompts/welcome.py +6 -7
  30. golf/examples/basic/resources/current_time.py +10 -9
  31. golf/examples/basic/resources/info.py +6 -5
  32. golf/examples/basic/resources/weather/common.py +16 -10
  33. golf/examples/basic/resources/weather/current.py +15 -11
  34. golf/examples/basic/resources/weather/forecast.py +15 -11
  35. golf/examples/basic/tools/github_user.py +19 -21
  36. golf/examples/basic/tools/hello.py +10 -6
  37. golf/examples/basic/tools/payments/charge.py +34 -25
  38. golf/examples/basic/tools/payments/common.py +8 -6
  39. golf/examples/basic/tools/payments/refund.py +29 -25
  40. golf/telemetry/__init__.py +6 -6
  41. golf/telemetry/instrumentation.py +781 -276
  42. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/METADATA +1 -1
  43. golf_mcp-0.1.12.dist-info/RECORD +55 -0
  44. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/WHEEL +1 -1
  45. golf_mcp-0.1.10.dist-info/RECORD +0 -55
  46. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/entry_points.txt +0 -0
  47. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/licenses/LICENSE +0 -0
  48. {golf_mcp-0.1.10.dist-info → golf_mcp-0.1.12.dist-info}/top_level.txt +0 -0
golf/core/parser.py CHANGED
@@ -5,7 +5,7 @@ import re
5
5
  from dataclasses import dataclass
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Any, Dict, List, Optional
8
+ from typing import Any
9
9
 
10
10
  from rich.console import Console
11
11
 
@@ -14,7 +14,7 @@ console = Console()
14
14
 
15
15
  class ComponentType(str, Enum):
16
16
  """Type of component discovered by the parser."""
17
-
17
+
18
18
  TOOL = "tool"
19
19
  RESOURCE = "resource"
20
20
  PROMPT = "prompt"
@@ -24,65 +24,69 @@ class ComponentType(str, Enum):
24
24
  @dataclass
25
25
  class ParsedComponent:
26
26
  """Represents a parsed MCP component (tool, resource, or prompt)."""
27
-
27
+
28
28
  name: str # Derived from file path or explicit name
29
29
  type: ComponentType
30
30
  file_path: Path
31
31
  module_path: str
32
- docstring: Optional[str] = None
33
- input_schema: Optional[Dict[str, Any]] = None
34
- output_schema: Optional[Dict[str, Any]] = None
35
- uri_template: Optional[str] = None # For resources
36
- parameters: Optional[List[str]] = None # For resources with URI params
37
- parent_module: Optional[str] = None # For nested components
38
- entry_function: Optional[str] = None # Store the name of the function to use
32
+ docstring: str | None = None
33
+ input_schema: dict[str, Any] | None = None
34
+ output_schema: dict[str, Any] | None = None
35
+ uri_template: str | None = None # For resources
36
+ parameters: list[str] | None = None # For resources with URI params
37
+ parent_module: str | None = None # For nested components
38
+ entry_function: str | None = None # Store the name of the function to use
39
39
 
40
40
 
41
41
  class AstParser:
42
42
  """AST-based parser for extracting MCP components from Python files."""
43
-
44
- def __init__(self, project_root: Path):
43
+
44
+ def __init__(self, project_root: Path) -> None:
45
45
  """Initialize the parser.
46
-
46
+
47
47
  Args:
48
48
  project_root: Root directory of the project
49
49
  """
50
50
  self.project_root = project_root
51
- self.components: Dict[str, ParsedComponent] = {}
52
-
53
- def parse_directory(self, directory: Path) -> List[ParsedComponent]:
51
+ self.components: dict[str, ParsedComponent] = {}
52
+
53
+ def parse_directory(self, directory: Path) -> list[ParsedComponent]:
54
54
  """Parse all Python files in a directory recursively."""
55
55
  components = []
56
-
56
+
57
57
  for file_path in directory.glob("**/*.py"):
58
58
  # Skip __pycache__ and other hidden directories
59
- if "__pycache__" in file_path.parts or any(part.startswith('.') for part in file_path.parts):
59
+ if "__pycache__" in file_path.parts or any(
60
+ part.startswith(".") for part in file_path.parts
61
+ ):
60
62
  continue
61
-
63
+
62
64
  try:
63
65
  file_components = self.parse_file(file_path)
64
66
  components.extend(file_components)
65
67
  except Exception as e:
66
68
  relative_path = file_path.relative_to(self.project_root)
67
- console.print(f"[bold red]Error parsing {relative_path}:[/bold red] {e}")
68
-
69
+ console.print(
70
+ f"[bold red]Error parsing {relative_path}:[/bold red] {e}"
71
+ )
72
+
69
73
  return components
70
-
71
- def parse_file(self, file_path: Path) -> List[ParsedComponent]:
74
+
75
+ def parse_file(self, file_path: Path) -> list[ParsedComponent]:
72
76
  """Parse a single Python file using AST to extract MCP components."""
73
77
  # Handle common.py files
74
78
  if file_path.name == "common.py":
75
79
  # Register as a known shared module but don't return as a component
76
80
  return []
77
-
81
+
78
82
  # Skip __init__.py files for direct parsing
79
83
  if file_path.name == "__init__.py":
80
84
  return []
81
-
85
+
82
86
  # Determine component type based on directory structure
83
87
  rel_path = file_path.relative_to(self.project_root)
84
88
  parent_dir = rel_path.parts[0] if rel_path.parts else None
85
-
89
+
86
90
  component_type = ComponentType.UNKNOWN
87
91
  if parent_dir == "tools":
88
92
  component_type = ComponentType.TOOL
@@ -90,29 +94,29 @@ class AstParser:
90
94
  component_type = ComponentType.RESOURCE
91
95
  elif parent_dir == "prompts":
92
96
  component_type = ComponentType.PROMPT
93
-
97
+
94
98
  if component_type == ComponentType.UNKNOWN:
95
99
  return [] # Not in a recognized directory
96
-
100
+
97
101
  # Read the file content and parse it with AST
98
- with open(file_path, 'r', encoding='utf-8') as f:
102
+ with open(file_path, encoding="utf-8") as f:
99
103
  file_content = f.read()
100
-
104
+
101
105
  try:
102
106
  tree = ast.parse(file_content)
103
107
  except SyntaxError as e:
104
108
  raise ValueError(f"Syntax error in {file_path}: {e}")
105
-
109
+
106
110
  # Extract module docstring
107
111
  module_docstring = ast.get_docstring(tree)
108
112
  if not module_docstring:
109
113
  raise ValueError(f"Missing module docstring in {file_path}")
110
-
111
- # Find the entry function - look for "export = function_name" pattern,
114
+
115
+ # Find the entry function - look for "export = function_name" pattern,
112
116
  # or any top-level function (like "run") as a fallback
113
117
  entry_function = None
114
118
  export_target = None
115
-
119
+
116
120
  # Look for export = function_name assignment
117
121
  for node in tree.body:
118
122
  if isinstance(node, ast.Assign):
@@ -121,33 +125,35 @@ class AstParser:
121
125
  if isinstance(node.value, ast.Name):
122
126
  export_target = node.value.id
123
127
  break
124
-
128
+
125
129
  # Find all top-level functions
126
130
  functions = []
127
131
  for node in tree.body:
128
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
132
+ if isinstance(node, ast.FunctionDef | ast.AsyncFunctionDef):
129
133
  functions.append(node)
130
134
  # If this function matches our export target, it's our entry function
131
135
  if export_target and node.name == export_target:
132
136
  entry_function = node
133
-
137
+
134
138
  # Check for the run function as a fallback
135
139
  run_function = None
136
140
  for func in functions:
137
141
  if func.name == "run":
138
142
  run_function = func
139
-
143
+
140
144
  # If we have an export but didn't find the target function, warn
141
145
  if export_target and not entry_function:
142
- console.print(f"[yellow]Warning: Export target '{export_target}' not found in {file_path}[/yellow]")
143
-
146
+ console.print(
147
+ f"[yellow]Warning: Export target '{export_target}' not found in {file_path}[/yellow]"
148
+ )
149
+
144
150
  # Use the export target function if found, otherwise fall back to run
145
151
  entry_function = entry_function or run_function
146
-
152
+
147
153
  # If no valid function found, skip this file
148
154
  if not entry_function:
149
155
  return []
150
-
156
+
151
157
  # Create component
152
158
  component = ParsedComponent(
153
159
  name="", # Will be set later
@@ -155,12 +161,13 @@ class AstParser:
155
161
  file_path=file_path,
156
162
  module_path=file_path.relative_to(self.project_root).as_posix(),
157
163
  docstring=module_docstring,
158
- entry_function=export_target or "run" # Store the name of the entry function
164
+ entry_function=export_target
165
+ or "run", # Store the name of the entry function
159
166
  )
160
-
167
+
161
168
  # Process the entry function
162
169
  self._process_entry_function(component, entry_function, tree, file_path)
163
-
170
+
164
171
  # Process other component-specific information
165
172
  if component_type == ComponentType.TOOL:
166
173
  self._process_tool(component, tree)
@@ -168,72 +175,86 @@ class AstParser:
168
175
  self._process_resource(component, tree)
169
176
  elif component_type == ComponentType.PROMPT:
170
177
  self._process_prompt(component, tree)
171
-
178
+
172
179
  # Set component name based on file path
173
180
  component.name = self._derive_component_name(file_path, component_type)
174
-
181
+
175
182
  # Set parent module if it's in a nested structure
176
183
  if len(rel_path.parts) > 2: # More than just "tools/file.py"
177
- parent_parts = rel_path.parts[1:-1] # Skip the root category and the file itself
184
+ parent_parts = rel_path.parts[
185
+ 1:-1
186
+ ] # Skip the root category and the file itself
178
187
  if parent_parts:
179
188
  component.parent_module = ".".join(parent_parts)
180
-
189
+
181
190
  return [component]
182
-
183
- def _process_entry_function(self, component: ParsedComponent, func_node: ast.FunctionDef | ast.AsyncFunctionDef, tree: ast.Module, file_path: Path) -> None:
191
+
192
+ def _process_entry_function(
193
+ self,
194
+ component: ParsedComponent,
195
+ func_node: ast.FunctionDef | ast.AsyncFunctionDef,
196
+ tree: ast.Module,
197
+ file_path: Path,
198
+ ) -> None:
184
199
  """Process the entry function to extract parameters and return type."""
185
200
  # Extract function docstring
186
- func_docstring = ast.get_docstring(func_node)
187
-
201
+ ast.get_docstring(func_node)
202
+
188
203
  # Extract parameter names and annotations
189
204
  parameters = []
190
205
  for arg in func_node.args.args:
191
206
  # Skip self, cls parameters
192
207
  if arg.arg in ("self", "cls"):
193
208
  continue
194
-
209
+
195
210
  # Skip ctx parameter - GolfMCP will inject this
196
211
  if arg.arg == "ctx":
197
212
  continue
198
-
213
+
199
214
  parameters.append(arg.arg)
200
-
215
+
201
216
  # Check for return annotation - STRICT requirement
202
217
  if func_node.returns is None:
203
- raise ValueError(f"Missing return annotation for {func_node.name} function in {file_path}")
204
-
218
+ raise ValueError(
219
+ f"Missing return annotation for {func_node.name} function in {file_path}"
220
+ )
221
+
205
222
  # Store parameters
206
223
  component.parameters = parameters
207
-
224
+
208
225
  def _process_tool(self, component: ParsedComponent, tree: ast.Module) -> None:
209
226
  """Process a tool component to extract input/output schemas."""
210
227
  # Look for Input and Output classes in the AST
211
228
  input_class = None
212
229
  output_class = None
213
-
230
+
214
231
  for node in tree.body:
215
232
  if isinstance(node, ast.ClassDef):
216
233
  if node.name == "Input":
217
234
  input_class = node
218
235
  elif node.name == "Output":
219
236
  output_class = node
220
-
237
+
221
238
  # Process Input class if found
222
239
  if input_class:
223
240
  # Check if it inherits from BaseModel
224
241
  for base in input_class.bases:
225
242
  if isinstance(base, ast.Name) and base.id == "BaseModel":
226
- component.input_schema = self._extract_pydantic_schema_from_ast(input_class)
243
+ component.input_schema = self._extract_pydantic_schema_from_ast(
244
+ input_class
245
+ )
227
246
  break
228
-
247
+
229
248
  # Process Output class if found
230
249
  if output_class:
231
250
  # Check if it inherits from BaseModel
232
251
  for base in output_class.bases:
233
252
  if isinstance(base, ast.Name) and base.id == "BaseModel":
234
- component.output_schema = self._extract_pydantic_schema_from_ast(output_class)
253
+ component.output_schema = self._extract_pydantic_schema_from_ast(
254
+ output_class
255
+ )
235
256
  break
236
-
257
+
237
258
  def _process_resource(self, component: ParsedComponent, tree: ast.Module) -> None:
238
259
  """Process a resource component to extract URI template."""
239
260
  # Look for resource_uri assignment in the AST
@@ -244,68 +265,66 @@ class AstParser:
244
265
  if isinstance(node.value, ast.Constant):
245
266
  uri_template = node.value.value
246
267
  component.uri_template = uri_template
247
-
268
+
248
269
  # Extract URI parameters (parts in {})
249
270
  uri_params = re.findall(r"{([^}]+)}", uri_template)
250
271
  if uri_params:
251
272
  component.parameters = uri_params
252
273
  break
253
-
274
+
254
275
  def _process_prompt(self, component: ParsedComponent, tree: ast.Module) -> None:
255
276
  """Process a prompt component (no special processing needed)."""
256
277
  pass
257
-
258
- def _derive_component_name(self, file_path: Path, component_type: ComponentType) -> str:
278
+
279
+ def _derive_component_name(
280
+ self, file_path: Path, component_type: ComponentType
281
+ ) -> str:
259
282
  """Derive a component name from its file path according to the spec.
260
-
283
+
261
284
  Following the spec: <filename> + ("-" + "-".join(PathRev) if PathRev else "")
262
285
  where PathRev is the reversed list of parent directories under the category.
263
286
  """
264
287
  rel_path = file_path.relative_to(self.project_root)
265
-
288
+
266
289
  # Find which category directory this is in
267
- category = None
268
290
  category_idx = -1
269
291
  for i, part in enumerate(rel_path.parts):
270
292
  if part in ["tools", "resources", "prompts"]:
271
- category = part
272
293
  category_idx = i
273
294
  break
274
-
295
+
275
296
  if category_idx == -1:
276
297
  return ""
277
-
298
+
278
299
  # Get the filename without extension
279
300
  filename = rel_path.stem
280
-
301
+
281
302
  # Get parent directories between category and file
282
- parent_dirs = list(rel_path.parts[category_idx+1:-1])
283
-
303
+ parent_dirs = list(rel_path.parts[category_idx + 1 : -1])
304
+
284
305
  # Reverse parent dirs according to spec
285
306
  parent_dirs.reverse()
286
-
307
+
287
308
  # Form the ID according to spec
288
309
  if parent_dirs:
289
310
  return f"{filename}-{'-'.join(parent_dirs)}"
290
311
  else:
291
312
  return filename
292
-
293
- def _extract_pydantic_schema_from_ast(self, class_node: ast.ClassDef) -> Dict[str, Any]:
313
+
314
+ def _extract_pydantic_schema_from_ast(
315
+ self, class_node: ast.ClassDef
316
+ ) -> dict[str, Any]:
294
317
  """Extract a JSON schema from an AST class definition.
295
-
318
+
296
319
  This is a simplified version that extracts basic field information.
297
320
  For complex annotations, a more sophisticated approach would be needed.
298
321
  """
299
- schema = {
300
- "type": "object",
301
- "properties": {},
302
- "required": []
303
- }
304
-
322
+ schema = {"type": "object", "properties": {}, "required": []}
323
+
305
324
  for node in class_node.body:
306
325
  if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
307
326
  field_name = node.target.id
308
-
327
+
309
328
  # Extract type annotation as string
310
329
  annotation = ""
311
330
  if isinstance(node.annotation, ast.Name):
@@ -315,24 +334,29 @@ class AstParser:
315
334
  annotation = ast.unparse(node.annotation)
316
335
  else:
317
336
  annotation = ast.unparse(node.annotation)
318
-
337
+
319
338
  # Create property definition
320
339
  prop = {
321
340
  "type": self._type_hint_to_json_type(annotation),
322
- "title": field_name.replace('_', ' ').title()
341
+ "title": field_name.replace("_", " ").title(),
323
342
  }
324
-
343
+
325
344
  # Extract default value if present
326
345
  if node.value is not None:
327
346
  if isinstance(node.value, ast.Constant):
328
347
  # Simple constant default
329
348
  prop["default"] = node.value.value
330
- elif (isinstance(node.value, ast.Call) and
331
- isinstance(node.value.func, ast.Name) and
332
- node.value.func.id == "Field"):
349
+ elif (
350
+ isinstance(node.value, ast.Call)
351
+ and isinstance(node.value.func, ast.Name)
352
+ and node.value.func.id == "Field"
353
+ ):
333
354
  # Field object - extract its parameters
334
355
  for keyword in node.value.keywords:
335
- if keyword.arg == "default" or keyword.arg == "default_factory":
356
+ if (
357
+ keyword.arg == "default"
358
+ or keyword.arg == "default_factory"
359
+ ):
336
360
  if isinstance(keyword.value, ast.Constant):
337
361
  prop["default"] = keyword.value.value
338
362
  elif keyword.arg == "description":
@@ -341,47 +365,58 @@ class AstParser:
341
365
  elif keyword.arg == "title":
342
366
  if isinstance(keyword.value, ast.Constant):
343
367
  prop["title"] = keyword.value.value
344
-
368
+
345
369
  # Check for position default argument (Field(..., "description"))
346
370
  if node.value.args:
347
371
  for i, arg in enumerate(node.value.args):
348
- if i == 0 and isinstance(arg, ast.Constant) and arg.value != Ellipsis:
372
+ if (
373
+ i == 0
374
+ and isinstance(arg, ast.Constant)
375
+ and arg.value != Ellipsis
376
+ ):
349
377
  prop["default"] = arg.value
350
378
  elif i == 1 and isinstance(arg, ast.Constant):
351
379
  prop["description"] = arg.value
352
-
380
+
353
381
  # Add to properties
354
382
  schema["properties"][field_name] = prop
355
-
383
+
356
384
  # Check if required (no default value or Field(...))
357
385
  is_required = True
358
386
  if node.value is not None:
359
387
  if isinstance(node.value, ast.Constant):
360
388
  is_required = False
361
- elif (isinstance(node.value, ast.Call) and
362
- isinstance(node.value.func, ast.Name) and
363
- node.value.func.id == "Field"):
389
+ elif (
390
+ isinstance(node.value, ast.Call)
391
+ and isinstance(node.value.func, ast.Name)
392
+ and node.value.func.id == "Field"
393
+ ):
364
394
  # Field has default if it doesn't use ... or if it has a default keyword
365
395
  has_ellipsis = False
366
396
  has_default = False
367
-
368
- if node.value.args and isinstance(node.value.args[0], ast.Constant):
397
+
398
+ if node.value.args and isinstance(
399
+ node.value.args[0], ast.Constant
400
+ ):
369
401
  has_ellipsis = node.value.args[0].value is Ellipsis
370
-
402
+
371
403
  for keyword in node.value.keywords:
372
- if keyword.arg == "default" or keyword.arg == "default_factory":
404
+ if (
405
+ keyword.arg == "default"
406
+ or keyword.arg == "default_factory"
407
+ ):
373
408
  has_default = True
374
-
409
+
375
410
  is_required = has_ellipsis and not has_default
376
-
411
+
377
412
  if is_required:
378
413
  schema["required"].append(field_name)
379
-
414
+
380
415
  return schema
381
-
416
+
382
417
  def _type_hint_to_json_type(self, type_hint: str) -> str:
383
418
  """Convert a Python type hint to a JSON schema type.
384
-
419
+
385
420
  This is a simplified version. A more sophisticated approach would
386
421
  handle complex types correctly.
387
422
  """
@@ -393,76 +428,79 @@ class AstParser:
393
428
  "list": "array",
394
429
  "dict": "object",
395
430
  }
396
-
431
+
397
432
  # Handle simple types
398
433
  for py_type, json_type in type_map.items():
399
434
  if py_type in type_hint.lower():
400
435
  return json_type
401
-
436
+
402
437
  # Default to string for unknown types
403
438
  return "string"
404
439
 
405
440
 
406
- def parse_project(project_path: Path) -> Dict[ComponentType, List[ParsedComponent]]:
441
+ def parse_project(project_path: Path) -> dict[ComponentType, list[ParsedComponent]]:
407
442
  """Parse a GolfMCP project to extract all components."""
408
443
  parser = AstParser(project_path)
409
-
410
- components: Dict[ComponentType, List[ParsedComponent]] = {
444
+
445
+ components: dict[ComponentType, list[ParsedComponent]] = {
411
446
  ComponentType.TOOL: [],
412
447
  ComponentType.RESOURCE: [],
413
- ComponentType.PROMPT: []
448
+ ComponentType.PROMPT: [],
414
449
  }
415
-
450
+
416
451
  # Parse each directory
417
452
  for comp_type, dir_name in [
418
453
  (ComponentType.TOOL, "tools"),
419
454
  (ComponentType.RESOURCE, "resources"),
420
- (ComponentType.PROMPT, "prompts")
455
+ (ComponentType.PROMPT, "prompts"),
421
456
  ]:
422
457
  dir_path = project_path / dir_name
423
458
  if dir_path.exists() and dir_path.is_dir():
424
459
  dir_components = parser.parse_directory(dir_path)
425
- components[comp_type].extend([c for c in dir_components if c.type == comp_type])
426
-
460
+ components[comp_type].extend(
461
+ [c for c in dir_components if c.type == comp_type]
462
+ )
463
+
427
464
  # Check for ID collisions
428
465
  all_ids = []
429
466
  for comp_type, comps in components.items():
430
467
  for comp in comps:
431
468
  if comp.name in all_ids:
432
- raise ValueError(f"ID collision detected: {comp.name} is used by multiple components")
469
+ raise ValueError(
470
+ f"ID collision detected: {comp.name} is used by multiple components"
471
+ )
433
472
  all_ids.append(comp.name)
434
-
435
- return components
436
-
437
-
438
473
 
474
+ return components
439
475
 
440
476
 
441
- def parse_common_files(project_path: Path) -> Dict[str, Path]:
477
+ def parse_common_files(project_path: Path) -> dict[str, Path]:
442
478
  """Find all common.py files in the project.
443
-
479
+
444
480
  Args:
445
481
  project_path: Path to the project root
446
-
482
+
447
483
  Returns:
448
484
  Dictionary mapping directory paths to common.py file paths
449
485
  """
450
486
  common_files = {}
451
-
487
+
452
488
  # Search for common.py files in tools, resources, and prompts directories
453
489
  for dir_name in ["tools", "resources", "prompts"]:
454
490
  base_dir = project_path / dir_name
455
491
  if not base_dir.exists() or not base_dir.is_dir():
456
492
  continue
457
-
493
+
458
494
  # Find all common.py files (recursively)
459
495
  for common_file in base_dir.glob("**/common.py"):
460
496
  # Skip files in __pycache__ or other hidden directories
461
- if "__pycache__" in common_file.parts or any(part.startswith('.') for part in common_file.parts):
497
+ if "__pycache__" in common_file.parts or any(
498
+ part.startswith(".") for part in common_file.parts
499
+ ):
462
500
  continue
463
-
501
+
464
502
  # Get the parent directory as the module path
465
503
  module_path = str(common_file.parent.relative_to(project_path))
466
504
  common_files[module_path] = common_file
467
-
468
- return common_files
505
+
506
+ return common_files