spaceforge 1.1.0__py3-none-any.whl → 1.1.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.
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '1.1.0'
32
- __version_tuple__ = version_tuple = (1, 1, 0)
31
+ __version__ = version = '1.1.1'
32
+ __version_tuple__ = version_tuple = (1, 1, 1)
33
33
 
34
34
  __commit_id__ = commit_id = None
spaceforge/plugin.py CHANGED
@@ -2,7 +2,6 @@
2
2
  Base plugin class for Spaceforge framework.
3
3
  """
4
4
 
5
- import inspect
6
5
  import json
7
6
  import logging
8
7
  import os
@@ -162,35 +161,6 @@ class SpaceforgePlugin(ABC):
162
161
  else:
163
162
  self.logger.error(f"API call returned no data: {resp}")
164
163
 
165
- def get_available_hooks(self) -> List[str]:
166
- """
167
- Get list of hook methods available in this plugin.
168
-
169
- Returns:
170
- List of hook method names that are implemented
171
- """
172
- hook_methods = []
173
- for method_name in dir(self):
174
- if not method_name.startswith("_") and method_name != "get_available_hooks":
175
- method = getattr(self, method_name)
176
- if callable(method) and not inspect.isbuiltin(method):
177
- # Check if it's a hook method (not inherited from base class)
178
- if method_name in [
179
- "before_init",
180
- "after_init",
181
- "before_plan",
182
- "after_plan",
183
- "before_apply",
184
- "after_apply",
185
- "before_perform",
186
- "after_perform",
187
- "before_destroy",
188
- "after_destroy",
189
- "after_run",
190
- ]:
191
- hook_methods.append(method_name)
192
- return hook_methods
193
-
194
164
  def run_cli(
195
165
  self, *command: str, expect_code: int = 0, print_output: bool = True
196
166
  ) -> Tuple[int, List[str], List[str]]:
@@ -8,8 +8,6 @@ if command -v {{binary.name}}; then
8
8
  return
9
9
  fi
10
10
 
11
- mkdir -p {{static_binary_directory}}
12
-
13
11
  echo "Installing {{binary.name}}..."
14
12
  mkdir -p {{static_binary_directory}}
15
13
  cd {{static_binary_directory}}
@@ -31,70 +31,3 @@ class TestSpaceforgePluginHooks:
31
31
  assert callable(hook_method)
32
32
  # Should be able to call without error (default implementation is pass)
33
33
  hook_method()
34
-
35
- def test_should_return_all_available_hooks_for_base_class(self) -> None:
36
- """Should detect and return all hook methods defined in base class."""
37
- # Arrange
38
- plugin = SpaceforgePlugin()
39
- expected_hooks = [
40
- "before_init",
41
- "after_init",
42
- "before_plan",
43
- "after_plan",
44
- "before_apply",
45
- "after_apply",
46
- "before_perform",
47
- "after_perform",
48
- "before_destroy",
49
- "after_destroy",
50
- "after_run",
51
- ]
52
-
53
- # Act
54
- hooks = plugin.get_available_hooks()
55
-
56
- # Assert
57
- for expected_hook in expected_hooks:
58
- assert expected_hook in hooks
59
-
60
- def test_should_detect_overridden_hooks_in_custom_plugin(self) -> None:
61
- """Should include all hooks including overridden ones in custom plugins."""
62
-
63
- # Arrange
64
- class TestPluginWithHooks(SpaceforgePlugin):
65
- def after_plan(self) -> None:
66
- pass
67
-
68
- def before_apply(self) -> None:
69
- pass
70
-
71
- def custom_method(self) -> None: # Not a hook
72
- pass
73
-
74
- plugin = TestPluginWithHooks()
75
-
76
- # Act
77
- hooks = plugin.get_available_hooks()
78
-
79
- # Assert
80
- assert "after_plan" in hooks
81
- assert "before_apply" in hooks
82
- assert "custom_method" not in hooks # Not a recognized hook
83
-
84
- # Should still have all the expected hooks from the base class
85
- expected_hooks = [
86
- "before_init",
87
- "after_init",
88
- "before_plan",
89
- "after_plan",
90
- "before_apply",
91
- "after_apply",
92
- "before_perform",
93
- "after_perform",
94
- "before_destroy",
95
- "after_destroy",
96
- "after_run",
97
- ]
98
-
99
- for expected_hook in expected_hooks:
100
- assert expected_hook in hooks
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spaceforge
3
- Version: 1.1.0
3
+ Version: 1.1.1
4
4
  Summary: A Python framework for building Spacelift plugins
5
5
  Home-page: https://github.com/spacelift-io/plugins
6
6
  Author: Spacelift
@@ -63,7 +63,7 @@ pip install spaceforge
63
63
  Create a Python file (e.g., `my_plugin.py`) and inherit from `SpaceforgePlugin`:
64
64
 
65
65
  ```python
66
- from spaceforge import SpaceforgePlugin, Parameter, Variable, Context
66
+ from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary, Policy, Webhook, MountedFile
67
67
  import os
68
68
 
69
69
  class MyPlugin(SpaceforgePlugin):
@@ -71,17 +71,20 @@ class MyPlugin(SpaceforgePlugin):
71
71
  __plugin_name__ = "my-awesome-plugin"
72
72
  __version__ = "1.0.0"
73
73
  __author__ = "Your Name"
74
+ __labels__ = ["security", "monitoring"] # Optional labels for categorization
74
75
 
75
76
  # Define plugin parameters
76
77
  __parameters__ = [
77
78
  Parameter(
78
- name="api_key",
79
+ name="API Key",
80
+ id="api_key", # Optional ID for parameter reference
79
81
  description="API key for external service",
80
82
  required=True,
81
83
  sensitive=True
82
84
  ),
83
85
  Parameter(
84
- name="environment",
86
+ name="Environment",
87
+ id="environment",
85
88
  description="Target environment",
86
89
  required=False,
87
90
  default="production"
@@ -96,12 +99,12 @@ class MyPlugin(SpaceforgePlugin):
96
99
  env=[
97
100
  Variable(
98
101
  key="API_KEY",
99
- value_from_parameter="api_key",
102
+ value_from_parameter="api_key", # Matches parameter id or name
100
103
  sensitive=True
101
104
  ),
102
105
  Variable(
103
106
  key="ENVIRONMENT",
104
- value_from_parameter="environment"
107
+ value_from_parameter="environment" # Matches parameter id or name
105
108
  )
106
109
  ]
107
110
  )
@@ -160,6 +163,15 @@ Override these methods in your plugin to add custom logic:
160
163
 
161
164
  ## Plugin Components
162
165
 
166
+ ### Labels
167
+
168
+ Add optional labels to categorize your plugin:
169
+
170
+ ```python
171
+ class MyPlugin(SpaceforgePlugin):
172
+ __labels__ = ["security", "monitoring", "compliance"]
173
+ ```
174
+
163
175
  ### Parameters
164
176
 
165
177
  Define user-configurable parameters:
@@ -167,21 +179,30 @@ Define user-configurable parameters:
167
179
  ```python
168
180
  __parameters__ = [
169
181
  Parameter(
170
- name="database_url",
182
+ name="Database URL",
183
+ id="database_url", # Optional: used for parameter reference
171
184
  description="Database connection URL",
172
185
  required=True,
173
- sensitive=True,
174
- default="postgresql://localhost:5432/mydb"
186
+ sensitive=True
175
187
  ),
176
188
  Parameter(
177
- name="timeout",
189
+ name="Timeout",
190
+ id="timeout",
178
191
  description="Timeout in seconds",
179
192
  required=False,
180
- default=30
193
+ default="30" # Default values should be strings
181
194
  )
182
195
  ]
183
196
  ```
184
197
 
198
+ **Parameter Notes:**
199
+ - Parameter `name` is displayed in the Spacelift UI
200
+ - Parameter `id` (optional) is used for programmatic reference
201
+ - `value_from_parameter` can reference either the `id` (if present) or the `name`
202
+ - Parameters are made available as environment variables through Variable definitions
203
+ - Default values must be strings
204
+ - Required parameters cannot have default values
205
+
185
206
  ### Contexts
186
207
 
187
208
  Define Spacelift contexts with environment variables and custom hooks:
@@ -191,11 +212,11 @@ __contexts__ = [
191
212
  Context(
192
213
  name_prefix="production",
193
214
  description="Production environment context",
194
- labels={"env": "prod"},
215
+ labels=["env:prod"],
195
216
  env=[
196
217
  Variable(
197
218
  key="DATABASE_URL",
198
- value_from_parameter="database_url",
219
+ value_from_parameter="database_url", # Matches parameter id
199
220
  sensitive=True
200
221
  ),
201
222
  Variable(
@@ -229,6 +250,88 @@ __binaries__ = [
229
250
  ]
230
251
  ```
231
252
 
253
+ **Context Priority System:**
254
+
255
+ Control the execution order of contexts using the `priority` field:
256
+
257
+ ```python
258
+ __contexts__ = [
259
+ Context(
260
+ name_prefix="setup",
261
+ description="Setup context (runs first)",
262
+ priority=0, # Lower numbers run first
263
+ hooks={
264
+ "before_init": ["echo 'Setting up environment'"]
265
+ }
266
+ ),
267
+ Context(
268
+ name_prefix="main",
269
+ description="Main context (runs second)",
270
+ priority=1, # Higher numbers run after lower ones
271
+ hooks={
272
+ "before_init": ["echo 'Main execution'"]
273
+ }
274
+ )
275
+ ]
276
+ ```
277
+
278
+ **Priority Notes:**
279
+ - Default priority is `0`
280
+ - Lower numbers execute first (0, then 1, then 2, etc.)
281
+ - Useful for ensuring setup contexts run before main execution contexts
282
+
283
+ **Binary PATH Management:**
284
+ - When using Python hook methods (e.g., `def before_apply()`), binaries are automatically available in PATH
285
+ - When using raw context hooks, you must manually export the PATH:
286
+
287
+ ```python
288
+ __contexts__ = [
289
+ Context(
290
+ name_prefix="kubectl-setup",
291
+ description="Setup kubectl binary for raw hooks",
292
+ hooks={
293
+ "before_init": [
294
+ 'export PATH="/mnt/workspace/plugins/plugin_binaries:$PATH"',
295
+ "kubectl version"
296
+ ]
297
+ }
298
+ )
299
+ ]
300
+ ```
301
+
302
+ ### Mounted Files
303
+
304
+ Mount file content directly into contexts:
305
+
306
+ ```python
307
+ from spaceforge import MountedFile
308
+
309
+ __contexts__ = [
310
+ Context(
311
+ name_prefix="config",
312
+ description="Context with mounted configuration files",
313
+ mounted_files=[
314
+ MountedFile(
315
+ path="tmp/config.json",
316
+ content='{"environment": "production", "debug": false}',
317
+ sensitive=False
318
+ ),
319
+ MountedFile(
320
+ path="tmp/secret-config.yaml",
321
+ content="api_key: secret-value\nendpoint: https://api.example.com",
322
+ sensitive=True # Marks content as sensitive
323
+ )
324
+ ]
325
+ )
326
+ ]
327
+ ```
328
+
329
+ **MountedFile Notes:**
330
+ - Files are created at the specified path when the context is applied
331
+ - Content is written exactly as provided
332
+ - Use `sensitive=True` for files containing secrets or sensitive data
333
+ - path is from `/mnt/workspace/`. An example would be `tmp/config.json` which would be mounted at `/mnt/workspace/tmp/config.json`
334
+
232
335
  ### Policies
233
336
 
234
337
  Define OPA policies for your plugin:
@@ -237,7 +340,7 @@ Define OPA policies for your plugin:
237
340
  __policies__ = [
238
341
  Policy(
239
342
  name_prefix="security-check",
240
- type="notification",
343
+ type="NOTIFICATION",
241
344
  body="""
242
345
  package spacelift
243
346
 
@@ -245,7 +348,7 @@ webhook[{"endpoint_id": "security-alerts"}] {
245
348
  input.run_updated.run.marked_unsafe == true
246
349
  }
247
350
  """,
248
- labels={"type": "security"}
351
+ labels=["security"]
249
352
  )
250
353
  ]
251
354
  ```
@@ -258,11 +361,9 @@ Define webhooks to trigger external actions:
258
361
  __webhooks__ = [
259
362
  Webhook(
260
363
  name_prefix="security-alerts",
261
- description="Send security alerts to external service",
262
364
  endpoint="https://alerts.example.com/webhook",
263
- secrets=[
264
- Variable(key="amazing", value="secret-value", sensitive=True)
265
- ],
365
+ secretFromParameter="webhook_secret", # Parameter id/name for webhook secret
366
+ labels=["security"]
266
367
  )
267
368
  ]
268
369
  ```
@@ -297,7 +398,7 @@ def before_apply(self):
297
398
 
298
399
  ### Spacelift API Integration
299
400
 
300
- Query the Spacelift GraphQL API (requires `SPACELIFT_API_TOKEN` and `SPACELIFT_DOMAIN`):
401
+ Query the Spacelift GraphQL API (requires `SPACELIFT_API_TOKEN` and `TF_VAR_spacelift_graphql_endpoint`):
301
402
 
302
403
  ```python
303
404
  def after_plan(self):
@@ -317,6 +418,38 @@ def after_plan(self):
317
418
  self.logger.info(f"Stack state: {result['stack']['state']}")
318
419
  ```
319
420
 
421
+ ### User Token Authentication
422
+
423
+ Use user API tokens instead of service tokens for Spacelift API access. This is useful because the token on the run may not have sufficient permissions for certain operations.
424
+
425
+ ```python
426
+ def before_plan(self):
427
+ # Use user API token for authentication
428
+ user_id = os.environ.get('SPACELIFT_USER_ID')
429
+ user_secret = os.environ.get('SPACELIFT_USER_SECRET')
430
+
431
+ if user_id and user_secret:
432
+ self.use_user_token(user_id, user_secret)
433
+
434
+ # Now you can use the API with user permissions
435
+ result = self.query_api("""
436
+ query {
437
+ viewer {
438
+ id
439
+ login
440
+ }
441
+ }
442
+ """)
443
+
444
+ self.logger.info(f"Authenticated as: {result['viewer']['login']}")
445
+ ```
446
+
447
+ **User Token Notes:**
448
+ - Allows plugins to act on behalf of a specific user
449
+ - Useful for operations requiring user-specific permissions
450
+ - User tokens may have different access levels than service tokens
451
+ - Call `use_user_token()` before making API requests
452
+
320
453
  ### Access Plan and State
321
454
 
322
455
  Access Terraform plan and state data:
@@ -353,14 +486,14 @@ def after_plan(self):
353
486
  self.send_markdown(markdown)
354
487
  ```
355
488
 
356
- ### Append to Policy Input
489
+ ### Add to Policy Input
357
490
 
358
- Append custom data to the OPA policy input:
491
+ Add custom data to the OPA policy input:
359
492
 
360
493
  The following example will create input available via `input.third_party_metadata.custom.my_custom_data` in your OPA policies:
361
494
  ```python
362
495
  def after_plan(self):
363
- self.append_policy_input("my_custom_data", {
496
+ self.add_to_policy_input("my_custom_data", {
364
497
  "scan_results": {
365
498
  "passed": True,
366
499
  "issues": []
@@ -389,9 +522,9 @@ spaceforge generate --help
389
522
  ### Test Plugin Hooks
390
523
 
391
524
  ```bash
392
- # Set parameters via environment variables
393
- export SPACEFORGE_PARAM_API_KEY="test-key"
394
- export SPACEFORGE_PARAM_TIMEOUT="60"
525
+ # Set parameters for local testing (parameters are normally provided by Spacelift)
526
+ export API_KEY="test-key"
527
+ export TIMEOUT="60"
395
528
 
396
529
  # Test specific hook
397
530
  spaceforge runner after_plan
@@ -420,8 +553,8 @@ Access Spacelift environment variables in your hooks:
420
553
 
421
554
  ```python
422
555
  def after_plan(self):
423
- run_id = os.environ.get('SPACELIFT_RUN_ID')
424
- stack_id = os.environ.get('SPACELIFT_STACK_ID')
556
+ run_id = os.environ.get('TF_VAR_spacelift_run_id')
557
+ stack_id = os.environ.get('TF_VAR_spacelift_stack_id')
425
558
  self.logger.info(f"Processing run {run_id} for stack {stack_id}")
426
559
  ```
427
560
 
@@ -455,7 +588,7 @@ Here's a complete example of a security scanning plugin:
455
588
  ```python
456
589
  import os
457
590
  import json
458
- from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary
591
+ from spaceforge import SpaceforgePlugin, Parameter, Variable, Context, Binary, Policy, MountedFile
459
592
 
460
593
  class SecurityScannerPlugin(SpaceforgePlugin):
461
594
  __plugin_name__ = "security-scanner"
@@ -474,13 +607,15 @@ class SecurityScannerPlugin(SpaceforgePlugin):
474
607
 
475
608
  __parameters__ = [
476
609
  Parameter(
477
- name="api_token",
610
+ name="API Token",
611
+ id="api_token",
478
612
  description="Security service API token",
479
613
  required=True,
480
614
  sensitive=True
481
615
  ),
482
616
  Parameter(
483
- name="severity_threshold",
617
+ name="Severity Threshold",
618
+ id="severity_threshold",
484
619
  description="Minimum severity level to report",
485
620
  required=False,
486
621
  default="medium"
@@ -581,8 +716,8 @@ Generate and test this plugin:
581
716
  spaceforge generate security_scanner.py
582
717
 
583
718
  # Test locally
584
- export SPACEFORGE_PARAM_API_TOKEN="your-token"
585
- export SPACEFORGE_PARAM_SEVERITY_THRESHOLD="high"
719
+ export API_TOKEN="your-token"
720
+ export SEVERITY_THRESHOLD="high"
586
721
  spaceforge runner after_plan
587
722
  ```
588
723
 
@@ -2,11 +2,11 @@ spaceforge/README.md,sha256=8o1Nuyasb4OxX3E7ZycyducOrR4J19bZcHrLvFeoFNg,7730
2
2
  spaceforge/__init__.py,sha256=TU-vvm15dK1ucixNW0V42eTT72x3_hmKSyxP4MC1Occ,589
3
3
  spaceforge/__main__.py,sha256=c3nAw4WBnHXIcfMlRV6Ja7r87pEhSeK-SAqiSYIasIY,643
4
4
  spaceforge/_version.py,sha256=RP_LfUd4ODnrfwn9nam8wB6bR3lM4VwmoRxK08Tkiiw,2155
5
- spaceforge/_version_scm.py,sha256=ePNVzJOkxR8FY5bezqKQ_fgBRbzH1G7QTaRDHvGQRAY,704
5
+ spaceforge/_version_scm.py,sha256=O2MwkHziz63f5tgvyTDcaX64UwyuPAN_tWOgO5wh1WI,704
6
6
  spaceforge/cls.py,sha256=oYW5t5_xs9ZM6Ne_b4trxCPxLHQrqbqgUeibeM8O4PU,6329
7
7
  spaceforge/conftest.py,sha256=U-xCavCsgRAQXqflIIOMeq9pcGbeqRviUNkEXgZol8g,2141
8
8
  spaceforge/generator.py,sha256=hCxtbOKmrGd7HCCz7HMaiK566kIre5MNxcJEx2TVURM,18430
9
- spaceforge/plugin.py,sha256=nNMus9cfVsazqvZG-buTWhK9CkeROdC5vQdOvc8EUQA,16129
9
+ spaceforge/plugin.py,sha256=Ytm2B7nnJNH231V4gUFY1pBmwVNTpIg3YviUL_Bnf24,14963
10
10
  spaceforge/runner.py,sha256=EUZ98gmOiJ766zOSk7YcTTrLCtHfst1xf3iE2Xu7Tao,3172
11
11
  spaceforge/schema.json,sha256=89IROLVlCj8txGMiLt4Bbuo_2muSxKoZCyaXQ2vuA9c,10869
12
12
  spaceforge/test_cls.py,sha256=nXAgbnFnGdFxrtA7vNXiePjNUASuoYW-lEuQGx9WMGs,468
@@ -17,17 +17,17 @@ spaceforge/test_generator_hooks.py,sha256=2lJs8dYlFb7QehWcYF0O4qg38s5UudEpzJyBi1
17
17
  spaceforge/test_generator_parameters.py,sha256=77az9rcocFny2AC4O2eTzjCW712fR1DBHzGrgBKeR4w,1878
18
18
  spaceforge/test_plugin.py,sha256=rZ4Uv_0lIR0qb1GFHkiosGO3WHTWhO7epz8INDxV8Q0,13018
19
19
  spaceforge/test_plugin_file_operations.py,sha256=B0qvIo5EcfKMiHLhBv-hAnpSonn83ojcmJHXasydojA,3782
20
- spaceforge/test_plugin_hooks.py,sha256=rNCZZyd_SDMkm1x3yl5mjQ5tBMGm3YNd1U6h_niWRQs,2962
20
+ spaceforge/test_plugin_hooks.py,sha256=ugaVdzH1-heRJSJN0lu8zoqLcLPC3tg_PzUX98qu9Sw,1038
21
21
  spaceforge/test_plugin_inheritance.py,sha256=WHfvU5s-2GtfcI9-1bHXH7bacr77ikq68V3Z3BBQKvQ,3617
22
22
  spaceforge/test_runner.py,sha256=fDnUf6gEuf1CNMxz6zs3xXvERQsQU3z8qy9KdUc0Wo4,17739
23
23
  spaceforge/test_runner_cli.py,sha256=Sf5X0O9Wc9EhGB5L8SzvlmO7QmgQZQoClSdNYefa-lQ,2299
24
24
  spaceforge/test_runner_core.py,sha256=eNR9YOwJwv7LsMtNQ4WXXMPIW6RE_A7hUp4bCpzz1Rk,3941
25
25
  spaceforge/test_runner_execution.py,sha256=GJhoECdhIY2M3MWcmTrIYfkJd2P5n86zixO3FY38_CQ,5344
26
- spaceforge/templates/binary_install.sh.j2,sha256=znz2G9mm3dOg0iqyxZoj4_ATBEi7HviMETMGRk5D7uM,536
26
+ spaceforge/templates/binary_install.sh.j2,sha256=3vcKUSIpMxYWUuAfAWs3gP3tNr4ZnBIb2ELAqISViZo,498
27
27
  spaceforge/templates/ensure_spaceforge_and_run.sh.j2,sha256=g5BldIEve0IkZ-mCzTXfB_rFvyWqUJqymRRaaMrpp0s,550
28
- spaceforge-1.1.0.dist-info/licenses/LICENSE,sha256=wyljRrfnWY2ggQKkSCg3Nw2hxwPMmupopaKs9Kpgys8,1065
29
- spaceforge-1.1.0.dist-info/METADATA,sha256=pn-bRqlzGeEYFB-mqbrdgfYWOJUvy9pU3lGsbUuR-Ro,16803
30
- spaceforge-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- spaceforge-1.1.0.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
32
- spaceforge-1.1.0.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
33
- spaceforge-1.1.0.dist-info/RECORD,,
28
+ spaceforge-1.1.1.dist-info/licenses/LICENSE,sha256=wyljRrfnWY2ggQKkSCg3Nw2hxwPMmupopaKs9Kpgys8,1065
29
+ spaceforge-1.1.1.dist-info/METADATA,sha256=0Mc6aKdf5jOm3KwfG0mrn7-owmKGZTZ-H5DXAfRojnc,21164
30
+ spaceforge-1.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
+ spaceforge-1.1.1.dist-info/entry_points.txt,sha256=qawuuKBSNTGg-njnQnhxxFldFvXYAPej6bF_f3iyQ48,56
32
+ spaceforge-1.1.1.dist-info/top_level.txt,sha256=eVw-Lw4Th0oHM8Gx1Y8YetyNgbNbMBU00yWs-kwGeSs,11
33
+ spaceforge-1.1.1.dist-info/RECORD,,