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/telemetry.py CHANGED
@@ -1,12 +1,12 @@
1
1
  """Telemetry module for anonymous usage tracking with PostHog."""
2
2
 
3
- import os
4
3
  import hashlib
5
- import platform
6
- from pathlib import Path
7
- from typing import Optional, Dict, Any
8
4
  import json
5
+ import os
6
+ import platform
9
7
  import uuid
8
+ from pathlib import Path
9
+ from typing import Any
10
10
 
11
11
  import posthog
12
12
  from rich.console import Console
@@ -23,11 +23,22 @@ POSTHOG_API_KEY = os.environ.get("GOLF_POSTHOG_API_KEY", DEFAULT_POSTHOG_API_KEY
23
23
  POSTHOG_HOST = "https://us.i.posthog.com"
24
24
 
25
25
  # Telemetry state
26
- _telemetry_enabled: Optional[bool] = None
27
- _anonymous_id: Optional[str] = None
26
+ _telemetry_enabled: bool | None = None
27
+ _anonymous_id: str | None = None
28
28
  _user_identified: bool = False # Track if we've already identified the user
29
29
 
30
30
 
31
+ def _is_test_mode() -> bool:
32
+ """Check if we're in test mode."""
33
+ return os.environ.get("GOLF_TEST_MODE", "").lower() in ("1", "true", "yes", "on")
34
+
35
+
36
+ def _ensure_posthog_disabled_in_test_mode() -> None:
37
+ """Ensure PostHog is disabled when in test mode."""
38
+ if _is_test_mode() and not posthog.disabled:
39
+ posthog.disabled = True
40
+
41
+
31
42
  def get_telemetry_config_path() -> Path:
32
43
  """Get the path to the telemetry configuration file."""
33
44
  return Path.home() / ".golf" / "telemetry.json"
@@ -37,12 +48,9 @@ def save_telemetry_preference(enabled: bool) -> None:
37
48
  """Save telemetry preference to persistent storage."""
38
49
  config_path = get_telemetry_config_path()
39
50
  config_path.parent.mkdir(parents=True, exist_ok=True)
40
-
41
- config = {
42
- "enabled": enabled,
43
- "version": 1
44
- }
45
-
51
+
52
+ config = {"enabled": enabled, "version": 1}
53
+
46
54
  try:
47
55
  with open(config_path, "w") as f:
48
56
  json.dump(config, f)
@@ -51,15 +59,15 @@ def save_telemetry_preference(enabled: bool) -> None:
51
59
  pass
52
60
 
53
61
 
54
- def load_telemetry_preference() -> Optional[bool]:
62
+ def load_telemetry_preference() -> bool | None:
55
63
  """Load telemetry preference from persistent storage."""
56
64
  config_path = get_telemetry_config_path()
57
-
65
+
58
66
  if not config_path.exists():
59
67
  return None
60
-
68
+
61
69
  try:
62
- with open(config_path, "r") as f:
70
+ with open(config_path) as f:
63
71
  config = json.load(f)
64
72
  return config.get("enabled")
65
73
  except Exception:
@@ -68,19 +76,25 @@ def load_telemetry_preference() -> Optional[bool]:
68
76
 
69
77
  def is_telemetry_enabled() -> bool:
70
78
  """Check if telemetry is enabled.
71
-
79
+
72
80
  Checks in order:
73
81
  1. Cached value
74
- 2. GOLF_TELEMETRY environment variable
75
- 3. Persistent preference file
76
- 4. Default to True (opt-out model)
82
+ 2. GOLF_TEST_MODE environment variable (always disabled in test mode)
83
+ 3. GOLF_TELEMETRY environment variable
84
+ 4. Persistent preference file
85
+ 5. Default to True (opt-out model)
77
86
  """
78
87
  global _telemetry_enabled
79
-
88
+
80
89
  if _telemetry_enabled is not None:
81
90
  return _telemetry_enabled
82
-
83
- # Check environment variables (highest priority)
91
+
92
+ # Check if we're in test mode (highest priority after cache)
93
+ if _is_test_mode():
94
+ _telemetry_enabled = False
95
+ return False
96
+
97
+ # Check environment variables (second highest priority)
84
98
  env_telemetry = os.environ.get("GOLF_TELEMETRY", "").lower()
85
99
  if env_telemetry in ("0", "false", "no", "off"):
86
100
  _telemetry_enabled = False
@@ -88,13 +102,13 @@ def is_telemetry_enabled() -> bool:
88
102
  elif env_telemetry in ("1", "true", "yes", "on"):
89
103
  _telemetry_enabled = True
90
104
  return True
91
-
105
+
92
106
  # Check persistent preference
93
107
  saved_preference = load_telemetry_preference()
94
108
  if saved_preference is not None:
95
109
  _telemetry_enabled = saved_preference
96
110
  return saved_preference
97
-
111
+
98
112
  # Default to enabled (opt-out model)
99
113
  _telemetry_enabled = True
100
114
  return True
@@ -102,58 +116,64 @@ def is_telemetry_enabled() -> bool:
102
116
 
103
117
  def set_telemetry_enabled(enabled: bool, persist: bool = True) -> None:
104
118
  """Set telemetry enabled state.
105
-
119
+
106
120
  Args:
107
121
  enabled: Whether telemetry should be enabled
108
122
  persist: Whether to save this preference persistently
109
123
  """
110
124
  global _telemetry_enabled
111
125
  _telemetry_enabled = enabled
112
-
126
+
113
127
  if persist:
114
128
  save_telemetry_preference(enabled)
115
129
 
116
130
 
117
131
  def get_anonymous_id() -> str:
118
132
  """Get or create a persistent anonymous ID for this machine.
119
-
133
+
120
134
  The ID is stored in the user's home directory and is unique per installation.
121
135
  """
122
136
  global _anonymous_id
123
-
137
+
124
138
  if _anonymous_id:
125
139
  return _anonymous_id
126
-
140
+
127
141
  # Try to load existing ID
128
142
  id_file = Path.home() / ".golf" / "telemetry_id"
129
-
143
+
130
144
  if id_file.exists():
131
145
  try:
132
146
  _anonymous_id = id_file.read_text().strip()
133
147
  # Check if ID is in the old format (no hyphen between hash and random component)
134
148
  # Old format: golf-[8 chars hash][8 chars random]
135
149
  # New format: golf-[8 chars hash]-[8 chars random]
136
- if _anonymous_id and _anonymous_id.startswith("golf-") and len(_anonymous_id) == 21:
150
+ if (
151
+ _anonymous_id
152
+ and _anonymous_id.startswith("golf-")
153
+ and len(_anonymous_id) == 21
154
+ ):
137
155
  # This is likely the old format, regenerate
138
156
  _anonymous_id = None
139
157
  elif _anonymous_id:
140
158
  return _anonymous_id
141
159
  except Exception:
142
160
  pass
143
-
161
+
144
162
  # Generate new ID with more unique data
145
163
  # Use only non-identifying system information
146
-
164
+
147
165
  # Combine non-identifying factors for uniqueness
148
- machine_data = f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
166
+ machine_data = (
167
+ f"{platform.machine()}-{platform.system()}-{platform.python_version()}"
168
+ )
149
169
  machine_hash = hashlib.sha256(machine_data.encode()).hexdigest()[:8]
150
-
170
+
151
171
  # Add a random component to ensure uniqueness
152
- random_component = str(uuid.uuid4()).split('-')[0] # First 8 chars of UUID
153
-
172
+ random_component = str(uuid.uuid4()).split("-")[0] # First 8 chars of UUID
173
+
154
174
  # Use hyphen separator for clarity and ensure PostHog treats these as different IDs
155
175
  _anonymous_id = f"golf-{machine_hash}-{random_component}"
156
-
176
+
157
177
  # Try to save for next time
158
178
  try:
159
179
  id_file.parent.mkdir(parents=True, exist_ok=True)
@@ -161,56 +181,70 @@ def get_anonymous_id() -> str:
161
181
  except Exception:
162
182
  # Not critical if we can't save
163
183
  pass
164
-
184
+
165
185
  return _anonymous_id
166
186
 
167
187
 
168
188
  def initialize_telemetry() -> None:
169
189
  """Initialize PostHog telemetry if enabled."""
190
+ # Ensure PostHog is disabled in test mode
191
+ _ensure_posthog_disabled_in_test_mode()
192
+
193
+ # Don't initialize if PostHog is disabled (test mode)
194
+ if posthog.disabled:
195
+ return
196
+
170
197
  if not is_telemetry_enabled():
171
198
  return
172
-
199
+
173
200
  # Skip initialization if no valid API key (empty or placeholder)
174
201
  if not POSTHOG_API_KEY or POSTHOG_API_KEY.startswith("phc_YOUR"):
175
202
  return
176
-
203
+
177
204
  try:
178
205
  posthog.project_api_key = POSTHOG_API_KEY
179
206
  posthog.host = POSTHOG_HOST
180
-
207
+
181
208
  # Disable PostHog's own logging to avoid noise
182
209
  posthog.disabled = False
183
210
  posthog.debug = False
184
-
211
+
185
212
  except Exception:
186
213
  # Telemetry should never break the application
187
214
  pass
188
215
 
189
216
 
190
- def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) -> None:
217
+ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> None:
191
218
  """Track an anonymous event with minimal data.
192
-
219
+
193
220
  Args:
194
221
  event_name: Name of the event (e.g., "cli_init", "cli_build")
195
222
  properties: Optional properties to include with the event
196
223
  """
197
224
  global _user_identified
198
-
225
+
226
+ # Ensure PostHog is disabled in test mode
227
+ _ensure_posthog_disabled_in_test_mode()
228
+
229
+ # Early return if PostHog is disabled (test mode)
230
+ if posthog.disabled:
231
+ return
232
+
199
233
  if not is_telemetry_enabled():
200
234
  return
201
-
235
+
202
236
  # Skip if no valid API key (empty or placeholder)
203
237
  if not POSTHOG_API_KEY or POSTHOG_API_KEY.startswith("phc_YOUR"):
204
238
  return
205
-
239
+
206
240
  try:
207
241
  # Initialize if needed
208
242
  if posthog.project_api_key != POSTHOG_API_KEY:
209
243
  initialize_telemetry()
210
-
244
+
211
245
  # Get anonymous ID
212
246
  anonymous_id = get_anonymous_id()
213
-
247
+
214
248
  # Only identify the user once per session
215
249
  if not _user_identified:
216
250
  # Set person properties to differentiate installations
@@ -222,15 +256,12 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
222
256
  "python_version": f"{platform.python_version_tuple()[0]}.{platform.python_version_tuple()[1]}",
223
257
  }
224
258
  }
225
-
259
+
226
260
  # Identify the user with properties
227
- posthog.identify(
228
- distinct_id=anonymous_id,
229
- properties=person_properties
230
- )
231
-
261
+ posthog.identify(distinct_id=anonymous_id, properties=person_properties)
262
+
232
263
  _user_identified = True
233
-
264
+
234
265
  # Only include minimal, non-identifying properties
235
266
  safe_properties = {
236
267
  "golf_version": __version__,
@@ -239,43 +270,112 @@ def track_event(event_name: str, properties: Optional[Dict[str, Any]] = None) ->
239
270
  # Explicitly disable IP tracking
240
271
  "$ip": None,
241
272
  }
242
-
273
+
243
274
  # Filter properties to only include safe ones
244
275
  if properties:
245
276
  # Only include specific safe properties
246
- safe_keys = {"success", "environment", "template", "command_type"}
277
+ safe_keys = {
278
+ "success",
279
+ "environment",
280
+ "template",
281
+ "command_type",
282
+ "error_type",
283
+ "error_message",
284
+ }
247
285
  for key in safe_keys:
248
286
  if key in properties:
249
287
  safe_properties[key] = properties[key]
250
-
288
+
251
289
  # Send event
252
290
  posthog.capture(
253
291
  distinct_id=anonymous_id,
254
292
  event=event_name,
255
293
  properties=safe_properties,
256
294
  )
257
-
295
+
258
296
  except Exception:
259
297
  # Telemetry should never break the application
260
298
  pass
261
299
 
262
300
 
263
- def track_command(command: str, success: bool = True) -> None:
301
+ def track_command(
302
+ command: str,
303
+ success: bool = True,
304
+ error_type: str | None = None,
305
+ error_message: str | None = None,
306
+ ) -> None:
264
307
  """Track a CLI command execution with minimal info.
265
-
308
+
266
309
  Args:
267
310
  command: The command being executed (e.g., "init", "build", "run")
268
311
  success: Whether the command was successful
312
+ error_type: Type of error if command failed (e.g., "ValueError", "FileNotFoundError")
313
+ error_message: Sanitized error message (no sensitive data)
314
+ """
315
+ properties = {"success": success}
316
+
317
+ # Add error details if command failed
318
+ if not success and (error_type or error_message):
319
+ if error_type:
320
+ properties["error_type"] = error_type
321
+ if error_message:
322
+ # Sanitize error message - remove file paths and sensitive info
323
+ sanitized_message = _sanitize_error_message(error_message)
324
+ properties["error_message"] = sanitized_message
325
+
326
+ track_event(f"cli_{command}", properties)
327
+
328
+
329
+ def _sanitize_error_message(message: str) -> str:
330
+ """Sanitize error message to remove sensitive information.
331
+
332
+ Args:
333
+ message: Raw error message
334
+
335
+ Returns:
336
+ Sanitized error message
269
337
  """
270
- # Simplify the event to just command and success
271
- track_event(f"cli_{command}", {"success": success})
338
+ import re
339
+
340
+ # Remove absolute file paths but keep the filename
341
+ # Unix-style paths
342
+ message = re.sub(
343
+ r'/(?:Users|home|var|tmp|opt|usr|etc)/[^\s"\']+/([^/\s"\']+)', r"\1", message
344
+ )
345
+ # Windows-style paths
346
+ message = re.sub(r'[A-Za-z]:\\[^\s"\']+\\([^\\s"\']+)', r"\1", message)
347
+ # Generic path pattern (catches remaining paths)
348
+ message = re.sub(r'(?:^|[\s"])(/[^\s"\']+/)+([^/\s"\']+)', r"\2", message)
349
+
350
+ # Remove potential API keys or tokens (common patterns)
351
+ # Generic API keys (20+ alphanumeric with underscores/hyphens)
352
+ message = re.sub(r"\b[a-zA-Z0-9_-]{32,}\b", "[REDACTED]", message)
353
+ # Bearer tokens
354
+ message = re.sub(r"Bearer\s+[a-zA-Z0-9_.-]+", "Bearer [REDACTED]", message)
355
+
356
+ # Remove email addresses
357
+ message = re.sub(
358
+ r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", "[EMAIL]", message
359
+ )
360
+
361
+ # Remove IP addresses
362
+ message = re.sub(r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b", "[IP]", message)
363
+
364
+ # Remove port numbers in URLs
365
+ message = re.sub(r":[0-9]{2,5}(?=/|$|\s)", ":[PORT]", message)
366
+
367
+ # Truncate to reasonable length
368
+ if len(message) > 200:
369
+ message = message[:197] + "..."
370
+
371
+ return message
272
372
 
273
373
 
274
374
  def flush() -> None:
275
375
  """Flush any pending telemetry events."""
276
376
  if not is_telemetry_enabled():
277
377
  return
278
-
378
+
279
379
  try:
280
380
  posthog.flush()
281
381
  except Exception:
@@ -287,9 +387,9 @@ def shutdown() -> None:
287
387
  """Shutdown telemetry and flush pending events."""
288
388
  if not is_telemetry_enabled():
289
389
  return
290
-
390
+
291
391
  try:
292
392
  posthog.shutdown()
293
393
  except Exception:
294
394
  # Ignore shutdown errors
295
- pass
395
+ pass
golf/core/transformer.py CHANGED
@@ -6,21 +6,23 @@ into explicit FastMCP component registrations.
6
6
 
7
7
  import ast
8
8
  from pathlib import Path
9
- from typing import Dict, Any
9
+ from typing import Any
10
10
 
11
11
  from golf.core.parser import ParsedComponent
12
12
 
13
13
 
14
14
  class ImportTransformer(ast.NodeTransformer):
15
15
  """AST transformer for rewriting imports in component files."""
16
-
17
- def __init__(self,
18
- original_path: Path,
19
- target_path: Path,
20
- import_map: Dict[str, str],
21
- project_root: Path):
16
+
17
+ def __init__(
18
+ self,
19
+ original_path: Path,
20
+ target_path: Path,
21
+ import_map: dict[str, str],
22
+ project_root: Path,
23
+ ) -> None:
22
24
  """Initialize the import transformer.
23
-
25
+
24
26
  Args:
25
27
  original_path: Path to the original file
26
28
  target_path: Path to the target file
@@ -31,39 +33,35 @@ class ImportTransformer(ast.NodeTransformer):
31
33
  self.target_path = target_path
32
34
  self.import_map = import_map
33
35
  self.project_root = project_root
34
-
36
+
35
37
  def visit_Import(self, node: ast.Import) -> Any:
36
38
  """Transform import statements."""
37
39
  return node
38
-
40
+
39
41
  def visit_ImportFrom(self, node: ast.ImportFrom) -> Any:
40
42
  """Transform import from statements."""
41
43
  if node.module is None:
42
44
  return node
43
-
45
+
44
46
  # Handle relative imports
45
47
  if node.level > 0:
46
48
  # Calculate the source module path
47
49
  source_dir = self.original_path.parent
48
50
  for _ in range(node.level - 1):
49
51
  source_dir = source_dir.parent
50
-
52
+
51
53
  if node.module:
52
54
  source_module = source_dir / node.module.replace(".", "/")
53
55
  else:
54
56
  source_module = source_dir
55
-
57
+
56
58
  # Check if this is a common module import
57
59
  source_str = str(source_module.relative_to(self.project_root))
58
60
  if source_str in self.import_map:
59
61
  # Replace with absolute import
60
62
  new_module = self.import_map[source_str]
61
- return ast.ImportFrom(
62
- module=new_module,
63
- names=node.names,
64
- level=0
65
- )
66
-
63
+ return ast.ImportFrom(module=new_module, names=node.names, level=0)
64
+
67
65
  return node
68
66
 
69
67
 
@@ -71,18 +69,18 @@ def transform_component(
71
69
  component: ParsedComponent,
72
70
  output_file: Path,
73
71
  project_path: Path,
74
- import_map: Dict[str, str],
72
+ import_map: dict[str, str],
75
73
  source_file: Path = None,
76
74
  ) -> str:
77
75
  """Transform a GolfMCP component into a standalone FastMCP component.
78
-
76
+
79
77
  Args:
80
78
  component: Parsed component to transform
81
79
  output_file: Path to write the transformed component to
82
80
  project_path: Path to the project root
83
81
  import_map: Mapping of original module paths to generated paths
84
82
  source_file: Optional path to source file (for common.py files)
85
-
83
+
86
84
  Returns:
87
85
  Generated component code
88
86
  """
@@ -93,43 +91,41 @@ def transform_component(
93
91
  file_path = Path(component.file_path)
94
92
  else:
95
93
  raise ValueError("Either component or source_file must be provided")
96
-
97
- with open(file_path, "r") as f:
94
+
95
+ with open(file_path) as f:
98
96
  source_code = f.read()
99
-
97
+
100
98
  # Parse the source code into an AST
101
99
  tree = ast.parse(source_code)
102
-
100
+
103
101
  # Transform imports
104
- transformer = ImportTransformer(
105
- file_path,
106
- output_file,
107
- import_map,
108
- project_path
109
- )
102
+ transformer = ImportTransformer(file_path, output_file, import_map, project_path)
110
103
  tree = transformer.visit(tree)
111
-
104
+
112
105
  # Get all imports and docstring
113
106
  imports = []
114
107
  docstring = None
115
-
108
+
116
109
  # Find the module docstring if present
117
- if (len(tree.body) > 0 and isinstance(tree.body[0], ast.Expr) and
118
- isinstance(tree.body[0].value, ast.Constant) and
119
- isinstance(tree.body[0].value.value, str)):
110
+ if (
111
+ len(tree.body) > 0
112
+ and isinstance(tree.body[0], ast.Expr)
113
+ and isinstance(tree.body[0].value, ast.Constant)
114
+ and isinstance(tree.body[0].value.value, str)
115
+ ):
120
116
  docstring = tree.body[0].value.value
121
-
117
+
122
118
  # Find imports
123
119
  for node in tree.body:
124
- if isinstance(node, (ast.Import, ast.ImportFrom)):
120
+ if isinstance(node, ast.Import | ast.ImportFrom):
125
121
  imports.append(node)
126
-
127
- # Generate the transformed code
122
+
123
+ # Generate the transformed code
128
124
  transformed_imports = ast.unparse(ast.Module(body=imports, type_ignores=[]))
129
-
125
+
130
126
  # Build full transformed code
131
127
  transformed_code = transformed_imports + "\n\n"
132
-
128
+
133
129
  # Add docstring if present, using proper triple quotes for multi-line docstrings
134
130
  if docstring:
135
131
  # Check if docstring contains newlines
@@ -139,30 +135,32 @@ def transform_component(
139
135
  else:
140
136
  # Use single quotes for single-line docstrings
141
137
  transformed_code += f'"{docstring}"\n\n'
142
-
138
+
143
139
  # Add the rest of the code except imports and the original docstring
144
140
  remaining_nodes = []
145
141
  for node in tree.body:
146
142
  # Skip imports
147
- if isinstance(node, (ast.Import, ast.ImportFrom)):
143
+ if isinstance(node, ast.Import | ast.ImportFrom):
148
144
  continue
149
-
145
+
150
146
  # Skip the original docstring
151
- if (isinstance(node, ast.Expr) and
152
- isinstance(node.value, ast.Constant) and
153
- isinstance(node.value.value, str)):
147
+ if (
148
+ isinstance(node, ast.Expr)
149
+ and isinstance(node.value, ast.Constant)
150
+ and isinstance(node.value.value, str)
151
+ ):
154
152
  continue
155
-
153
+
156
154
  remaining_nodes.append(node)
157
-
155
+
158
156
  remaining_code = ast.unparse(ast.Module(body=remaining_nodes, type_ignores=[]))
159
157
  transformed_code += remaining_code
160
-
158
+
161
159
  # Ensure the directory exists
162
160
  output_file.parent.mkdir(parents=True, exist_ok=True)
163
-
161
+
164
162
  # Write the transformed code to the output file
165
163
  with open(output_file, "w") as f:
166
164
  f.write(transformed_code)
167
-
168
- return transformed_code
165
+
166
+ return transformed_code
golf/examples/__init__.py CHANGED
@@ -1 +0,0 @@
1
-
@@ -7,5 +7,5 @@ from golf.auth import configure_api_key
7
7
  configure_api_key(
8
8
  header_name="Authorization",
9
9
  header_prefix="Bearer ", # Will handle both "Bearer " and "token " prefixes
10
- required=True # Reject requests without a valid API key
11
- )
10
+ required=True, # Reject requests without a valid API key
11
+ )