sqlalchemy-cratedb 0.41.0.dev2__py3-none-any.whl → 0.42.0.dev1__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.
@@ -52,7 +52,22 @@ if SA_VERSION < SA_1_4:
52
52
  monkeypatch_add_exec_driver_sql()
53
53
 
54
54
 
55
+ try:
56
+ from importlib.metadata import PackageNotFoundError, version
57
+ except (ImportError, ModuleNotFoundError): # pragma:nocover
58
+ from importlib_metadata import ( # type: ignore[assignment,no-redef,unused-ignore]
59
+ PackageNotFoundError,
60
+ version,
61
+ )
62
+
63
+ try:
64
+ __version__ = version("sqlalchemy-cratedb")
65
+ except PackageNotFoundError: # pragma: no cover
66
+ __version__ = "unknown"
67
+
68
+
55
69
  __all__ = [
70
+ __version__,
56
71
  dialect,
57
72
  FloatVector,
58
73
  Geopoint,
@@ -317,11 +317,7 @@ def _get_crud_params(compiler, stmt, compile_state, **kw):
317
317
  _column_as_key,
318
318
  kw,
319
319
  )
320
- elif (
321
- not values
322
- and compiler.for_executemany # noqa: W503
323
- and compiler.dialect.supports_default_metavalue # noqa: W503
324
- ):
320
+ elif not values and compiler.for_executemany and compiler.dialect.supports_default_metavalue:
325
321
  # convert an "INSERT DEFAULT VALUES"
326
322
  # into INSERT (firstcol) VALUES (DEFAULT) which can be turned
327
323
  # into an in-place multi values. This supports
@@ -200,6 +200,17 @@ class CrateDDLCompiler(compiler.DDLCompiler):
200
200
  )
201
201
  return
202
202
 
203
+ def visit_create_index(self, create, **kw) -> str:
204
+ """
205
+ CrateDB does not support `CREATE INDEX` statements.
206
+ """
207
+ warnings.warn(
208
+ "CrateDB does not support `CREATE INDEX` statements, "
209
+ "they will be omitted when generating DDL statements.",
210
+ stacklevel=2,
211
+ )
212
+ return "SELECT 1"
213
+
203
214
 
204
215
  class CrateTypeCompiler(compiler.GenericTypeCompiler):
205
216
  def visit_string(self, type_, **kw):
@@ -254,6 +265,36 @@ class CrateTypeCompiler(compiler.GenericTypeCompiler):
254
265
  """
255
266
  return "TIMESTAMP %s" % ((type_.timezone and "WITH" or "WITHOUT") + " TIME ZONE",)
256
267
 
268
+ def visit_BLOB(self, type_, **kw):
269
+ return "STRING"
270
+
271
+ def visit_FLOAT(self, type_, **kw):
272
+ """
273
+ From `sqlalchemy.sql.sqltypes.Float`.
274
+
275
+ When a :paramref:`.Float.precision` is not provided in a
276
+ :class:`_types.Float` type some backend may compile this type as
277
+ an 8 bytes / 64 bit float datatype. To use a 4 bytes / 32 bit float
278
+ datatype a precision <= 24 can usually be provided or the
279
+ :class:`_types.REAL` type can be used.
280
+ This is known to be the case in the PostgreSQL and MSSQL dialects
281
+ that render the type as ``FLOAT`` that's in both an alias of
282
+ ``DOUBLE PRECISION``. Other third party dialects may have similar
283
+ behavior.
284
+ """
285
+ if not type_.precision:
286
+ return "FLOAT"
287
+ elif type_.precision <= 24:
288
+ return "FLOAT"
289
+ else:
290
+ return "DOUBLE"
291
+
292
+ def visit_JSON(self, type_, **kw):
293
+ return "OBJECT"
294
+
295
+ def visit_JSONB(self, type_, **kw):
296
+ return "OBJECT"
297
+
257
298
 
258
299
  class CrateCompiler(compiler.SQLCompiler):
259
300
  def visit_getitem_binary(self, binary, operator, **kw):
@@ -34,46 +34,60 @@ from .compiler import (
34
34
  )
35
35
  from .sa_version import SA_1_4, SA_2_0, SA_VERSION
36
36
  from .type import FloatVector, ObjectArray, ObjectType
37
+ from .type.binary import LargeBinary
37
38
 
39
+ # For SQLAlchemy >= 1.1.
38
40
  TYPES_MAP = {
39
- "boolean": sqltypes.Boolean,
40
- "short": sqltypes.SmallInteger,
41
- "smallint": sqltypes.SmallInteger,
42
- "timestamp": sqltypes.TIMESTAMP(timezone=False),
43
- "timestamp with time zone": sqltypes.TIMESTAMP(timezone=True),
41
+ "boolean": sqltypes.BOOLEAN,
42
+ "short": sqltypes.SMALLINT,
43
+ "smallint": sqltypes.SMALLINT,
44
+ "timestamp": sqltypes.TIMESTAMP,
45
+ "timestamp with time zone": sqltypes.TIMESTAMP(timezone=False),
46
+ "timestamp without time zone": sqltypes.TIMESTAMP(timezone=True),
44
47
  "object": ObjectType,
45
- "integer": sqltypes.Integer,
46
- "long": sqltypes.NUMERIC,
47
- "bigint": sqltypes.NUMERIC,
48
+ "object_array": ObjectArray, # TODO: Can this also be improved to use `sqltypes.ARRAY`?
49
+ "integer": sqltypes.INTEGER,
50
+ "long": sqltypes.BIGINT,
51
+ "bigint": sqltypes.BIGINT,
52
+ "float": sqltypes.FLOAT,
48
53
  "double": sqltypes.DECIMAL,
49
54
  "double precision": sqltypes.DECIMAL,
50
- "object_array": ObjectArray,
51
- "float": sqltypes.Float,
52
- "real": sqltypes.Float,
53
- "string": sqltypes.String,
54
- "text": sqltypes.String,
55
+ "real": sqltypes.REAL,
56
+ "string": sqltypes.VARCHAR,
57
+ "text": sqltypes.VARCHAR,
55
58
  "float_vector": FloatVector,
56
59
  }
57
60
 
58
- # Needed for SQLAlchemy >= 1.1.
59
- # TODO: Dissolve.
61
+ # For SQLAlchemy >= 1.4.
60
62
  try:
61
63
  from sqlalchemy.types import ARRAY
62
64
 
63
- TYPES_MAP["integer_array"] = ARRAY(sqltypes.Integer)
64
- TYPES_MAP["boolean_array"] = ARRAY(sqltypes.Boolean)
65
- TYPES_MAP["short_array"] = ARRAY(sqltypes.SmallInteger)
66
- TYPES_MAP["smallint_array"] = ARRAY(sqltypes.SmallInteger)
65
+ TYPES_MAP["integer_array"] = ARRAY(sqltypes.INTEGER)
66
+ TYPES_MAP["boolean_array"] = ARRAY(sqltypes.BOOLEAN)
67
+ TYPES_MAP["short_array"] = ARRAY(sqltypes.SMALLINT)
68
+ TYPES_MAP["smallint_array"] = ARRAY(sqltypes.SMALLINT)
69
+ TYPES_MAP["timestamp_array"] = ARRAY(sqltypes.TIMESTAMP)
67
70
  TYPES_MAP["timestamp_array"] = ARRAY(sqltypes.TIMESTAMP(timezone=False))
68
71
  TYPES_MAP["timestamp with time zone_array"] = ARRAY(sqltypes.TIMESTAMP(timezone=True))
69
- TYPES_MAP["long_array"] = ARRAY(sqltypes.NUMERIC)
70
- TYPES_MAP["bigint_array"] = ARRAY(sqltypes.NUMERIC)
71
- TYPES_MAP["double_array"] = ARRAY(sqltypes.DECIMAL)
72
- TYPES_MAP["double precision_array"] = ARRAY(sqltypes.DECIMAL)
73
- TYPES_MAP["float_array"] = ARRAY(sqltypes.Float)
74
- TYPES_MAP["real_array"] = ARRAY(sqltypes.Float)
75
- TYPES_MAP["string_array"] = ARRAY(sqltypes.String)
76
- TYPES_MAP["text_array"] = ARRAY(sqltypes.String)
72
+ TYPES_MAP["long_array"] = ARRAY(sqltypes.BIGINT)
73
+ TYPES_MAP["bigint_array"] = ARRAY(sqltypes.BIGINT)
74
+ TYPES_MAP["float_array"] = ARRAY(sqltypes.FLOAT)
75
+ TYPES_MAP["real_array"] = ARRAY(sqltypes.REAL)
76
+ TYPES_MAP["string_array"] = ARRAY(sqltypes.VARCHAR)
77
+ TYPES_MAP["text_array"] = ARRAY(sqltypes.VARCHAR)
78
+ except Exception: # noqa: S110
79
+ pass
80
+
81
+ # For SQLAlchemy >= 2.0.
82
+ try:
83
+ from sqlalchemy.types import DOUBLE, DOUBLE_PRECISION
84
+
85
+ TYPES_MAP["real"] = DOUBLE
86
+ TYPES_MAP["real_array"] = ARRAY(DOUBLE)
87
+ TYPES_MAP["double"] = DOUBLE
88
+ TYPES_MAP["double_array"] = ARRAY(DOUBLE)
89
+ TYPES_MAP["double precision"] = DOUBLE_PRECISION
90
+ TYPES_MAP["double precision_array"] = ARRAY(DOUBLE_PRECISION)
77
91
  except Exception: # noqa: S110
78
92
  pass
79
93
 
@@ -158,6 +172,7 @@ colspecs = {
158
172
  sqltypes.Date: Date,
159
173
  sqltypes.DateTime: DateTime,
160
174
  sqltypes.TIMESTAMP: DateTime,
175
+ sqltypes.LargeBinary: LargeBinary,
161
176
  }
162
177
 
163
178
 
@@ -206,6 +221,15 @@ class CrateDialect(default.DefaultDialect):
206
221
  # start with _. Adding it here causes sqlalchemy to quote such columns.
207
222
  self.identifier_preparer.illegal_initial_characters.add("_")
208
223
 
224
+ def get_isolation_level_values(self, dbapi_conn):
225
+ return ()
226
+
227
+ def set_isolation_level(self, dbapi_connection, level):
228
+ pass
229
+
230
+ def get_isolation_level(self, dbapi_connection):
231
+ return "NONE"
232
+
209
233
  def initialize(self, connection):
210
234
  # get lowest server version
211
235
  self.server_version_info = self._get_server_version_info(connection)
@@ -228,8 +252,12 @@ class CrateDialect(default.DefaultDialect):
228
252
  servers = to_list(server)
229
253
  if servers:
230
254
  use_ssl = asbool(kwargs.pop("ssl", False))
231
- if use_ssl:
255
+ # TODO: Switch to the canonical default `sslmode=prefer` later.
256
+ sslmode = kwargs.pop("sslmode", "disable")
257
+ if use_ssl or sslmode in ["allow", "prefer", "require", "verify-ca", "verify-full"]:
232
258
  servers = ["https://" + server for server in servers]
259
+ if sslmode == "require":
260
+ kwargs["verify_ssl_cert"] = False
233
261
  return self.dbapi.connect(servers=servers, **kwargs)
234
262
  return self.dbapi.connect(**kwargs)
235
263
 
@@ -1,4 +1,5 @@
1
1
  from .array import ObjectArray
2
+ from .binary import LargeBinary
2
3
  from .geo import Geopoint, Geoshape
3
4
  from .object import ObjectType
4
5
  from .vector import FloatVector, knn_match
@@ -6,6 +7,7 @@ from .vector import FloatVector, knn_match
6
7
  __all__ = [
7
8
  Geopoint,
8
9
  Geoshape,
10
+ LargeBinary,
9
11
  ObjectArray,
10
12
  ObjectType,
11
13
  FloatVector,
@@ -96,6 +96,8 @@ class Any(expression.ColumnElement):
96
96
  self.operator = operator
97
97
 
98
98
 
99
+ # TODO: Should this be inherited from PostgreSQL's
100
+ # `ARRAY`, in order to improve type checking?
99
101
  class _ObjectArray(sqltypes.UserDefinedType):
100
102
  cache_ok = True
101
103
 
@@ -139,5 +141,8 @@ class _ObjectArray(sqltypes.UserDefinedType):
139
141
  def get_col_spec(self, **kws):
140
142
  return "ARRAY(OBJECT)"
141
143
 
144
+ def as_generic(self, **kwargs):
145
+ return sqltypes.ARRAY
146
+
142
147
 
143
148
  ObjectArray = MutableList.as_mutable(_ObjectArray)
@@ -0,0 +1,44 @@
1
+ import base64
2
+
3
+ import sqlalchemy as sa
4
+
5
+
6
+ class LargeBinary(sa.String):
7
+ """A type for large binary byte data.
8
+
9
+ The :class:`.LargeBinary` type corresponds to a large and/or unlengthed
10
+ binary type for the target platform, such as BLOB on MySQL and BYTEA for
11
+ PostgreSQL. It also handles the necessary conversions for the DBAPI.
12
+
13
+ """
14
+
15
+ __visit_name__ = "large_binary"
16
+
17
+ def bind_processor(self, dialect):
18
+ if dialect.dbapi is None:
19
+ return None
20
+
21
+ # TODO: DBAPIBinary = dialect.dbapi.Binary
22
+
23
+ def process(value):
24
+ if value is not None:
25
+ # TODO: return DBAPIBinary(value)
26
+ return base64.b64encode(value).decode()
27
+ else:
28
+ return None
29
+
30
+ return process
31
+
32
+ # Python 3 has native bytes() type
33
+ # both sqlite3 and pg8000 seem to return it,
34
+ # psycopg2 as of 2.5 returns 'memoryview'
35
+ def result_processor(self, dialect, coltype):
36
+ if dialect.returns_native_bytes:
37
+ return None
38
+
39
+ def process(value):
40
+ if value is not None:
41
+ return base64.b64decode(value)
42
+ return value
43
+
44
+ return process
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: sqlalchemy-cratedb
3
- Version: 0.41.0.dev2
3
+ Version: 0.42.0.dev1
4
4
  Summary: SQLAlchemy dialect for CrateDB.
5
5
  Author-email: "Crate.io" <office@crate.io>
6
6
  License: Apache License 2.0
@@ -64,19 +64,20 @@ Description-Content-Type: text/markdown
64
64
  License-File: LICENSE
65
65
  License-File: NOTICE
66
66
  Requires-Dist: backports.zoneinfo<1; python_version < "3.9"
67
- Requires-Dist: crate<3,>=2.0.0.dev6
67
+ Requires-Dist: crate<3,>=2
68
68
  Requires-Dist: geojson<4,>=2.5
69
+ Requires-Dist: importlib-metadata; python_version < "3.8"
69
70
  Requires-Dist: importlib-resources; python_version < "3.9"
70
71
  Requires-Dist: sqlalchemy<2.1,>=1
71
- Requires-Dist: verlib2==0.2
72
+ Requires-Dist: verlib2<0.4
72
73
  Provides-Extra: all
73
74
  Requires-Dist: sqlalchemy-cratedb[vector]; extra == "all"
74
75
  Provides-Extra: develop
75
- Requires-Dist: mypy<1.15; extra == "develop"
76
- Requires-Dist: poethepoet<0.33; extra == "develop"
77
- Requires-Dist: pyproject-fmt<2.6; extra == "develop"
78
- Requires-Dist: ruff<0.10; extra == "develop"
79
- Requires-Dist: validate-pyproject<0.24; extra == "develop"
76
+ Requires-Dist: mypy<1.16; extra == "develop"
77
+ Requires-Dist: poethepoet<1; extra == "develop"
78
+ Requires-Dist: pyproject-fmt<3; extra == "develop"
79
+ Requires-Dist: ruff<0.12; extra == "develop"
80
+ Requires-Dist: validate-pyproject<1; extra == "develop"
80
81
  Provides-Extra: doc
81
82
  Requires-Dist: crate-docs-theme>=0.26.5; extra == "doc"
82
83
  Requires-Dist: sphinx<9,>=3.5; extra == "doc"
@@ -85,14 +86,15 @@ Requires-Dist: build<2; extra == "release"
85
86
  Requires-Dist: twine<7; extra == "release"
86
87
  Provides-Extra: test
87
88
  Requires-Dist: cratedb-toolkit[testing]; extra == "test"
88
- Requires-Dist: dask[dataframe]; python_version < "3.13" and extra == "test"
89
- Requires-Dist: pandas<2.3; extra == "test"
89
+ Requires-Dist: dask[dataframe]; extra == "test"
90
+ Requires-Dist: pandas[test]<2.3; extra == "test"
90
91
  Requires-Dist: pueblo>=0.0.7; extra == "test"
91
92
  Requires-Dist: pytest<9; extra == "test"
92
93
  Requires-Dist: pytest-cov<7; extra == "test"
93
94
  Requires-Dist: pytest-mock<4; extra == "test"
94
95
  Provides-Extra: vector
95
96
  Requires-Dist: numpy; extra == "vector"
97
+ Dynamic: license-file
96
98
 
97
99
  # SQLAlchemy dialect for CrateDB
98
100
 
@@ -1,26 +1,27 @@
1
- sqlalchemy_cratedb/__init__.py,sha256=1W1uYA3Ax7HJTQatj6UulvPEmetUICEx_S7C2bmp76Q,2326
2
- sqlalchemy_cratedb/compiler.py,sha256=obD3DivOzy6i-ewMZz2Qaj9I8J7EkPPhFTAESqvtNsA,13144
3
- sqlalchemy_cratedb/dialect.py,sha256=YEp5bYO6rfKM8rSXddaF40J0iz4bH6RurkBmly4tUpo,15037
1
+ sqlalchemy_cratedb/__init__.py,sha256=G_VAyHNGzbjlqWNrxTz6cM_gfKPOQZtQxR8MGzCsfFQ,2748
2
+ sqlalchemy_cratedb/compiler.py,sha256=hbjQDOj3LCBxNeiLJu_a0b9b7LOTRdVl9pHmTjmAFjM,14535
3
+ sqlalchemy_cratedb/dialect.py,sha256=Rm7gAqXlzKovrSipHxytJ-gAyALYp5CwX7PIn3VXw7g,16067
4
4
  sqlalchemy_cratedb/predicate.py,sha256=mUIHwGv9tuNmN_BqRD5LC5Ll3Lxpb4WI_muZqbP6HRs,3568
5
5
  sqlalchemy_cratedb/sa_version.py,sha256=sQtmUQEJcguJRRSlbkVpYcdEzPduA-sUHtmOtWSH8qI,1168
6
6
  sqlalchemy_cratedb/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sqlalchemy_cratedb/compat/api13.py,sha256=cWgQxPuc47tOjWb4wvCAMBrOsrT9xJJffZqmdfyJA24,5619
8
8
  sqlalchemy_cratedb/compat/core10.py,sha256=VDugi9u4wr5tSoxyD8C9kPmxXgtik0avNPUPnSmP4DI,8191
9
- sqlalchemy_cratedb/compat/core14.py,sha256=qtL1UfvT7aZK2TafNi5KiXA1b0nO84PgoBMI0FgnXos,11144
9
+ sqlalchemy_cratedb/compat/core14.py,sha256=cIqt6fSqWNvnOkeoBAO9A-Cmx6ijzxN4S9hY87xltZo,11084
10
10
  sqlalchemy_cratedb/compat/core20.py,sha256=bKeAqSZH0qyK3Mj6qpj8x8IG2hcchVsGUvPW9xS7z_I,14675
11
11
  sqlalchemy_cratedb/support/__init__.py,sha256=zPXmiW2M5-yLZFfKZX78mZiL0T5MzD0IhUNEoRs9NuY,507
12
12
  sqlalchemy_cratedb/support/pandas.py,sha256=yt5re_PmiwEv7nMK1A2xIufXbmxUjWXP_06pH3V2qyg,4397
13
13
  sqlalchemy_cratedb/support/polyfill.py,sha256=yyqlXBvb6FfWpA3ChDF8XoiCImHsMD8NknZe4gTfvI0,5009
14
14
  sqlalchemy_cratedb/support/util.py,sha256=HjZpOy8uTqkMwUrFYsL3JEDgjmAVSqk4zYMJAyNhpEE,2442
15
- sqlalchemy_cratedb/type/__init__.py,sha256=qT3s2Dt83BPp0ZXCv5e1vHod7MKzDjVzSSV8Pcnl9JM,249
16
- sqlalchemy_cratedb/type/array.py,sha256=N5tl_XrrTVhMOqHeDt0q4M0VOcQC_DSAkF3NlGDsXlc,4339
15
+ sqlalchemy_cratedb/type/__init__.py,sha256=v9-2k_Nl1TJjm6L_iZZIFhPQbOQPp9yFbKMNg9IHdIc,298
16
+ sqlalchemy_cratedb/type/array.py,sha256=m8zSmkVbVolp7xgENxsd9cpfokHdnw1nOQbV6_VaxW4,4509
17
+ sqlalchemy_cratedb/type/binary.py,sha256=9LjMYmyxwE1G_HJcTmUQ9CVxuvIAc9SbZlGv--NQi7M,1185
17
18
  sqlalchemy_cratedb/type/geo.py,sha256=8Z81m8wtpb0TBdSy_3v5FeXd0MTGZD3LxVguf9PzCZA,1160
18
19
  sqlalchemy_cratedb/type/object.py,sha256=aAkmr55RYZUbJ2MVUDV1-5eQ7kKuATVy6oy2OFqmibU,3015
19
20
  sqlalchemy_cratedb/type/vector.py,sha256=g8_C-ObDWEBoGz7u5QdljDCRWYNSRVafCAsmWJSxXiE,4746
20
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/LICENSE,sha256=s_w3FXmAYQuatqsgvyYLnGyC_13KOqp3W1DUEXO9RpY,10175
21
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/METADATA,sha256=sRuNMYA5RucWhqZl01MDqpxVWGTgGYk086a4MfJNN94,6589
22
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/NOTICE,sha256=yU9CWOf_XrVU7fpqGgM9tDjppoMyfHHBmFVMiINZk-k,1167
23
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
24
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/entry_points.txt,sha256=c14wyCG3OeM64_DUbI_vLVUXR3e3GhDyO_PCjo6UQMU,57
25
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/top_level.txt,sha256=UjjXz0burl_-2MApzLzffHG_2RXm6KljZvoGJHISMPo,19
26
- sqlalchemy_cratedb-0.41.0.dev2.dist-info/RECORD,,
21
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/licenses/LICENSE,sha256=s_w3FXmAYQuatqsgvyYLnGyC_13KOqp3W1DUEXO9RpY,10175
22
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/licenses/NOTICE,sha256=yU9CWOf_XrVU7fpqGgM9tDjppoMyfHHBmFVMiINZk-k,1167
23
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/METADATA,sha256=Lx2wMVucDppNkGBh_WzI5VhafUVV-ImbWY77GnYoxsI,6629
24
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
25
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/entry_points.txt,sha256=c14wyCG3OeM64_DUbI_vLVUXR3e3GhDyO_PCjo6UQMU,57
26
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/top_level.txt,sha256=UjjXz0burl_-2MApzLzffHG_2RXm6KljZvoGJHISMPo,19
27
+ sqlalchemy_cratedb-0.42.0.dev1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5