seamless-database 2.1.0__tar.gz → 2.3.0__tar.gz

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.
Files changed (19) hide show
  1. seamless_database-2.3.0/LICENSE.txt +22 -0
  2. {seamless_database-2.1.0 → seamless_database-2.3.0}/PKG-INFO +55 -6
  3. {seamless_database-2.1.0 → seamless_database-2.3.0}/README.md +52 -5
  4. {seamless_database-2.1.0 → seamless_database-2.3.0}/database.py +145 -129
  5. {seamless_database-2.1.0 → seamless_database-2.3.0}/database_models.py +69 -10
  6. {seamless_database-2.1.0 → seamless_database-2.3.0}/pyproject.toml +1 -1
  7. {seamless_database-2.1.0 → seamless_database-2.3.0}/seamless_database.egg-info/PKG-INFO +55 -6
  8. {seamless_database-2.1.0 → seamless_database-2.3.0}/seamless_database.egg-info/SOURCES.txt +5 -0
  9. seamless_database-2.3.0/tests/test_celljoin_records.py +72 -0
  10. seamless_database-2.3.0/tests/test_contract_hashtype_db.py +154 -0
  11. seamless_database-2.3.0/tests/test_contract_reference_lifecycle_db.py +65 -0
  12. {seamless_database-2.1.0 → seamless_database-2.3.0}/tests/test_execution_records.py +212 -1
  13. seamless_database-2.3.0/tests/test_hash_type_tightening.py +159 -0
  14. {seamless_database-2.1.0 → seamless_database-2.3.0}/seamless_database.egg-info/dependency_links.txt +0 -0
  15. {seamless_database-2.1.0 → seamless_database-2.3.0}/seamless_database.egg-info/entry_points.txt +0 -0
  16. {seamless_database-2.1.0 → seamless_database-2.3.0}/seamless_database.egg-info/requires.txt +0 -0
  17. {seamless_database-2.1.0 → seamless_database-2.3.0}/seamless_database.egg-info/top_level.txt +0 -0
  18. {seamless_database-2.1.0 → seamless_database-2.3.0}/setup.cfg +0 -0
  19. {seamless_database-2.1.0 → seamless_database-2.3.0}/tests/test_random_port.py +0 -0
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Author: Sjoerd de Vries, MBI platform.
4
+ Copyright (c) 2023-2026 CNRS.
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: seamless-database
3
- Version: 2.1.0
3
+ Version: 2.3.0
4
4
  Summary: SQLite-backed metadata database service for Seamless
5
5
  Author: Sjoerd de Vries
6
6
  License-Expression: MIT
@@ -18,8 +18,10 @@ Classifier: Topic :: Scientific/Engineering
18
18
  Classifier: Topic :: Software Development :: Libraries
19
19
  Requires-Python: >=3.10
20
20
  Description-Content-Type: text/markdown
21
+ License-File: LICENSE.txt
21
22
  Requires-Dist: aiohttp
22
23
  Requires-Dist: peewee
24
+ Dynamic: license-file
23
25
 
24
26
  # seamless-database
25
27
 
@@ -35,13 +37,60 @@ The database stores the following kinds of records:
35
37
  |-------|---------|
36
38
  | **Transformation** | Maps a transformation checksum to its result checksum |
37
39
  | **RevTransformation** | Reverse lookup: finds which transformations produced a given result |
38
- | **BufferInfo** | Stores buffer metadata (length, dtype, encoding, etc.) for a checksum |
40
+ | **HashType** | Stores the packed checksum classification word used by new HashType validation and conversion checks |
39
41
  | **SyntacticToSemantic** | Maps between syntactic and semantic checksums per celltype |
40
- | **Expression** | Caches expression evaluation results (input checksum + path + celltype → result checksum) |
41
- | **MetaData** | Stores execution metadata for transformations (executor, environment, timing) |
42
- | **IrreproducibleTransformation** | Records transformations whose results are not reproducible |
42
+ | **Expression** | Caches expression evaluation results keyed by `(input_checksum, path, input_celltype, celltype)` |
43
+ | **MetaData** | Stores a canonical execution record for each successful, non-probe transformation |
44
+ | **IrreproducibleTransformation** | Records transformations whose results are not reproducible; metadata is preserved on migration |
43
45
 
44
- All data is persisted in a single SQLite file (typically `seamless.db`).
46
+ All data is persisted in a single SQLite file (typically `seamless.db`). The current protocol version is **2.3**.
47
+
48
+ ## Expression cache schema
49
+
50
+ Expression `input_celltype` describes the input checksum's interpretation;
51
+ `celltype` describes the produced result. Both are part of the composite cache
52
+ key. Database, jobserver, remote client, and Dask payloads use those same names.
53
+ The unreleased old `celltype`/`target_celltype` keys are not accepted aliases.
54
+ Existing development caches need these statements, in order, or a recreated
55
+ expression cache table:
56
+
57
+ ```sql
58
+ ALTER TABLE expression RENAME COLUMN celltype TO input_celltype;
59
+ ALTER TABLE expression RENAME COLUMN target_celltype TO celltype;
60
+ ```
61
+
62
+ ## Execution records
63
+
64
+ `MetaData` stores one canonical execution record per successful, non-probe transformation, keyed by `tf_checksum`. Records are **write-once**: subsequent calls to a recorded transformation hit the normal `Transformation` cache and do not re-enter the record path.
65
+
66
+ Two record sizes coexist under the same schema:
67
+
68
+ - **Minimal record (default)** — `schema_version`, `tf_checksum`, `result_checksum`, `seamless_version`, `execution_mode`, `remote_target`, `wall_time_seconds`, `cpu_time_user_seconds`, `cpu_time_system_seconds`, `memory_peak_bytes`, `gpu_memory_peak_bytes`. The hot path pays only timing/memory capture and one write.
69
+ - **Full record (`record: true` in `seamless.profile.yaml`)** — adds environment fingerprints (content-addressed `node`, `environment`, `queue` sub-buffer checksums), compilation context, validation snapshots, contract-violation lists, execution envelope (requested cluster/queue/node, scratch/fingertip flags, resolved `__env__`), and per-job freshness/retry/worker fields.
70
+
71
+ The validator on `PUT metadata` checks identity only — record syntax (integer `schema_version`, body `tf_checksum`/`result_checksum` matching the request, sane `checksum_fields` if present) — not the full payload schema. Identical duplicates are idempotent successes; differing bodies are rejected. Once `IrreproducibleTransformation` rows exist for a `tf_checksum`, `PUT metadata` for that checksum is rejected to avoid silently migrating it back into the normal cache.
72
+
73
+ When a normal entry moves to `IrreproducibleTransformation`, its metadata body travels with it unchanged.
74
+
75
+ ### Schema upgrade from legacy
76
+
77
+ The legacy `meta_data` table had two columns (`checksum PRIMARY KEY`, `metadata JSON`). On startup, `seamless-database`:
78
+
79
+ - creates the upgraded table fresh if absent,
80
+ - drops and recreates the legacy table if it is empty,
81
+ - preserves an already-upgraded table on subsequent starts,
82
+ - and **fails loudly** if a non-empty legacy table is present (it must be migrated explicitly).
83
+
84
+ ### Endpoints (request types)
85
+
86
+ - `PUT metadata` — atomically creates missing `Transformation` and `RevTransformation` rows alongside the `MetaData` row. Body: `{type: "metadata", checksum: <tf>, result: <result>, value: <record>}`.
87
+ - `GET metadata` — return the canonical record body for a `tf_checksum`.
88
+ - `GET irreproducible` — return all rows for a `tf_checksum`, optionally filtered by `result`. Each row includes `checksum`, `result`, and `metadata`.
89
+ - `PUT irreproducible` — move a normal entry into `IrreproducibleTransformation`, preserving metadata.
90
+
91
+ The remote-client equivalents are `set_execution_record`, `get_execution_record`, and `get_irreproducible_records` in `seamless-remote`.
92
+
93
+ See the [agent contract](https://github.com/sjdv1982/seamless/blob/main/docs/agent/contracts/execution-records.md) for the full behavioral spec.
45
94
 
46
95
  ## Role in the Seamless ecosystem
47
96
 
@@ -12,13 +12,60 @@ The database stores the following kinds of records:
12
12
  |-------|---------|
13
13
  | **Transformation** | Maps a transformation checksum to its result checksum |
14
14
  | **RevTransformation** | Reverse lookup: finds which transformations produced a given result |
15
- | **BufferInfo** | Stores buffer metadata (length, dtype, encoding, etc.) for a checksum |
15
+ | **HashType** | Stores the packed checksum classification word used by new HashType validation and conversion checks |
16
16
  | **SyntacticToSemantic** | Maps between syntactic and semantic checksums per celltype |
17
- | **Expression** | Caches expression evaluation results (input checksum + path + celltype → result checksum) |
18
- | **MetaData** | Stores execution metadata for transformations (executor, environment, timing) |
19
- | **IrreproducibleTransformation** | Records transformations whose results are not reproducible |
17
+ | **Expression** | Caches expression evaluation results keyed by `(input_checksum, path, input_celltype, celltype)` |
18
+ | **MetaData** | Stores a canonical execution record for each successful, non-probe transformation |
19
+ | **IrreproducibleTransformation** | Records transformations whose results are not reproducible; metadata is preserved on migration |
20
20
 
21
- All data is persisted in a single SQLite file (typically `seamless.db`).
21
+ All data is persisted in a single SQLite file (typically `seamless.db`). The current protocol version is **2.3**.
22
+
23
+ ## Expression cache schema
24
+
25
+ Expression `input_celltype` describes the input checksum's interpretation;
26
+ `celltype` describes the produced result. Both are part of the composite cache
27
+ key. Database, jobserver, remote client, and Dask payloads use those same names.
28
+ The unreleased old `celltype`/`target_celltype` keys are not accepted aliases.
29
+ Existing development caches need these statements, in order, or a recreated
30
+ expression cache table:
31
+
32
+ ```sql
33
+ ALTER TABLE expression RENAME COLUMN celltype TO input_celltype;
34
+ ALTER TABLE expression RENAME COLUMN target_celltype TO celltype;
35
+ ```
36
+
37
+ ## Execution records
38
+
39
+ `MetaData` stores one canonical execution record per successful, non-probe transformation, keyed by `tf_checksum`. Records are **write-once**: subsequent calls to a recorded transformation hit the normal `Transformation` cache and do not re-enter the record path.
40
+
41
+ Two record sizes coexist under the same schema:
42
+
43
+ - **Minimal record (default)** — `schema_version`, `tf_checksum`, `result_checksum`, `seamless_version`, `execution_mode`, `remote_target`, `wall_time_seconds`, `cpu_time_user_seconds`, `cpu_time_system_seconds`, `memory_peak_bytes`, `gpu_memory_peak_bytes`. The hot path pays only timing/memory capture and one write.
44
+ - **Full record (`record: true` in `seamless.profile.yaml`)** — adds environment fingerprints (content-addressed `node`, `environment`, `queue` sub-buffer checksums), compilation context, validation snapshots, contract-violation lists, execution envelope (requested cluster/queue/node, scratch/fingertip flags, resolved `__env__`), and per-job freshness/retry/worker fields.
45
+
46
+ The validator on `PUT metadata` checks identity only — record syntax (integer `schema_version`, body `tf_checksum`/`result_checksum` matching the request, sane `checksum_fields` if present) — not the full payload schema. Identical duplicates are idempotent successes; differing bodies are rejected. Once `IrreproducibleTransformation` rows exist for a `tf_checksum`, `PUT metadata` for that checksum is rejected to avoid silently migrating it back into the normal cache.
47
+
48
+ When a normal entry moves to `IrreproducibleTransformation`, its metadata body travels with it unchanged.
49
+
50
+ ### Schema upgrade from legacy
51
+
52
+ The legacy `meta_data` table had two columns (`checksum PRIMARY KEY`, `metadata JSON`). On startup, `seamless-database`:
53
+
54
+ - creates the upgraded table fresh if absent,
55
+ - drops and recreates the legacy table if it is empty,
56
+ - preserves an already-upgraded table on subsequent starts,
57
+ - and **fails loudly** if a non-empty legacy table is present (it must be migrated explicitly).
58
+
59
+ ### Endpoints (request types)
60
+
61
+ - `PUT metadata` — atomically creates missing `Transformation` and `RevTransformation` rows alongside the `MetaData` row. Body: `{type: "metadata", checksum: <tf>, result: <result>, value: <record>}`.
62
+ - `GET metadata` — return the canonical record body for a `tf_checksum`.
63
+ - `GET irreproducible` — return all rows for a `tf_checksum`, optionally filtered by `result`. Each row includes `checksum`, `result`, and `metadata`.
64
+ - `PUT irreproducible` — move a normal entry into `IrreproducibleTransformation`, preserving metadata.
65
+
66
+ The remote-client equivalents are `set_execution_record`, `get_execution_record`, and `get_irreproducible_records` in `seamless-remote`.
67
+
68
+ See the [agent contract](https://github.com/sjdv1982/seamless/blob/main/docs/agent/contracts/execution-records.md) for the full behavioral spec.
22
69
 
23
70
  ## Role in the Seamless ecosystem
24
71
 
@@ -9,14 +9,16 @@ import socket
9
9
  import sys
10
10
  import time
11
11
  from urllib.parse import quote
12
- from peewee import DoesNotExist
12
+ from peewee import DoesNotExist, IntegrityError
13
13
 
14
14
  from database_models import (
15
15
  db_init,
16
16
  db_atomic,
17
17
  Transformation,
18
18
  RevTransformation,
19
- BufferInfo,
19
+ CellJoin,
20
+ RevCellJoin,
21
+ HashType,
20
22
  SyntacticToSemantic,
21
23
  Expression,
22
24
  MetaData,
@@ -54,80 +56,14 @@ def parse_checksum(checksum, as_bytes=False):
54
56
  raise TypeError(type(checksum))
55
57
 
56
58
 
57
- # from the Seamless code
58
- class SeamlessBufferInfo:
59
- __slots__ = (
60
- "checksum",
61
- "length",
62
- "is_utf8",
63
- "is_json",
64
- "json_type",
65
- "is_json_numeric_array",
66
- "is_json_numeric_scalar",
67
- "is_numpy",
68
- "dtype",
69
- "shape",
70
- "is_seamless_mixed",
71
- "str2text",
72
- "text2str",
73
- "binary2bytes",
74
- "bytes2binary",
75
- "binary2json",
76
- "json2binary",
77
- )
78
-
79
- def __init__(self, checksum, params: dict = {}):
80
- for slot in self.__slots__:
81
- setattr(self, slot, params.get(slot))
82
- if isinstance(checksum, str):
83
- checksum = parse_checksum(checksum)
84
- self.checksum = checksum
85
-
86
- def __setattr__(self, attr, value):
87
- if value is not None:
88
- if attr == "length":
89
- if not isinstance(value, int):
90
- raise TypeError(type(value))
91
- if not value >= 0:
92
- raise ValueError
93
- if attr.startswith("is_"):
94
- if not isinstance(value, bool):
95
- raise TypeError(type(value))
96
- if attr.find("2") > -1 and value is not None:
97
- if isinstance(value, bytes):
98
- value = value.hex()
99
- super().__setattr__(attr, value)
100
-
101
- def __setitem__(self, item, value):
102
- return setattr(self, item, value)
103
-
104
- def __getitem__(self, item):
105
- return getattr(self, item)
106
-
107
- def update(self, other):
108
- if not isinstance(other, SeamlessBufferInfo):
109
- raise TypeError
110
- for attr in self.__slots__:
111
- v = getattr(other, attr)
112
- if v is not None:
113
- setattr(self, attr, v)
114
-
115
- def get(self, attr, default=None):
116
- value = getattr(self, attr)
117
- if value is None:
118
- return default
119
- else:
120
- return value
121
-
122
- def as_dict(self):
123
- result = {}
124
- for attr in self.__slots__:
125
- if attr == "checksum":
126
- continue
127
- v = getattr(self, attr)
128
- if v is not None:
129
- result[attr] = v
130
- return result
59
+ def _valid_hash_type_word(value):
60
+ if not isinstance(value, int):
61
+ return False
62
+ try:
63
+ from seamless.checksum.hash_type import HashType as CoreHashType
64
+ except ImportError:
65
+ return False
66
+ return CoreHashType.is_valid_word(value)
131
67
 
132
68
 
133
69
  def err(*args, **kwargs):
@@ -153,6 +89,12 @@ def _normalize_metadata_value(value):
153
89
  raise TypeError(type(value))
154
90
 
155
91
 
92
+ def _normalize_expression_path_payload(path):
93
+ if not isinstance(path, str):
94
+ raise TypeError(type(path))
95
+ return json.dumps(path)
96
+
97
+
156
98
  def _normalize_freshness_tokens(value):
157
99
  if isinstance(value, dict):
158
100
  return value
@@ -223,6 +165,17 @@ def _ensure_rev_transformation_row(checksum: str, result: str) -> None:
223
165
  RevTransformation.create(checksum=checksum, result=result)
224
166
 
225
167
 
168
+ def _ensure_rev_celljoin_row(checksum: str, celltype: str, result: str) -> None:
169
+ query = RevCellJoin.select().where(
170
+ RevCellJoin.checksum == checksum,
171
+ RevCellJoin.celltype == celltype,
172
+ RevCellJoin.result == result,
173
+ )
174
+ if query.exists():
175
+ return
176
+ RevCellJoin.create(checksum=checksum, celltype=celltype, result=result)
177
+
178
+
226
179
  def _delete_rev_transformation_rows(checksum: str, result: str) -> None:
227
180
  (
228
181
  RevTransformation.delete()
@@ -342,16 +295,18 @@ def build_sqlite_readonly_uri(path: str) -> str:
342
295
 
343
296
  types = (
344
297
  "protocol",
345
- "buffer_info",
298
+ "hash_type",
346
299
  "syntactic_to_semantic",
347
300
  "semantic_to_syntactic",
348
301
  "transformation",
349
302
  "metadata",
350
303
  "bucket_probe",
351
304
  "expression",
305
+ "celljoin",
352
306
  "irreproducible",
353
307
  "rev_expression", # only GET
354
308
  "rev_transformations", # only GET
309
+ "rev_celljoins", # only GET
355
310
  )
356
311
 
357
312
 
@@ -364,7 +319,7 @@ def format_response(response, *, none_as_404=False):
364
319
  else:
365
320
  status = 404
366
321
  response = "ERROR: Unknown key"
367
- elif isinstance(response, (bool, dict, list)):
322
+ elif isinstance(response, (bool, dict, list, int)):
368
323
  response = json.dumps(response)
369
324
  elif not isinstance(response, (str, bytes)):
370
325
  status = 400
@@ -377,7 +332,7 @@ def format_response(response, *, none_as_404=False):
377
332
 
378
333
  class DatabaseServer:
379
334
  future = None
380
- PROTOCOL = ("seamless", "database", "2.2")
335
+ PROTOCOL = ("seamless", "database", "2.4")
381
336
 
382
337
  def __init__(
383
338
  self,
@@ -582,11 +537,11 @@ class DatabaseServer:
582
537
  pass
583
538
 
584
539
  async def _get(self, type_, checksum, request):
585
- if type_ == "buffer_info":
540
+ if type_ == "hash_type":
586
541
  try:
587
- return json.loads(BufferInfo[checksum].buffer_info)
542
+ return HashType[checksum].hash_type
588
543
  except DoesNotExist:
589
- raise DatabaseError("Unknown key") from None
544
+ return None
590
545
 
591
546
  elif type_ == "semantic_to_syntactic":
592
547
  try:
@@ -686,18 +641,18 @@ class DatabaseServer:
686
641
 
687
642
  elif type_ == "expression":
688
643
  try:
644
+ input_celltype = request["input_celltype"]
645
+ path = _normalize_expression_path_payload(request["path"])
689
646
  celltype = request["celltype"]
690
- path = json.dumps(request["path"])
691
- target_celltype = request["target_celltype"]
692
- except KeyError:
647
+ except (KeyError, TypeError):
693
648
  raise DatabaseError("Malformed expression request")
694
649
  result = (
695
650
  Expression.select()
696
651
  .where(
697
652
  Expression.input_checksum == checksum,
698
653
  Expression.path == path,
654
+ Expression.input_celltype == input_celltype,
699
655
  Expression.celltype == celltype,
700
- Expression.target_celltype == target_celltype,
701
656
  )
702
657
  .execute()
703
658
  )
@@ -705,6 +660,20 @@ class DatabaseServer:
705
660
  return None
706
661
  return parse_checksum(result[0].result)
707
662
 
663
+ elif type_ == "celljoin":
664
+ try:
665
+ celltype = request["celltype"]
666
+ except (KeyError, TypeError):
667
+ raise DatabaseError("Malformed celljoin request") from None
668
+ if celltype not in ("mixed", "plain", "deepcell", "deepfolder"):
669
+ raise DatabaseError("Malformed celljoin request")
670
+ row = CellJoin.get_or_none(
671
+ (CellJoin.checksum == checksum) & (CellJoin.celltype == celltype)
672
+ )
673
+ if row is None:
674
+ return None
675
+ return parse_checksum(row.result)
676
+
708
677
  elif type_ == "rev_expression":
709
678
  expressions = (
710
679
  Expression.select()
@@ -720,8 +689,8 @@ class DatabaseServer:
720
689
  expr = {
721
690
  "checksum": expression.input_checksum,
722
691
  "path": json.loads(expression.path),
692
+ "input_celltype": expression.input_celltype,
723
693
  "celltype": expression.celltype,
724
- "target_celltype": expression.target_celltype,
725
694
  "result": checksum,
726
695
  }
727
696
  result.append(expr)
@@ -740,27 +709,35 @@ class DatabaseServer:
740
709
  result = [transformation.checksum for transformation in transformations]
741
710
  return result
742
711
 
712
+ elif type_ == "rev_celljoins":
713
+ rows = (
714
+ RevCellJoin.select()
715
+ .where(RevCellJoin.result == checksum)
716
+ .execute()
717
+ )
718
+ if not rows:
719
+ return None
720
+ return [
721
+ {"checksum": row.checksum, "celltype": row.celltype}
722
+ for row in rows
723
+ ]
724
+
743
725
  else:
744
726
  raise DatabaseError("Unknown request type")
745
727
 
746
728
  async def _put(self, type_, checksum, request):
747
729
 
748
- if type_ == "buffer_info":
730
+ if type_ == "hash_type":
749
731
  try:
750
732
  value = request["value"]
751
- if not isinstance(value, dict):
733
+ if not _valid_hash_type_word(value):
752
734
  raise TypeError
753
- SeamlessBufferInfo(checksum, value)
754
- try:
755
- existing = json.loads(BufferInfo[checksum].buffer_info)
756
- existing.update(value)
757
- value = existing
758
- except DoesNotExist:
759
- pass
760
- value = json.dumps(value, sort_keys=True, indent=2)
761
- except Exception:
762
- raise DatabaseError("Malformed PUT buffer info request") from None
763
- BufferInfo.create(checksum=checksum, buffer_info=value)
735
+ except (KeyError, TypeError):
736
+ raise DatabaseError("Malformed PUT hash_type request") from None
737
+ try:
738
+ HashType.create(checksum=checksum, hash_type=value)
739
+ except IntegrityError:
740
+ return _conflict_response("HashType already exists with different value")
764
741
 
765
742
  elif type_ == "semantic_to_syntactic":
766
743
  try:
@@ -771,9 +748,7 @@ class DatabaseServer:
771
748
  try:
772
749
  celltype, subcelltype = request["celltype"], request["subcelltype"]
773
750
  except KeyError:
774
- raise DatabaseError(
775
- "Malformed PUT semantic-to-syntactic request"
776
- ) from None
751
+ raise DatabaseError("Malformed PUT semantic-to-syntactic request") from None
777
752
  for syntactic_checksum0 in value:
778
753
  syntactic_checksum = parse_checksum(syntactic_checksum0, as_bytes=False)
779
754
  with db_atomic():
@@ -791,35 +766,65 @@ class DatabaseServer:
791
766
  raise DatabaseError(
792
767
  "Malformed PUT transformation result request: value must be a checksum"
793
768
  ) from None
794
- Transformation.create(checksum=checksum, result=value)
795
- RevTransformation.create(checksum=checksum, result=value)
769
+ with db_atomic():
770
+ row = _get_transformation_row(checksum)
771
+ if row is not None and row.result != value:
772
+ return _conflict_response(
773
+ "Transformation already exists with different result"
774
+ )
775
+ if row is None:
776
+ Transformation.create(checksum=checksum, result=value)
777
+ _ensure_rev_transformation_row(checksum, value)
778
+
779
+ elif type_ == "celljoin":
780
+ try:
781
+ value = parse_checksum(request["value"], as_bytes=False)
782
+ celltype = request["celltype"]
783
+ except (KeyError, TypeError, ValueError):
784
+ raise DatabaseError(
785
+ "Malformed PUT celljoin result request"
786
+ ) from None
787
+ if celltype not in ("mixed", "plain", "deepcell", "deepfolder"):
788
+ raise DatabaseError("Malformed PUT celljoin result request")
789
+ with db_atomic():
790
+ row = CellJoin.get_or_none(
791
+ (CellJoin.checksum == checksum)
792
+ & (CellJoin.celltype == celltype)
793
+ )
794
+ if row is not None and row.result != value:
795
+ return _conflict_response(
796
+ "CellJoin already exists with different result"
797
+ )
798
+ if row is None:
799
+ CellJoin.create(
800
+ checksum=checksum, celltype=celltype, result=value
801
+ )
802
+ _ensure_rev_celljoin_row(checksum, celltype, value)
796
803
 
797
804
  elif type_ == "expression":
798
805
  try:
799
806
  value = parse_checksum(request["value"], as_bytes=False)
807
+ input_celltype = request["input_celltype"]
808
+ path = _normalize_expression_path_payload(request["path"])
800
809
  celltype = request["celltype"]
801
- path = json.dumps(request["path"])
802
- target_celltype = request["target_celltype"]
803
- except KeyError:
810
+ except (KeyError, TypeError):
804
811
  raise DatabaseError("Malformed expression request")
805
812
  try:
806
- # assert celltype in celltypes TODO? also for target_celltype
807
- assert len(path) <= 100
808
- if len(request["path"]):
809
- assert celltype in ("mixed", "plain", "binary")
813
+ # assert input_celltype in celltypes TODO? also for celltype
814
+ assert len(input_celltype) <= 20
810
815
  assert len(celltype) <= 20
811
- assert len(target_celltype) <= 20
812
816
  except AssertionError:
813
- raise DatabaseError(
814
- "Malformed expression request (constraint violation)"
817
+ raise DatabaseError("Malformed expression request (constraint violation)")
818
+ try:
819
+ Expression.create(
820
+ input_checksum=checksum,
821
+ path=path,
822
+ input_celltype=input_celltype,
823
+ celltype=celltype,
824
+ result=value,
815
825
  )
816
- Expression.create(
817
- input_checksum=checksum,
818
- path=path,
819
- celltype=celltype,
820
- target_celltype=target_celltype,
821
- result=value,
822
- )
826
+ except IntegrityError:
827
+ return _conflict_response("Expression already exists with different result")
823
828
 
824
829
  elif type_ == "metadata":
825
830
  try:
@@ -867,12 +872,8 @@ class DatabaseServer:
867
872
  try:
868
873
  bucket_kind = request["bucket_kind"]
869
874
  label = request["label"]
870
- bucket_checksum = parse_checksum(
871
- request["bucket_checksum"], as_bytes=False
872
- )
873
- freshness_tokens = _normalize_freshness_tokens(
874
- request["freshness_tokens"]
875
- )
875
+ bucket_checksum = parse_checksum(request["bucket_checksum"], as_bytes=False)
876
+ freshness_tokens = _normalize_freshness_tokens(request["freshness_tokens"])
876
877
  captured_at = request["captured_at"]
877
878
  _validate_bucket_probe_request(
878
879
  bucket_kind,
@@ -908,8 +909,24 @@ class DatabaseServer:
908
909
  result = parse_checksum(request["result"], as_bytes=False)
909
910
  except (KeyError, ValueError):
910
911
  raise DatabaseError("Malformed 'irreproducible' request") from None
912
+ mode = request.get("mode", "manual")
913
+ if mode not in ("manual", "automatic"):
914
+ raise DatabaseError("Malformed irreproducible mode")
911
915
  with db_atomic():
912
916
  tf_row = _get_transformation_row(checksum)
917
+ if mode == "automatic":
918
+ if tf_row is None:
919
+ return web.Response(status=404, text="Transformation not found")
920
+ if parse_checksum(tf_row.result, as_bytes=False) == result:
921
+ return _conflict_response("Reported result is the recorded result")
922
+ if not IrreproducibleTransformation.select().where(
923
+ IrreproducibleTransformation.checksum == checksum,
924
+ IrreproducibleTransformation.result == result,
925
+ ).exists():
926
+ IrreproducibleTransformation.create(
927
+ checksum=checksum, result=result, metadata=""
928
+ )
929
+ return "OK"
913
930
  if tf_row is not None:
914
931
  tf_result = parse_checksum(tf_row.result, as_bytes=False)
915
932
  if tf_result != result:
@@ -1064,4 +1081,3 @@ If it doesn't exist, a new file is created.""",
1064
1081
  BUCKET_KINDS = frozenset(
1065
1082
  ("node", "environment", "node_env", "queue", "queue_node")
1066
1083
  )
1067
-