wasm-action 0.0.6__py3-none-any.whl → 0.0.8__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.
wasm_action/cli.py CHANGED
@@ -1,8 +1,14 @@
1
+ import os
1
2
  import sys
2
3
  import click
3
4
  import importlib.metadata
5
+ import json
4
6
 
5
7
  from . import lib
8
+ from .warg.crypto import generate_key
9
+ from .wasm import runtime
10
+ from .util import cli_error_handler
11
+
6
12
 
7
13
  @click.group()
8
14
  def cli():
@@ -19,49 +25,120 @@ def version():
19
25
 
20
26
 
21
27
  @cli.command(help="Push to registry")
22
- @click.option('--registry', required=True, help="registry domain name")
23
- @click.option('--package', required=True, help="package spec")
28
+ @click.option('-r', '--registry', required=True, help="registry domain name")
29
+ @click.option('-p', '--package', required=True, help="package spec")
24
30
  @click.option('--path', required=True, help="filename")
25
31
  @click.option('--warg-token', required=False, envvar='WARG_TOKEN', help="warg token (or $WARG_TOKEN)")
26
32
  @click.option('--warg-private-key', required=False, envvar='WARG_PRIVATE_KEY', help="warg private key (or $WARG_PRIVATE_KEY)")
33
+ @cli_error_handler
27
34
  def push(registry, package, path, warg_token, warg_private_key):
28
-
29
- try:
30
-
31
- lib.push_file(
32
- registry=registry,
33
- package=package,
34
- path=path,
35
- warg_token=warg_token,
36
- warg_private_key=warg_private_key,
37
- cli=True,
38
- )
39
-
40
- except Exception as e:
41
- print(e)
42
- sys.exit(1)
35
+ lib.push_file(
36
+ registry=registry,
37
+ package=package,
38
+ path=path,
39
+ warg_token=warg_token,
40
+ warg_private_key=warg_private_key,
41
+ cli=True,
42
+ )
43
43
 
44
44
 
45
45
  @cli.command(help="Pull from registry")
46
- @click.option('--registry', required=True, help="registry domain name")
47
- @click.option('--package', required=True, help="package spec")
46
+ @click.option('-r', '--registry', required=True, help="registry domain name")
47
+ @click.option('-p', '--package', required=True, help="package spec")
48
48
  @click.option('--path', required=False, help="filename")
49
49
  @click.option('--warg-token', required=False, envvar='WARG_TOKEN', help="warg token (or $WARG_TOKEN)")
50
+ @cli_error_handler
50
51
  def pull(registry, package, path=None, warg_token=None):
51
-
52
- try:
53
-
54
- lib.pull_file(
55
- registry=registry,
56
- package=package,
57
- path=path,
58
- warg_token=warg_token,
59
- cli=True,
60
- )
61
-
62
- except Exception as e:
63
- print(e)
64
- sys.exit(1)
52
+ lib.pull_file(
53
+ registry=registry,
54
+ package=package,
55
+ path=path,
56
+ warg_token=warg_token,
57
+ cli=True,
58
+ )
59
+
60
+
61
+ @cli.command(help="Generate private key or read one from stdin")
62
+ @cli_error_handler
63
+ def key():
64
+ """Generate key in json format.
65
+
66
+ Either:
67
+ - generate a new key
68
+ - read a private key from standard input
69
+ """
70
+ if sys.stdin.isatty():
71
+ data = generate_key()
72
+ else:
73
+ # read private key from standard input
74
+ private_key = sys.stdin.read()
75
+ data = generate_key(private_key)
76
+ del data['private']
77
+ print(json.dumps(data, indent=4))
78
+
79
+
80
+ @cli.command('x', help="Run a WebAssembly file")
81
+ @click.argument('filename', required=True)
82
+ @click.argument('func', required=False)
83
+ @click.argument('args', nargs=-1)
84
+ @cli_error_handler
85
+ def run(filename, func, args):
86
+ """Run a WebAssembly file"""
87
+
88
+ print(runtime
89
+ .module_file(filename)
90
+ .instance()
91
+ .function(func)
92
+ .call(*args)
93
+ )
94
+
95
+
96
+ @cli.command('eval', help="""Expression evaluator
97
+
98
+ Expression(s) specified in EXPRESSION or STDIN will be evaluated against the specified WebAssembly module.
99
+ The input must conform to a subset of Python's syntax that includes literals, tuples and function calls.
100
+ The latter are resolved to a valid function present in the exports of the WebAssembly module.
101
+ Example:
102
+ If calc.wasm exports `add` and `mul`, then the following is a valid expression in such context:
103
+ "mul(2, 3), add(mul(4, 5), 3)"
104
+ """)
105
+ @click.argument('filename', required=True)
106
+ @click.argument('expression', required=False)
107
+ @cli_error_handler
108
+ def evaluate(filename, expression):
109
+ """Evaluates an expression against a wasm module instance.
110
+
111
+ Any function calls are intercepted and resolved to a valid function
112
+ from the exports of the wasm module.
113
+
114
+ Expression syntax follows a subset of Python syntax.
115
+
116
+ """
117
+ instance = (runtime
118
+ .module_file(filename)
119
+ .instance()
120
+ )
121
+
122
+ expression = sys.stdin.read() if not sys.stdin.isatty() else expression
123
+
124
+ if expression:
125
+ for result in instance.evaluate(expression):
126
+ print(result)
127
+ return
128
+
129
+ if not sys.stdin.isatty():
130
+ return
131
+
132
+ # repl mode
133
+ import readline
134
+ prompt = "{} >>> ".format(os.path.basename(filename))
135
+ while True:
136
+ expression = input(prompt)
137
+ try:
138
+ for result in instance.evaluate(expression):
139
+ print(result)
140
+ except Exception as e:
141
+ print(e)
65
142
 
66
143
 
67
144
  if __name__ == "__main__":
wasm_action/lib.py CHANGED
@@ -5,7 +5,6 @@ import os
5
5
  import hashlib
6
6
  import base64
7
7
  import json
8
-
9
8
  import click
10
9
  import semver
11
10
  import validators
wasm_action/util.py CHANGED
@@ -2,6 +2,8 @@
2
2
  import os
3
3
  import re
4
4
  import sys
5
+ import datetime
6
+ import functools
5
7
  import requests
6
8
  import semver
7
9
 
@@ -73,6 +75,52 @@ def parse_package(package):
73
75
  if not re.match(r"[\w-]+", name):
74
76
  raise ValueError("invalid package name")
75
77
  if version:
78
+ # evaluate any calver specifiers
79
+ version = CalVer(version).version()
76
80
  # throws on failed validation
77
81
  semver.Version.parse(version)
78
82
  return namespace, name, version
83
+
84
+
85
+ class CalVer:
86
+ """Calendar Versioning according to https://calver.org (zero-padded formats excluded)"""
87
+
88
+ SPEC = {
89
+ 'YYYY': lambda d: str(d.year),
90
+ 'YY': lambda d: str(d.year % 100),
91
+ 'MM': lambda d: str(d.month),
92
+ 'WW': lambda d: str(d.isocalendar().week),
93
+ 'DD': lambda d: str(d.day),
94
+ }
95
+
96
+ def __init__(self, pattern):
97
+ self.pattern = pattern
98
+
99
+ def now(self) -> datetime.datetime:
100
+ if not hasattr(datetime, 'UTC'):
101
+ # 3.10
102
+ return datetime.datetime.utcnow()
103
+ return datetime.datetime.now(datetime.UTC)
104
+
105
+ def version(self, at: datetime.datetime=None):
106
+ when = at or self.now()
107
+ return ".".join([
108
+ self.SPEC[part.upper()](when)
109
+ if part.upper() in self.SPEC else part
110
+ for part in self.pattern.split('.')
111
+ ])
112
+
113
+
114
+ class cli_error_handler(object):
115
+ """Decorator for CLI error handling"""
116
+
117
+ def __init__(self, func):
118
+ self.func = func
119
+ functools.update_wrapper(self, func)
120
+
121
+ def __call__(self, *args, **kwargs):
122
+ try:
123
+ return self.func(*args, **kwargs)
124
+ except Exception as e:
125
+ # print error and return 1
126
+ sys.exit(e)
@@ -60,11 +60,14 @@ def warg_push(registry, warg_url, namespace, name, version, content_bytes, warg_
60
60
 
61
61
  # state: sourcing -> upload sources
62
62
  elif state == 'sourcing':
63
+ time.sleep(1)
63
64
  if 'missingContent' in res:
64
65
  for _, data in res['missingContent'].items():
65
66
  if 'upload' in data:
66
67
  for upload in data['upload']:
67
- requests.put(upload['url'], data=content_bytes)
68
+ r = requests.put(upload['url'], data=content_bytes)
69
+ if r.status_code not in (200, 201):
70
+ print(r.status_code, r.content)
68
71
 
69
72
  # state: rejected -> error
70
73
  elif state == 'rejected':
@@ -38,9 +38,14 @@ class PrivateKey:
38
38
  """
39
39
  Decode a key in `<algo>:<base64>` format.
40
40
  """
41
- if ':' not in text:
41
+ text = (text or '').strip()
42
+ if not text or ':' not in text:
42
43
  raise ValueError('required format: <algo>:<base64>')
43
44
 
45
+ # better interplay with jq, pbcopy, and pbpaste
46
+ if len(text)>1 and text.startswith('"') and text.endswith('"'):
47
+ text = text[1:-1]
48
+
44
49
  algo, key_b64 = text.split(':', 1)
45
50
 
46
51
  curve = cls.CURVES.get(algo)
@@ -48,7 +53,9 @@ class PrivateKey:
48
53
  raise ValueError('algorithm not supported: {}'.format(algo))
49
54
 
50
55
  key_bytes = base64.b64decode(key_b64)
51
- # todo: compare bytes length with curve key length
56
+ # compare bytes length with curve key size
57
+ if len(key_bytes) * 8 != curve.key_size:
58
+ raise ValueError('key size mismatch')
52
59
 
53
60
  key_int = int.from_bytes(key_bytes, byteorder='big')
54
61
  key = ec.derive_private_key(key_int, curve=curve)
@@ -134,3 +141,17 @@ class PublicKey:
134
141
  def fingerprint(self):
135
142
  """Used as Key ID"""
136
143
  return "sha256:{}".format(hashlib.sha256(self.canonical().encode('ascii')).hexdigest())
144
+
145
+
146
+ def generate_key(private_key:str=None):
147
+ """Generate key-pair."""
148
+ if private_key:
149
+ private = PrivateKey.load(private_key)
150
+ else:
151
+ private = PrivateKey.generate()
152
+ public = private.public_key()
153
+ return {
154
+ "private": private.canonical(),
155
+ "public": public.canonical(),
156
+ "id": public.fingerprint(),
157
+ }
File without changes
@@ -0,0 +1,96 @@
1
+ """
2
+ Expression evaluator.
3
+
4
+ Expressions are evaluated against a WebAssembly module instance.
5
+ Expressions allow to combine zero or more function invocations into one go.
6
+ An expression may consist of the following constituents:
7
+ * Literals
8
+ Simple types that can be represented in WA types.
9
+ 1, 0x23, true, false, 3.14
10
+ * Function invocations
11
+ Refer to functions that must be present in the exports section (what about allowing to call imports?).
12
+ sum(2, 3)
13
+ * Tuples for structure and multiple instructions.
14
+ Tuples - allow multiple invocations in one.
15
+
16
+
17
+ Example:
18
+ Suppose the file calculator.wasm provides the following exports:
19
+ - sum, mul
20
+ Then the following expressions can be constructed:
21
+ sum(2, 3)
22
+ sum(2, mul(3, 5))
23
+ sum(1, sum(2, sum(3, sum(4, 5))))
24
+ (1, sum(2, 3)) -> (1, 5)
25
+
26
+ Implementation takes a shortcut by using Python's ast module.
27
+ """
28
+
29
+ import ast
30
+
31
+ ALLOWED_SYNTAX = {
32
+ ast.Module,
33
+ ast.Expr,
34
+ ast.Constant,
35
+ ast.Tuple,
36
+ ast.Call,
37
+ ast.Name,
38
+ }
39
+
40
+
41
+ def parse(value: str) -> ast.AST:
42
+ """Parse an expression into an ast syntax tree."""
43
+ node = ast.parse(value)
44
+ for x in ast.walk(node):
45
+ if isinstance(x, ast.Load):
46
+ # ignored
47
+ continue
48
+ if x.__class__ not in ALLOWED_SYNTAX:
49
+ raise SyntaxError("{} not allowed in expression".format(x.__class__.__name__))
50
+ return node
51
+
52
+
53
+ def evaluate(expression, obj):
54
+ node = parse(expression)
55
+ return Evaluator(obj).evaluate(node)
56
+
57
+
58
+ class Evaluator:
59
+
60
+ def __init__(self, obj):
61
+ self.obj= obj
62
+
63
+ def evaluate(self, node: ast.AST) -> object:
64
+ if isinstance(node, ast.Module):
65
+ for sub in node.body:
66
+ yield from self.evaluate(sub)
67
+
68
+ elif isinstance(node, ast.Expr):
69
+ yield self.compute(node)
70
+
71
+ else:
72
+ raise Exception("value not allowed: {}".format(node))
73
+
74
+ def compute(self, node):
75
+ """Evaluate a single expression"""
76
+ if isinstance(node, ast.Expr):
77
+ return self.compute(node.value)
78
+
79
+ elif isinstance(node, ast.Tuple):
80
+ return tuple(self.compute(x) for x in node.elts)
81
+
82
+ elif isinstance(node, ast.Constant):
83
+ return node.value
84
+
85
+ elif isinstance(node, ast.Call):
86
+ assert isinstance(node.func, ast.Name)
87
+ name = node.func.id
88
+ func = getattr(self.obj, name)
89
+ args = [self.compute(x) for x in node.args]
90
+ return func(*args)
91
+
92
+ elif isinstance(node, ast.Name):
93
+ return getattr(self.obj, node.id)
94
+
95
+ else:
96
+ raise Exception("value not allowed: {}".format(node))
@@ -0,0 +1,96 @@
1
+
2
+ import os
3
+ import wasmtime
4
+
5
+ from . import expression
6
+
7
+
8
+ def module_file(filename):
9
+ if not os.path.exists(filename) or not os.path.isfile(filename):
10
+ raise Exception('file not found: {}'.format(filename))
11
+
12
+ with open(filename, 'rb') as f:
13
+ module_bytes = f.read()
14
+ return module(module_bytes)
15
+
16
+
17
+ def module(module_bytes):
18
+ return Module(module_bytes)
19
+
20
+
21
+ class Module:
22
+
23
+ def __init__(self, module_bytes):
24
+ self._store = wasmtime.Store()
25
+ self._module = wasmtime.Module(self._store.engine, module_bytes)
26
+
27
+ def instance(self):
28
+ return Instance(store=self._store, module=self._module)
29
+
30
+
31
+ class Instance:
32
+
33
+ def __init__(self, store, module):
34
+ self._store = store
35
+ self._module = module
36
+ self._instance = None
37
+ self._imports = []
38
+
39
+ def __getattr__(self, name):
40
+ """Ususally called by the evaluator to resolve function names."""
41
+ return self.function(name)
42
+
43
+ def function(self, name):
44
+ if not self._instance:
45
+ self._instance = wasmtime.Instance(self._store, self._module, self._imports)
46
+ exports = self._instance.exports(self._store)
47
+ if name not in exports.keys():
48
+ print('defined functions:', list(exports.keys()))
49
+ assert False, "function not found: {}".format(name)
50
+ return Function(store=self._store, func=exports[name], name=name)
51
+
52
+ def evaluate(self, text):
53
+ """Evaluate an expression against functions exported in the instance."""
54
+ return expression.evaluate(text or '', obj=self)
55
+
56
+
57
+ class Function:
58
+
59
+ _types = {
60
+ "i32": int,
61
+ "i64": int,
62
+ "f32": float,
63
+ "f64": float,
64
+ }
65
+
66
+ def __init__(self, store, func, name):
67
+ self._store = store
68
+ self._func = func
69
+ self._func_type = self._func.type(self._store)
70
+ self.name = name
71
+
72
+ def call(self, *args):
73
+ return self(*args)
74
+
75
+ def __call__(self, *args):
76
+ # convert to expected types before function call
77
+ typed_args = []
78
+ for param, arg in zip(self._func_type.params, args):
79
+ type = self._types.get(str(param))
80
+ arg = type(arg) if type else arg
81
+ typed_args.append(arg)
82
+ try:
83
+ result = self._func(self._store, *typed_args)
84
+ except Exception as e:
85
+ message = "Error calling {}({}): {}".format(
86
+ self.name,
87
+ ", ".join([str(x) for x in typed_args]),
88
+ str(e),
89
+ )
90
+ raise Exception(message)
91
+ return result
92
+
93
+ def __str__(self):
94
+ return "{}({})".format(self.name, ", ".join([
95
+ str(p) for p in self._func_type.params
96
+ ]))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: wasm-action
3
- Version: 0.0.6
3
+ Version: 0.0.8
4
4
  Summary: Interact with WebAssembly registries.
5
5
  Requires-Dist: click>=8.2.1
6
6
  Requires-Dist: cryptography>=45.0.6
@@ -12,6 +12,7 @@ Requires-Dist: requests>=2.32.4
12
12
  Requires-Dist: semver>=3.0.4
13
13
  Requires-Dist: urllib3>=1.25.3
14
14
  Requires-Dist: validators>=0.35.0
15
+ Requires-Dist: wasmtime>=40.0.0
15
16
  Requires-Python: >=3.10
16
17
  Project-URL: Repository, https://github.com/xelato/wasm-action
17
18
  Description-Content-Type: text/markdown
@@ -27,6 +28,10 @@ Description-Content-Type: text/markdown
27
28
  * Supported actions: push, pull
28
29
  * Supports Python 3.10+ on Linux, MacOS and Windows
29
30
 
31
+ #### Planned
32
+ * OCI registry support (a.k.a. Docker registry)
33
+ * Convert between formats (wit/wasm)
34
+
30
35
  ## Usage
31
36
  ### Pull from registry
32
37
  ```
@@ -37,7 +42,7 @@ Description-Content-Type: text/markdown
37
42
  package: component-book:adder
38
43
  ```
39
44
 
40
- To pull a private package define your [token](https://wa.dev/account/credentials/new):
45
+ To pull a private package, define your [token](https://wa.dev/account/credentials/new):
41
46
  ```
42
47
  env:
43
48
  WARG_TOKEN: ${{ secrets.WARG_TOKEN }}
@@ -77,8 +82,27 @@ To pull a private package define your [token](https://wa.dev/account/credentials
77
82
  WARG_PRIVATE_KEY: ${{ secrets.WARG_PRIVATE_KEY }}
78
83
  ```
79
84
 
85
+ ### Key generation
86
+ New [token](https://wa.dev/account/credentials/new) registration and push to wa.dev require generation and configuration of a private/public key pair which can be facilitated with:
87
+ ```
88
+ $ uvx wasm-action key
89
+ {
90
+ "private": "ecdsa-p256:9y5nigLvFp3KZZQtuvN9DchpGIMUB4bwGAtkIoOCla4=",
91
+ "public": "ecdsa-p256:AvspSQWBK65ItTou/uVCi5qC4P+HBCi4R34OIPb3ILRl",
92
+ "id": "sha256:c836bd8a3082f2e8d70bdfa48296e580ab847fcdeadb351f448d03f152d44093"
93
+ }
94
+ ```
95
+ ```
96
+ # use private key to configure in github or save it elsewhere in a secure manner
97
+ $ uvx wasm-action key | jq .private | pbcopy
98
+ ```
99
+ ```
100
+ # use corresponding public key for new token registration at wa.dev
101
+ $ pbpaste | uvx wasm-action key | jq .public
102
+ ```
103
+
80
104
  ## CLI
81
- The tool can be run without installing using [uv](https://docs.astral.sh/uv/).
105
+ The tool can be run without installing, using [uv](https://docs.astral.sh/uv/).
82
106
  ```
83
107
  $ uvx wasm-action --help
84
108
  Usage: wasm-action [OPTIONS] COMMAND [ARGS]...
@@ -87,8 +111,10 @@ Options:
87
111
  --help Show this message and exit.
88
112
 
89
113
  Commands:
90
- pull Pull from a WebAssembly registry
91
- push Push to a WebAssembly registry
114
+ key Generate private key or read one from stdin
115
+ pull Pull from registry
116
+ push Push to registry
117
+ version Print version
92
118
  ```
93
119
  ```
94
120
  $ uvx wasm-action pull --help
@@ -58,16 +58,19 @@ warg_openapi/models/timestamped_checkpoint.py,sha256=HvpqFUAzxb4AlhFOd1FHi8gKYAv
58
58
  warg_openapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  warg_openapi/rest.py,sha256=esTaIvd-RN8XURlnK_ALzgEyd5n1853hJRoAdCe7C3k,14044
60
60
  wasm_action/__init__.py,sha256=77Mz6_mfveQMO90YlCumicDwhzSa0Xvi3xpJ_10EYZ8,190
61
- wasm_action/cli.py,sha256=in720PEYgVbPwGqab8VJJUUcDIA8JzWpzMmQ93A3xVU,1827
62
- wasm_action/lib.py,sha256=AEomnfa4HObgGxgv29vJ7bsV6scw6cSg6yHPgkUy79I,5367
61
+ wasm_action/cli.py,sha256=skrL0WgJhKzsNUUckwh95nl0bSWTFAOQLMDFjm0_zao,4163
62
+ wasm_action/lib.py,sha256=xX66hhbxyxI5GP_k4DfDdViWj0FonqFXBwq8ssAuZo4,5366
63
63
  wasm_action/registry.py,sha256=JfsYJbzysbcgMAG9wf-8bPU-fmSm7wan_aReRXGGaMM,1497
64
- wasm_action/util.py,sha256=TqIMicfNDqtjhTuLLBujpw77Z2ktY9uXmddsztg-Y8E,2247
64
+ wasm_action/util.py,sha256=yxklrEViwjMn4k3f6_ckkqWD1il9R0hJj6qBsP7CrF4,3600
65
65
  wasm_action/warg/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
- wasm_action/warg/actions.py,sha256=5Hnqc6ImS3W4K5osagOfXJDjLEj_g9TSceX4r7CwoDU,4500
66
+ wasm_action/warg/actions.py,sha256=RfP3REOJp0bxuJ3FkkXFjrO1zfWrW5v7T_HOb8-g6mE,4658
67
67
  wasm_action/warg/client.py,sha256=FKKHcTwqAJotN3ZL2bIHJ0ET05YxRQw9ks8IyxDQ6Mo,6389
68
- wasm_action/warg/crypto.py,sha256=BKBM9BmOni1sXY2eVXky9kx371JhiMd-RitaDa188uY,4253
68
+ wasm_action/warg/crypto.py,sha256=eK2Vw1mLFg2wmL6_3jmLHKzo2JK3KpnOz4PRUFZowZY,4906
69
69
  wasm_action/warg/proto.py,sha256=ui6rPfC_5IQQheOAwCxOL--r_tmvN-hc50NlLC5bgn4,6226
70
- wasm_action-0.0.6.dist-info/WHEEL,sha256=XjEbIc5-wIORjWaafhI6vBtlxDBp7S9KiujWF1EM7Ak,79
71
- wasm_action-0.0.6.dist-info/entry_points.txt,sha256=lTdloFNHGTiEbfd1efp0ZQ7p9SpAbzIWYqxk__EAPFE,53
72
- wasm_action-0.0.6.dist-info/METADATA,sha256=FuDPVFACskSA0_XmFdJ9dVFnNTb9HCdsndd-UpzG7-o,4009
73
- wasm_action-0.0.6.dist-info/RECORD,,
70
+ wasm_action/wasm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
+ wasm_action/wasm/expression.py,sha256=0DWJbHKe50oIFkL11VTdkyCiIruQH7pXpG2SsI_Jr3g,2630
72
+ wasm_action/wasm/runtime.py,sha256=vwJjHwGs6Nt8qpNEEHrzeY2gmG8RbZ3VHSXrkzSQOrk,2699
73
+ wasm_action-0.0.8.dist-info/WHEEL,sha256=e_m4S054HL0hyR3CpOk-b7Q7fDX6BuFkgL5OjAExXas,80
74
+ wasm_action-0.0.8.dist-info/entry_points.txt,sha256=lTdloFNHGTiEbfd1efp0ZQ7p9SpAbzIWYqxk__EAPFE,53
75
+ wasm_action-0.0.8.dist-info/METADATA,sha256=_gSivhQlZQp5swDU6MfB80g-zgt7BBMV1zL8BAWfUXI,4921
76
+ wasm_action-0.0.8.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.25
2
+ Generator: uv 0.9.27
3
3
  Root-Is-Purelib: true
4
- Tag: py3-none-any
4
+ Tag: py3-none-any