velocity-python 0.0.85__py3-none-any.whl → 0.0.87__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 velocity-python might be problematic. Click here for more details.

velocity/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
- __version__ = version = "0.0.85"
1
+ __version__ = version = "0.0.87"
2
2
 
3
3
  from . import aws
4
4
  from . import db
5
5
  from . import misc
6
+ from . import app
File without changes
File without changes
velocity/app/orders.py ADDED
@@ -0,0 +1,182 @@
1
+ import datetime
2
+ import support.app
3
+ import pprint
4
+
5
+ engine = support.app.postgres()
6
+ REQUIRED = object()
7
+
8
+
9
+ @engine.transaction
10
+ class Order:
11
+ SCHEMA = {
12
+ "headers": {
13
+ "customer_email": REQUIRED,
14
+ "order_date": REQUIRED,
15
+ "order_type": REQUIRED,
16
+ },
17
+ "lineitems": {
18
+ "sku": REQUIRED,
19
+ "qty": REQUIRED,
20
+ "price": REQUIRED,
21
+ },
22
+ "lineitems_supp": {
23
+ "note": str,
24
+ },
25
+ }
26
+
27
+ DEFAULTS = {
28
+ "headers": {
29
+ "order_date": lambda: datetime.date.today(),
30
+ "effective_date": lambda: datetime.date.today(),
31
+ }
32
+ }
33
+
34
+ def __init__(self, order_id=None):
35
+ self.order_id = order_id
36
+ self.headers = {}
37
+ self.lineitems = {}
38
+ self.lineitems_supp = {}
39
+
40
+ def __repr__(self):
41
+ return f"Order(order_id={self.order_id}, headers={pprint.pformat(self.headers)}, lineitems={pprint.pformat(self.lineitems)}, lineitems_supp={pprint.pformat(self.lineitems_supp)})"
42
+
43
+ def exists(self, tx):
44
+ if not self.order_id:
45
+ raise ValueError("order_id must be set to check existence")
46
+ return tx.table("orders").find(self.order_id)
47
+
48
+ def __bool__(self):
49
+ return bool(self.order_id) and self.exists(engine.transaction())
50
+
51
+ def load(self, tx):
52
+ if not self.order_id:
53
+ raise ValueError("order_id must be set to load an order")
54
+
55
+ order = tx.table("orders").one(self.order_id)
56
+ if not order:
57
+ raise ValueError(f"Order {self.order_id} not found")
58
+
59
+ self.headers = dict(order)
60
+
61
+ items = (
62
+ tx.table("order_lineitems")
63
+ .select(where={"order_id": self.order_id}, orderby="line_number")
64
+ .all()
65
+ )
66
+ for idx, row in enumerate(items):
67
+ self.lineitems[idx] = dict(row)
68
+
69
+ supps = (
70
+ tx.table("order_lineitems_supp")
71
+ .select(where={"order_id": self.order_id}, orderby="line_number")
72
+ .all()
73
+ )
74
+ for idx, row in enumerate(supps):
75
+ self.lineitems_supp[idx] = dict(row)
76
+
77
+ def update_header(self, key, value):
78
+ self.headers[key] = value
79
+
80
+ def add_lineitem(self, data: dict, supp_data: dict = None):
81
+ index = len(self.lineitems)
82
+ self.lineitems[index] = data
83
+ self.lineitems_supp[index] = supp_data or {}
84
+
85
+ def update_lineitem(self, index: int, data: dict = None, supp_data: dict = None):
86
+ if index not in self.lineitems:
87
+ raise IndexError(f"No line item at index {index}")
88
+ if data:
89
+ self.lineitems[index].update(data)
90
+ if supp_data is not None:
91
+ self.lineitems_supp[index].update(supp_data)
92
+
93
+ def delete_lineitem(self, index: int):
94
+ if index not in self.lineitems:
95
+ raise IndexError(f"No line item at index {index}")
96
+ del self.lineitems[index]
97
+ if index in self.lineitems_supp:
98
+ del self.lineitems_supp[index]
99
+ self._reindex_lineitems()
100
+
101
+ def _reindex_lineitems(self):
102
+ """Re-index lineitems and supplemental data after deletion."""
103
+ new_items = {}
104
+ new_supps = {}
105
+ for i, key in enumerate(sorted(self.lineitems)):
106
+ new_items[i] = self.lineitems[key]
107
+ new_supps[i] = self.lineitems_supp.get(key, {})
108
+ self.lineitems = new_items
109
+ self.lineitems_supp = new_supps
110
+
111
+ def _apply_defaults(self):
112
+ for section, defaults in self.DEFAULTS.items():
113
+ target = getattr(self, section)
114
+ for key, default in defaults.items():
115
+ if key not in target:
116
+ target[key] = default() if callable(default) else default
117
+ elif key == "updated_at":
118
+ # Always update updated_at if present
119
+ target[key] = default() if callable(default) else default
120
+
121
+ def _validate(self):
122
+ self._apply_defaults()
123
+
124
+ for key, requirement in self.SCHEMA["headers"].items():
125
+ if requirement is REQUIRED and key not in self.headers:
126
+ raise ValueError(f"Missing required header field: {key}")
127
+ if (
128
+ key in self.headers
129
+ and requirement is not REQUIRED
130
+ and not isinstance(self.headers[key], requirement)
131
+ ):
132
+ raise TypeError(
133
+ f"Header field '{key}' must be of type {requirement.__name__}"
134
+ )
135
+
136
+ for idx, item in self.lineitems.items():
137
+ for key, requirement in self.SCHEMA["lineitems"].items():
138
+ if requirement is REQUIRED and key not in item:
139
+ raise ValueError(f"Line item {idx} missing required field: {key}")
140
+
141
+ for idx, supp in self.lineitems_supp.items():
142
+ for key, expected in self.SCHEMA["lineitems_supp"].items():
143
+ if key in supp and not isinstance(supp[key], expected):
144
+ raise TypeError(
145
+ f"Supplemental field '{key}' in item {idx} must be of type {expected.__name__}"
146
+ )
147
+
148
+ def persist(self, tx):
149
+ self._validate()
150
+
151
+ if self.order_id:
152
+ tx.table("orders").update(self.headers, self.order_id)
153
+ else:
154
+ record = tx.table("orders").new(self.headers)
155
+ self.order_id = record["sys_id"]
156
+
157
+ tx.table("order_lineitems").delete(where={"order_id": self.order_id})
158
+ tx.table("order_lineitems_supp").delete(where={"order_id": self.order_id})
159
+
160
+ for index in sorted(self.lineitems):
161
+ tx.table("order_lineitems").insert(
162
+ {
163
+ "order_id": self.order_id,
164
+ "line_number": index,
165
+ **self.lineitems[index],
166
+ }
167
+ )
168
+ tx.table("order_lineitems_supp").insert(
169
+ {
170
+ "order_id": self.order_id,
171
+ "line_number": index,
172
+ **self.lineitems_supp.get(index, {}),
173
+ }
174
+ )
175
+
176
+ def to_dict(self):
177
+ return {
178
+ "order_id": self.order_id,
179
+ "headers": self.headers,
180
+ "lineitems": self.lineitems,
181
+ "lineitems_supp": self.lineitems_supp,
182
+ }
File without changes
File without changes
@@ -88,7 +88,6 @@ def return_default(
88
88
  if result is None:
89
89
  result = default
90
90
  except func.exceptions:
91
- traceback.print_exc()
92
91
  self.tx.rollback_savepoint(sp, cursor=self.cursor())
93
92
  return default
94
93
  self.tx.release_savepoint(sp, cursor=self.cursor())
@@ -6,6 +6,11 @@ from functools import wraps
6
6
  from velocity.db import exceptions
7
7
  from velocity.db.core.transaction import Transaction
8
8
 
9
+ import logging
10
+
11
+ logger = logging.getLogger("velocity.db.engine")
12
+ logger.setLevel(logging.INFO) # Or DEBUG for more verbosity
13
+
9
14
 
10
15
  class Engine:
11
16
  """
@@ -14,10 +19,11 @@ class Engine:
14
19
 
15
20
  MAX_RETRIES = 100
16
21
 
17
- def __init__(self, driver, config, sql):
22
+ def __init__(self, driver, config, sql, connect_timeout=5):
18
23
  self.__config = config
19
24
  self.__sql = sql
20
25
  self.__driver = driver
26
+ self.__connect_timeout = connect_timeout
21
27
 
22
28
  def __str__(self):
23
29
  return f"[{self.sql.server}] engine({self.config})"
@@ -39,16 +45,52 @@ class Engine:
39
45
  def __connect(self):
40
46
  """
41
47
  Internal connection logic, raising suitable exceptions on error.
48
+ Enforces a connect timeout and handles different config types.
42
49
  """
50
+ server = self.sql.server.lower()
51
+ timeout_key = "timeout" if "sqlite" in server else "connect_timeout"
52
+ timeout_val = self.__connect_timeout
53
+
43
54
  try:
44
55
  if isinstance(self.config, dict):
45
- return self.driver.connect(**self.config)
46
- if isinstance(self.config, (tuple, list)):
47
- return self.driver.connect(*self.config)
48
- if isinstance(self.config, str):
49
- return self.driver.connect(self.config)
50
- raise Exception("Unhandled configuration parameter.")
51
- except:
56
+ config = self.config.copy()
57
+ if timeout_key not in config:
58
+ config[timeout_key] = timeout_val
59
+ logger.info(
60
+ "Connecting to %s with dict config: %s", self.sql.server, config
61
+ )
62
+ return self.driver.connect(**config)
63
+
64
+ elif isinstance(self.config, str):
65
+ conn_str = self.config
66
+ if timeout_key not in conn_str:
67
+ conn_str += f" {timeout_key}={timeout_val}"
68
+ logger.info(
69
+ "Connecting to %s with str config: %s", self.sql.server, conn_str
70
+ )
71
+ return self.driver.connect(conn_str)
72
+
73
+ elif isinstance(self.config, (tuple, list)):
74
+ config_args = list(self.config)
75
+ if config_args and isinstance(config_args[-1], dict):
76
+ if timeout_key not in config_args[-1]:
77
+ config_args[-1][timeout_key] = timeout_val
78
+ else:
79
+ config_args.append({timeout_key: timeout_val})
80
+
81
+ logger.info(
82
+ "Connecting to %s with tuple/list config (with timeout): %s",
83
+ self.sql.server,
84
+ config_args,
85
+ )
86
+ return self.driver.connect(*config_args)
87
+
88
+ else:
89
+ raise TypeError(
90
+ f"Unhandled configuration parameter type: {type(self.config)}"
91
+ )
92
+
93
+ except Exception:
52
94
  self.process_error()
53
95
 
54
96
  def transaction(self, func_or_cls=None):
@@ -307,67 +349,69 @@ class Engine:
307
349
 
308
350
  error_code, error_mesg = self.sql.get_error(e)
309
351
 
352
+ logger.warning(
353
+ "Database error caught. Attempting to transform: code=%s message=%s",
354
+ error_code,
355
+ error_mesg,
356
+ )
357
+
310
358
  if error_code in self.sql.ApplicationErrorCodes:
311
- raise exceptions.DbApplicationError(e)
359
+ raise exceptions.DbApplicationError from None
312
360
  if error_code in self.sql.ColumnMissingErrorCodes:
313
- raise exceptions.DbColumnMissingError(e)
361
+ raise exceptions.DbColumnMissingError from None
314
362
  if error_code in self.sql.TableMissingErrorCodes:
315
- raise exceptions.DbTableMissingError(e)
363
+ raise exceptions.DbTableMissingError from None
316
364
  if error_code in self.sql.DatabaseMissingErrorCodes:
317
- raise exceptions.DbDatabaseMissingError(e)
365
+ raise exceptions.DbDatabaseMissingError from None
318
366
  if error_code in self.sql.ForeignKeyMissingErrorCodes:
319
- raise exceptions.DbForeignKeyMissingError(e)
367
+ raise exceptions.DbForeignKeyMissingError from None
320
368
  if error_code in self.sql.TruncationErrorCodes:
321
- raise exceptions.DbTruncationError(e)
369
+ raise exceptions.DbTruncationError from None
322
370
  if error_code in self.sql.DataIntegrityErrorCodes:
323
- raise exceptions.DbDataIntegrityError(e)
371
+ raise exceptions.DbDataIntegrityError from None
324
372
  if error_code in self.sql.ConnectionErrorCodes:
325
- raise exceptions.DbConnectionError(e)
373
+ raise exceptions.DbConnectionError from None
326
374
  if error_code in self.sql.DuplicateKeyErrorCodes:
327
- raise exceptions.DbDuplicateKeyError(e)
375
+ raise exceptions.DbDuplicateKeyError from None
328
376
  if re.search(r"key \(sys_id\)=\(\d+\) already exists.", msg, re.M):
329
- raise exceptions.DbDuplicateKeyError(e)
377
+ raise exceptions.DbDuplicateKeyError from None
330
378
  if error_code in self.sql.DatabaseObjectExistsErrorCodes:
331
- raise exceptions.DbObjectExistsError(e)
379
+ raise exceptions.DbObjectExistsError from None
332
380
  if error_code in self.sql.LockTimeoutErrorCodes:
333
- raise exceptions.DbLockTimeoutError(e)
381
+ raise exceptions.DbLockTimeoutError from None
334
382
  if error_code in self.sql.RetryTransactionCodes:
335
- raise exceptions.DbRetryTransaction(e)
383
+ raise exceptions.DbRetryTransaction from None
336
384
  if re.findall(r"database.*does not exist", msg, re.M):
337
- raise exceptions.DbDatabaseMissingError(e)
385
+ raise exceptions.DbDatabaseMissingError from None
338
386
  if re.findall(r"no such database", msg, re.M):
339
- raise exceptions.DbDatabaseMissingError(e)
387
+ raise exceptions.DbDatabaseMissingError from None
340
388
  if re.findall(r"already exists", msg, re.M):
341
- raise exceptions.DbObjectExistsError(e)
389
+ raise exceptions.DbObjectExistsError from None
342
390
  if re.findall(r"server closed the connection unexpectedly", msg, re.M):
343
- raise exceptions.DbConnectionError(e)
391
+ raise exceptions.DbConnectionError from None
344
392
  if re.findall(r"no connection to the server", msg, re.M):
345
- raise exceptions.DbConnectionError(e)
393
+ raise exceptions.DbConnectionError from None
346
394
  if re.findall(r"connection timed out", msg, re.M):
347
- raise exceptions.DbConnectionError(e)
395
+ raise exceptions.DbConnectionError from None
348
396
  if re.findall(r"could not connect to server", msg, re.M):
349
- raise exceptions.DbConnectionError(e)
397
+ raise exceptions.DbConnectionError from None
350
398
  if re.findall(r"cannot connect to server", msg, re.M):
351
- raise exceptions.DbConnectionError(e)
399
+ raise exceptions.DbConnectionError from None
352
400
  if re.findall(r"connection already closed", msg, re.M):
353
- raise exceptions.DbConnectionError(e)
401
+ raise exceptions.DbConnectionError from None
354
402
  if re.findall(r"cursor already closed", msg, re.M):
355
- raise exceptions.DbConnectionError(e)
403
+ raise exceptions.DbConnectionError from None
356
404
  if "no such table:" in msg:
357
- raise exceptions.DbTableMissingError(e)
358
-
359
- msg = f"""
360
- Unhandled/Unknown Error in engine.process_error
361
- EXC_TYPE = {type(e)}
362
- EXC_MSG = {str(e).strip()}
363
-
364
- ERROR_CODE = {error_code}
365
- ERROR_MSG = {error_mesg}
366
-
367
- SQL_STMT = {sql_stmt}
368
- SQL_PARAMS = {sql_params}
369
-
370
- {traceback.format_exc()}
371
- """
372
- print(msg)
405
+ raise exceptions.DbTableMissingError from None
406
+
407
+ logger.error(
408
+ "Unhandled/Unknown Error in engine.process_error",
409
+ exc_info=True,
410
+ extra={
411
+ "error_code": error_code,
412
+ "error_msg": error_mesg,
413
+ "sql_stmt": sql_stmt,
414
+ "sql_params": sql_params,
415
+ },
416
+ )
373
417
  raise
velocity/db/core/table.py CHANGED
@@ -286,6 +286,7 @@ class Table:
286
286
  if use_where:
287
287
  return Row(self, where, lock=lock)
288
288
  return Row(self, result[0]["sys_id"], lock=lock)
289
+ one=find
289
290
 
290
291
  @return_default(None)
291
292
  def first(
@@ -319,20 +320,6 @@ class Table:
319
320
  return Row(self, where, lock=lock)
320
321
  return Row(self, results[0]["sys_id"], lock=lock)
321
322
 
322
- @return_default(None)
323
- def one(self, where=None, orderby=None, lock=None, use_where=False):
324
- """
325
- Returns exactly one row matching `where`, or None if none found.
326
- """
327
- if isinstance(where, int):
328
- where = {"sys_id": where}
329
- results = self.select("sys_id", where=where, orderby=orderby).all()
330
- if not results:
331
- return None
332
- if use_where:
333
- return Row(self, where, lock=lock)
334
- return Row(self, results[0]["sys_id"], lock=lock)
335
-
336
323
  def primary_keys(self):
337
324
  """
338
325
  Returns the list of primary key columns for this table.
@@ -661,6 +648,7 @@ class Table:
661
648
  return sql, vals
662
649
  return self.tx.execute(sql, vals, cursor=self.cursor()).one()
663
650
 
651
+ @return_default(0)
664
652
  def delete(self, where, **kwds):
665
653
  """
666
654
  Deletes rows matching `where`. Raises Exception if `where` is falsy.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: velocity-python
3
- Version: 0.0.85
3
+ Version: 0.0.87
4
4
  Summary: A rapid application development library for interfacing with data storage
5
5
  Author-email: Paul Perez <pperez@codeclubs.org>
6
6
  Project-URL: Homepage, https://codeclubs.org/projects/velocity
@@ -1,4 +1,9 @@
1
- velocity/__init__.py,sha256=tTtkpSZbAfGaBwnRjv2rkzNo37CyxNfcIzY_OahBe0M,88
1
+ velocity/__init__.py,sha256=ADiytoslPLwfK6AsSoQz6OBcwmKeojL4CaG-8UVv7Pc,106
2
+ velocity/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ velocity/app/invoices.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ velocity/app/orders.py,sha256=W-HAXEwY8-IFXbKh82HnMeRVZM7P-TWGEQOWtkLIzI4,6298
5
+ velocity/app/payments.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ velocity/app/purchase_orders.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
7
  velocity/aws/__init__.py,sha256=tj9-NliYxRVPYLnnDuA4FMwBHbbH4ed8gtHgwRskNgY,647
3
8
  velocity/aws/amplify.py,sha256=PkFVmskwhm-ajAiwGg0QnSKlg1b_55d9WLdF-3-70aE,15102
4
9
  velocity/aws/handlers/__init__.py,sha256=xnpFZJVlC2uoeeFW4zuPST8wA8ajaQDky5Y6iXZzi3A,172
@@ -10,13 +15,13 @@ velocity/db/__init__.py,sha256=vrn2AFNAKaqTdnPwLFS0OcREcCtzUCOodlmH54U7ADg,200
10
15
  velocity/db/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
16
  velocity/db/core/column.py,sha256=tAr8tL3a2nyaYpNHhGl508FrY_pGZTzyYgjAV5CEBv4,4092
12
17
  velocity/db/core/database.py,sha256=3zNGItklu9tZCKsbx2T2vCcU1so8AL9PPL0DLjvaz6s,3554
13
- velocity/db/core/decorators.py,sha256=ZwwNc6wGx7Qe7xPZGgeHuqqtXEeNqyDXB0M5ROY-40I,4612
14
- velocity/db/core/engine.py,sha256=NVsS9HPu13Lzuz7UHjUdsCRuBe2cYVKwgAVf9SLc1E0,13123
18
+ velocity/db/core/decorators.py,sha256=76Jkr9XptXt8cvcgp1zbHfuL8uHzWy8lwfR29u-DVu4,4574
19
+ velocity/db/core/engine.py,sha256=bDUBlUf_X897hU4Qgwd7mAm8lIoruu1CoC4LNKWK5Xc,15130
15
20
  velocity/db/core/exceptions.py,sha256=MOWyA1mlMe8eWbFkEHK0Lp9czdplpRyqbAn2JfGmMrM,707
16
21
  velocity/db/core/result.py,sha256=OVqoMwlx3CHNNwr-JGWRx5I8u_YX6hlUpecx99UT5nE,6164
17
22
  velocity/db/core/row.py,sha256=aliLYTTFirgJsOvmUsANwJMyxaATuhpGpFJhcu_twwY,6709
18
23
  velocity/db/core/sequence.py,sha256=VMBc0ZjGnOaWTwKW6xMNTdP8rZ2umQ8ml4fHTTwuGq4,3904
19
- velocity/db/core/table.py,sha256=1zI_GgkUCCjRmM30OGiLOgJnnAeJNkriK_mYeV34lC0,35058
24
+ velocity/db/core/table.py,sha256=g2mq7_VzLBtxITAn47BbgMcUoJAy9XVP6ohzScNl_so,34573
20
25
  velocity/db/core/transaction.py,sha256=IQEmrHAjCg6hqxQQOpPLWUbyLXrTjIPGLHHv7P6urKU,6589
21
26
  velocity/db/servers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
27
  velocity/db/servers/mysql.py,sha256=qHwlB_Mg02R7QFjD5QvJCorYYiP50CqEiQyZVl3uYns,20914
@@ -42,8 +47,8 @@ velocity/misc/tools.py,sha256=_bGneHHA_BV-kUonzw5H3hdJ5AOJRCKfzhgpkFbGqIo,1502
42
47
  velocity/misc/conv/__init__.py,sha256=MLYF58QHjzfDSxb1rdnmLnuEQCa3gnhzzZ30CwZVvQo,40
43
48
  velocity/misc/conv/iconv.py,sha256=d4_BucW8HTIkGNurJ7GWrtuptqUf-9t79ObzjJ5N76U,10603
44
49
  velocity/misc/conv/oconv.py,sha256=h5Lo05DqOQnxoD3y6Px_MQP_V-pBbWf8Hkgkb9Xp1jk,6032
45
- velocity_python-0.0.85.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
46
- velocity_python-0.0.85.dist-info/METADATA,sha256=yGWLn5g87dSFiXk7jM5ys0VCykM-SS-BqBkG2qUz8kw,8586
47
- velocity_python-0.0.85.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
48
- velocity_python-0.0.85.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
49
- velocity_python-0.0.85.dist-info/RECORD,,
50
+ velocity_python-0.0.87.dist-info/licenses/LICENSE,sha256=aoN245GG8s9oRUU89KNiGTU4_4OtnNmVi4hQeChg6rM,1076
51
+ velocity_python-0.0.87.dist-info/METADATA,sha256=MDcMH8szrQ4QLhGLftamZ1ALZFir0nstf6k5o_wtLzk,8586
52
+ velocity_python-0.0.87.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ velocity_python-0.0.87.dist-info/top_level.txt,sha256=JW2vJPmodgdgSz7H6yoZvnxF8S3fTMIv-YJWCT1sNW0,9
54
+ velocity_python-0.0.87.dist-info/RECORD,,