dense-arrays 0.2.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 (80) hide show
  1. dense_arrays-0.2.0/CITATION.cff +12 -0
  2. dense_arrays-0.2.0/LICENSE +21 -0
  3. dense_arrays-0.2.0/MANIFEST.in +2 -0
  4. dense_arrays-0.2.0/PKG-INFO +99 -0
  5. dense_arrays-0.2.0/README.md +69 -0
  6. dense_arrays-0.2.0/docs/assets/dense-arrays-banner.png +0 -0
  7. dense_arrays-0.2.0/pyproject.toml +90 -0
  8. dense_arrays-0.2.0/setup.cfg +4 -0
  9. dense_arrays-0.2.0/src/dense_arrays/__init__.py +51 -0
  10. dense_arrays-0.2.0/src/dense_arrays/_record_validation.py +138 -0
  11. dense_arrays-0.2.0/src/dense_arrays/cli.py +202 -0
  12. dense_arrays-0.2.0/src/dense_arrays/constraints.py +159 -0
  13. dense_arrays-0.2.0/src/dense_arrays/errors.py +24 -0
  14. dense_arrays-0.2.0/src/dense_arrays/greedy.py +74 -0
  15. dense_arrays-0.2.0/src/dense_arrays/model.py +328 -0
  16. dense_arrays-0.2.0/src/dense_arrays/optimizer.py +696 -0
  17. dense_arrays-0.2.0/src/dense_arrays/playback/__init__.py +47 -0
  18. dense_arrays-0.2.0/src/dense_arrays/playback/__main__.py +5 -0
  19. dense_arrays-0.2.0/src/dense_arrays/playback/cli.py +128 -0
  20. dense_arrays-0.2.0/src/dense_arrays/playback/duplex_drawing.py +154 -0
  21. dense_arrays-0.2.0/src/dense_arrays/playback/duplex_frames.py +181 -0
  22. dense_arrays-0.2.0/src/dense_arrays/playback/duplex_geometry.py +111 -0
  23. dense_arrays-0.2.0/src/dense_arrays/playback/export.py +200 -0
  24. dense_arrays-0.2.0/src/dense_arrays/playback/frame_schedule.py +93 -0
  25. dense_arrays-0.2.0/src/dense_arrays/playback/gif_writer.py +18 -0
  26. dense_arrays-0.2.0/src/dense_arrays/playback/graph/__init__.py +80 -0
  27. dense_arrays-0.2.0/src/dense_arrays/playback/graph/curves.py +124 -0
  28. dense_arrays-0.2.0/src/dense_arrays/playback/graph/edge_routing.py +93 -0
  29. dense_arrays-0.2.0/src/dense_arrays/playback/graph/geometry.py +102 -0
  30. dense_arrays-0.2.0/src/dense_arrays/playback/graph/isotropic_layout.py +422 -0
  31. dense_arrays-0.2.0/src/dense_arrays/playback/graph/labels.py +283 -0
  32. dense_arrays-0.2.0/src/dense_arrays/playback/graph/layout.py +347 -0
  33. dense_arrays-0.2.0/src/dense_arrays/playback/graph/layout_candidates.py +179 -0
  34. dense_arrays-0.2.0/src/dense_arrays/playback/graph/model.py +163 -0
  35. dense_arrays-0.2.0/src/dense_arrays/playback/graph/obstacles.py +180 -0
  36. dense_arrays-0.2.0/src/dense_arrays/playback/graph/presentation.py +38 -0
  37. dense_arrays-0.2.0/src/dense_arrays/playback/graph/projection.py +107 -0
  38. dense_arrays-0.2.0/src/dense_arrays/playback/graph/routing.py +118 -0
  39. dense_arrays-0.2.0/src/dense_arrays/playback/graph_drawing.py +341 -0
  40. dense_arrays-0.2.0/src/dense_arrays/playback/graph_layout.py +80 -0
  41. dense_arrays-0.2.0/src/dense_arrays/playback/matplotlib_renderer.py +110 -0
  42. dense_arrays-0.2.0/src/dense_arrays/playback/models.py +269 -0
  43. dense_arrays-0.2.0/src/dense_arrays/playback/output.py +89 -0
  44. dense_arrays-0.2.0/src/dense_arrays/playback/positions.py +52 -0
  45. dense_arrays-0.2.0/src/dense_arrays/playback/presentation.py +172 -0
  46. dense_arrays-0.2.0/src/dense_arrays/playback/reconstruction.py +195 -0
  47. dense_arrays-0.2.0/src/dense_arrays/playback/scene_drawing.py +443 -0
  48. dense_arrays-0.2.0/src/dense_arrays/playback/serialization.py +394 -0
  49. dense_arrays-0.2.0/src/dense_arrays/playback/theme.py +152 -0
  50. dense_arrays-0.2.0/src/dense_arrays/playback/timeline.py +55 -0
  51. dense_arrays-0.2.0/src/dense_arrays/playback/typography.py +47 -0
  52. dense_arrays-0.2.0/src/dense_arrays/playback/validation.py +155 -0
  53. dense_arrays-0.2.0/src/dense_arrays/problem.py +111 -0
  54. dense_arrays-0.2.0/src/dense_arrays/realized.py +221 -0
  55. dense_arrays-0.2.0/src/dense_arrays/sequence.py +149 -0
  56. dense_arrays-0.2.0/src/dense_arrays/solution.py +201 -0
  57. dense_arrays-0.2.0/src/dense_arrays.egg-info/PKG-INFO +99 -0
  58. dense_arrays-0.2.0/src/dense_arrays.egg-info/SOURCES.txt +78 -0
  59. dense_arrays-0.2.0/src/dense_arrays.egg-info/dependency_links.txt +1 -0
  60. dense_arrays-0.2.0/src/dense_arrays.egg-info/entry_points.txt +3 -0
  61. dense_arrays-0.2.0/src/dense_arrays.egg-info/requires.txt +19 -0
  62. dense_arrays-0.2.0/src/dense_arrays.egg-info/top_level.txt +1 -0
  63. dense_arrays-0.2.0/tests/test_cli.py +102 -0
  64. dense_arrays-0.2.0/tests/test_core_contracts.py +207 -0
  65. dense_arrays-0.2.0/tests/test_documentation.py +114 -0
  66. dense_arrays-0.2.0/tests/test_greedy.py +69 -0
  67. dense_arrays-0.2.0/tests/test_optimize.py +563 -0
  68. dense_arrays-0.2.0/tests/test_optional_playback_imports.py +39 -0
  69. dense_arrays-0.2.0/tests/test_playback.py +344 -0
  70. dense_arrays-0.2.0/tests/test_playback_cli.py +169 -0
  71. dense_arrays-0.2.0/tests/test_playback_contracts.py +365 -0
  72. dense_arrays-0.2.0/tests/test_playback_curves.py +27 -0
  73. dense_arrays-0.2.0/tests/test_playback_exports.py +364 -0
  74. dense_arrays-0.2.0/tests/test_playback_graph.py +76 -0
  75. dense_arrays-0.2.0/tests/test_playback_output.py +59 -0
  76. dense_arrays-0.2.0/tests/test_playback_presentation.py +225 -0
  77. dense_arrays-0.2.0/tests/test_playback_resting.py +381 -0
  78. dense_arrays-0.2.0/tests/test_playback_typography.py +329 -0
  79. dense_arrays-0.2.0/tests/test_release.py +79 -0
  80. dense_arrays-0.2.0/tests/test_solver_outcomes.py +178 -0
@@ -0,0 +1,12 @@
1
+ cff-version: 1.2.0
2
+ message: Cite Dense Arrays and retain the exact release or source revision used.
3
+ type: software
4
+ title: Dense Arrays
5
+ authors:
6
+ - family-names: Andreani
7
+ given-names: Virgile
8
+ - family-names: South
9
+ given-names: Eric J.
10
+ repository-code: https://github.com/e-south/dense-arrays
11
+ url: https://dunloplab.gitlab.io/dense-arrays
12
+ license: MIT
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Dunlop lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ include CITATION.cff
2
+ include docs/assets/dense-arrays-banner.png
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: dense-arrays
3
+ Version: 0.2.0
4
+ Summary: A library to create densely packed DNA arrays from motifs
5
+ Author-email: Virgile Andreani <andreani@bu.edu>, Eric J South <ericjohnsouth@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/e-south/dense-arrays
8
+ Project-URL: Documentation, https://dunloplab.gitlab.io/dense-arrays
9
+ Project-URL: Issues, https://github.com/e-south/dense-arrays/issues
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: numpy>=1.25.2
14
+ Requires-Dist: ortools>=9.7.2996
15
+ Requires-Dist: rich>=13.7.0
16
+ Requires-Dist: typer>=0.12.0
17
+ Provides-Extra: playback
18
+ Requires-Dist: matplotlib>=3.8.0; extra == "playback"
19
+ Requires-Dist: networkx>=3.6.1; extra == "playback"
20
+ Provides-Extra: docs
21
+ Requires-Dist: mkdocs>=1.6.1; extra == "docs"
22
+ Requires-Dist: mkdocs-material>=9.7.3; extra == "docs"
23
+ Requires-Dist: mkdocstrings[python]>=1.0.3; extra == "docs"
24
+ Provides-Extra: dev
25
+ Requires-Dist: pre-commit>=4.5.1; extra == "dev"
26
+ Requires-Dist: pip-audit>=2.10.0; extra == "dev"
27
+ Requires-Dist: pytest>=9.0.3; extra == "dev"
28
+ Requires-Dist: ruff>=0.14.14; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # ![Dense Arrays — overlapping motifs within a sequence-length limit](https://raw.githubusercontent.com/e-south/dense-arrays/v0.2.0/docs/assets/dense-arrays-banner.png)
32
+
33
+ [![CI](https://github.com/e-south/dense-arrays/actions/workflows/ci.yml/badge.svg)](https://github.com/e-south/dense-arrays/actions/workflows/ci.yml)
34
+ [![docs](https://img.shields.io/badge/docs-gitlab_pages-blue)](https://dunloplab.gitlab.io/dense-arrays)
35
+
36
+ Pack overlapping DNA motifs into a sequence-length limit. Dense Arrays selects
37
+ an arrangement and returns the sequence and each motif's position, so you can
38
+ inspect the overlaps or generate further solutions. Choose single- or
39
+ double-strand placement, require motif groups, or constrain positions.
40
+
41
+ The package also renders saved placements as images and video. [Watch the
42
+ worked playback](https://dunloplab.gitlab.io/dense-arrays/playback/#watch-four-overlapping-motifs)
43
+ to see how four motifs share sequence space.
44
+
45
+ ## First array
46
+
47
+ With Python 3.12 or later and [uv](https://docs.astral.sh/uv/), run from a
48
+ [source checkout](https://github.com/e-south/dense-arrays/blob/main/docs/quickstart.md#install-from-source):
49
+
50
+ ```bash
51
+ uv sync --frozen
52
+ uv run dense-arrays optimize \
53
+ --motif ACGTTGCAAGTCCTGA \
54
+ --motif AGTCCTGATCGTACCG \
55
+ --motif TCGTACCGATGCTTAG \
56
+ --motif ATGCTTAGGACGTTCA \
57
+ --length 40 --strands double
58
+ ```
59
+
60
+ The search considers both strands. One optimal arrangement packs all four
61
+ 16-base motifs into 40 bases:
62
+
63
+ ```text
64
+ ACGTTGCAAGTCCTGATCGTACCGATGCTTAGGACGTTCA
65
+ ```
66
+
67
+ In this orientation, the motifs start at 0, 8, 16, and 24. The solver may return
68
+ the reverse-complement arrangement. The
69
+ [quickstart](https://github.com/e-south/dense-arrays/blob/main/docs/quickstart.md) explains these offsets and shows Python use
70
+ and bounded enumeration of further solutions.
71
+
72
+ ## Documentation
73
+
74
+ - [Create and read your first array](https://github.com/e-south/dense-arrays/blob/main/docs/quickstart.md).
75
+ - [Set positional and regulator constraints](https://github.com/e-south/dense-arrays/blob/main/docs/constraints.md).
76
+ - [Render saved placements as images or video](https://github.com/e-south/dense-arrays/blob/main/docs/playback.md).
77
+ - [Understand the packing method](https://github.com/e-south/dense-arrays/blob/main/docs/method.md).
78
+ - [Look up Python interfaces](https://github.com/e-south/dense-arrays/blob/main/docs/api.md).
79
+ - [Update an existing caller](https://github.com/e-south/dense-arrays/blob/main/docs/migration.md).
80
+
81
+ The [documentation index](https://github.com/e-south/dense-arrays/blob/main/docs/index.md) also routes integrators and
82
+ contributors to the relevant interfaces and checks.
83
+
84
+ ## Citation
85
+
86
+ If you use Dense Arrays in your research, please cite:
87
+
88
+ Andreani V, South EJ, Dunlop MJ (2024). Generating information-dense promoter
89
+ sequences with optimal string packing. *PLOS Computational Biology* 20(7):
90
+ e1012276. [doi:10.1371/journal.pcbi.1012276](https://doi.org/10.1371/journal.pcbi.1012276).
91
+
92
+ Record the package version or commit alongside your results.
93
+
94
+ ## Contribute
95
+
96
+ [Development](https://github.com/e-south/dense-arrays/blob/main/docs/development.md) covers local checks and documentation builds.
97
+ [Report bugs](https://github.com/e-south/dense-arrays/issues) and follow the
98
+ [security policy](https://github.com/e-south/dense-arrays/blob/main/SECURITY.md) for vulnerabilities.
99
+ Dense Arrays is available under the [MIT license](https://github.com/e-south/dense-arrays/blob/main/LICENSE).
@@ -0,0 +1,69 @@
1
+ # ![Dense Arrays — overlapping motifs within a sequence-length limit](https://raw.githubusercontent.com/e-south/dense-arrays/v0.2.0/docs/assets/dense-arrays-banner.png)
2
+
3
+ [![CI](https://github.com/e-south/dense-arrays/actions/workflows/ci.yml/badge.svg)](https://github.com/e-south/dense-arrays/actions/workflows/ci.yml)
4
+ [![docs](https://img.shields.io/badge/docs-gitlab_pages-blue)](https://dunloplab.gitlab.io/dense-arrays)
5
+
6
+ Pack overlapping DNA motifs into a sequence-length limit. Dense Arrays selects
7
+ an arrangement and returns the sequence and each motif's position, so you can
8
+ inspect the overlaps or generate further solutions. Choose single- or
9
+ double-strand placement, require motif groups, or constrain positions.
10
+
11
+ The package also renders saved placements as images and video. [Watch the
12
+ worked playback](https://dunloplab.gitlab.io/dense-arrays/playback/#watch-four-overlapping-motifs)
13
+ to see how four motifs share sequence space.
14
+
15
+ ## First array
16
+
17
+ With Python 3.12 or later and [uv](https://docs.astral.sh/uv/), run from a
18
+ [source checkout](https://github.com/e-south/dense-arrays/blob/main/docs/quickstart.md#install-from-source):
19
+
20
+ ```bash
21
+ uv sync --frozen
22
+ uv run dense-arrays optimize \
23
+ --motif ACGTTGCAAGTCCTGA \
24
+ --motif AGTCCTGATCGTACCG \
25
+ --motif TCGTACCGATGCTTAG \
26
+ --motif ATGCTTAGGACGTTCA \
27
+ --length 40 --strands double
28
+ ```
29
+
30
+ The search considers both strands. One optimal arrangement packs all four
31
+ 16-base motifs into 40 bases:
32
+
33
+ ```text
34
+ ACGTTGCAAGTCCTGATCGTACCGATGCTTAGGACGTTCA
35
+ ```
36
+
37
+ In this orientation, the motifs start at 0, 8, 16, and 24. The solver may return
38
+ the reverse-complement arrangement. The
39
+ [quickstart](https://github.com/e-south/dense-arrays/blob/main/docs/quickstart.md) explains these offsets and shows Python use
40
+ and bounded enumeration of further solutions.
41
+
42
+ ## Documentation
43
+
44
+ - [Create and read your first array](https://github.com/e-south/dense-arrays/blob/main/docs/quickstart.md).
45
+ - [Set positional and regulator constraints](https://github.com/e-south/dense-arrays/blob/main/docs/constraints.md).
46
+ - [Render saved placements as images or video](https://github.com/e-south/dense-arrays/blob/main/docs/playback.md).
47
+ - [Understand the packing method](https://github.com/e-south/dense-arrays/blob/main/docs/method.md).
48
+ - [Look up Python interfaces](https://github.com/e-south/dense-arrays/blob/main/docs/api.md).
49
+ - [Update an existing caller](https://github.com/e-south/dense-arrays/blob/main/docs/migration.md).
50
+
51
+ The [documentation index](https://github.com/e-south/dense-arrays/blob/main/docs/index.md) also routes integrators and
52
+ contributors to the relevant interfaces and checks.
53
+
54
+ ## Citation
55
+
56
+ If you use Dense Arrays in your research, please cite:
57
+
58
+ Andreani V, South EJ, Dunlop MJ (2024). Generating information-dense promoter
59
+ sequences with optimal string packing. *PLOS Computational Biology* 20(7):
60
+ e1012276. [doi:10.1371/journal.pcbi.1012276](https://doi.org/10.1371/journal.pcbi.1012276).
61
+
62
+ Record the package version or commit alongside your results.
63
+
64
+ ## Contribute
65
+
66
+ [Development](https://github.com/e-south/dense-arrays/blob/main/docs/development.md) covers local checks and documentation builds.
67
+ [Report bugs](https://github.com/e-south/dense-arrays/issues) and follow the
68
+ [security policy](https://github.com/e-south/dense-arrays/blob/main/SECURITY.md) for vulnerabilities.
69
+ Dense Arrays is available under the [MIT license](https://github.com/e-south/dense-arrays/blob/main/LICENSE).
@@ -0,0 +1,90 @@
1
+ [project]
2
+ name = "dense-arrays"
3
+ version = "0.2.0"
4
+ description = "A library to create densely packed DNA arrays from motifs"
5
+ authors = [
6
+ {name = "Virgile Andreani", email = "andreani@bu.edu"},
7
+ {name = "Eric J South", email = "ericjohnsouth@gmail.com"},
8
+ ]
9
+ dependencies = [
10
+ "numpy>=1.25.2",
11
+ "ortools>=9.7.2996",
12
+ "rich>=13.7.0",
13
+ "typer>=0.12.0",
14
+ ]
15
+ requires-python = ">=3.12"
16
+ readme = "README.md"
17
+ license = "MIT"
18
+ license-files = ["LICENSE"]
19
+
20
+ [project.urls]
21
+ Repository = "https://github.com/e-south/dense-arrays"
22
+ Documentation = "https://dunloplab.gitlab.io/dense-arrays"
23
+ Issues = "https://github.com/e-south/dense-arrays/issues"
24
+
25
+ [project.optional-dependencies]
26
+ playback = [
27
+ "matplotlib>=3.8.0",
28
+ "networkx>=3.6.1",
29
+ ]
30
+ docs = [
31
+ "mkdocs>=1.6.1",
32
+ "mkdocs-material>=9.7.3",
33
+ "mkdocstrings[python]>=1.0.3",
34
+ ]
35
+ dev = [
36
+ "pre-commit>=4.5.1",
37
+ "pip-audit>=2.10.0",
38
+ "pytest>=9.0.3",
39
+ "ruff>=0.14.14",
40
+ ]
41
+
42
+ [project.scripts]
43
+ dense-arrays = "dense_arrays.cli:app"
44
+ dense-arrays-playback = "dense_arrays.playback.cli:app"
45
+
46
+ [build-system]
47
+ requires = ["setuptools>=77.0.3", "wheel"]
48
+ build-backend = "setuptools.build_meta"
49
+
50
+ [tool.ruff.lint]
51
+ preview = true
52
+ explicit-preview-rules = true
53
+ select = ["ALL"]
54
+ ignore = [
55
+ # The formatter owns trailing commas.
56
+ "COM812",
57
+ # Implicitly concatenated string literals on one line
58
+ # (does not work well with ruff format)
59
+ "ISC001",
60
+ # Unnecessary assignment to return
61
+ "RET504",
62
+ # Commented code
63
+ "ERA001",
64
+ # Copyright stuff
65
+ "CPY001",
66
+ ]
67
+
68
+ [tool.ruff.lint.per-file-ignores]
69
+ "maintenance/release.py" = ["T201"]
70
+ # Optional visualization imports stay inside the functions that use them.
71
+ # Relative imports follow the playback package boundary.
72
+ "src/dense_arrays/playback/**/*.py" = [
73
+ "PLC0415",
74
+ "TID252",
75
+ ]
76
+ "tests/test_optional_playback_imports.py" = ["S101", "S404", "S603"]
77
+ "src/dense_arrays/main.py" = ["T201"]
78
+ # Typer evaluates Path annotations and supplies option defaults at runtime.
79
+ "src/dense_arrays/cli.py" = ["FBT002"]
80
+ "src/dense_arrays/playback/cli.py" = ["TC003", "PLR0913", "PLR0917"]
81
+ # Keep the existing explicit media option signatures.
82
+ "src/dense_arrays/playback/matplotlib_renderer.py" = ["PLR0913"]
83
+ "src/dense_arrays/playback/export.py" = ["PLR0913"]
84
+ "tests/test_*.py" = ["ANN201", "D103", "S101", "PLR2004", "FBT001"]
85
+
86
+ [tool.ruff.lint.pydocstyle]
87
+ convention = "numpy"
88
+
89
+ [tool.ruff.lint.flake8-pytest-style]
90
+ parametrize-names-type = "csv"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,51 @@
1
+ """Pack DNA motif libraries and describe their realized placements.
2
+
3
+ Module Author(s): Virgile Andreani, Eric J. South
4
+ Dunlop Lab
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from importlib import import_module
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from .errors import (
14
+ InfeasibleError,
15
+ InvalidSolverResultError,
16
+ OptimizationError,
17
+ SolverBackendError,
18
+ UnprovenSolutionError,
19
+ )
20
+ from .optimizer import Optimizer
21
+ from .solution import DenseArray
22
+
23
+ __all__ = [
24
+ "DenseArray",
25
+ "InfeasibleError",
26
+ "InvalidSolverResultError",
27
+ "OptimizationError",
28
+ "Optimizer",
29
+ "SolverBackendError",
30
+ "UnprovenSolutionError",
31
+ ]
32
+
33
+
34
+ def __getattr__(name: str) -> object:
35
+ """Load public classes on demand so playback does not import a solver.
36
+
37
+ Returns
38
+ -------
39
+ object
40
+ The requested public class.
41
+
42
+ Raises
43
+ ------
44
+ AttributeError
45
+ If the name is not part of the public package interface.
46
+ """
47
+ if name not in __all__:
48
+ msg = f"module {__name__!r} has no attribute {name!r}"
49
+ raise AttributeError(msg)
50
+ module = {"Optimizer": "optimizer", "DenseArray": "solution"}.get(name, "errors")
51
+ return getattr(import_module(f"{__name__}.{module}"), name)
@@ -0,0 +1,138 @@
1
+ """Validate scalar fields and immutable JSON snapshots for persisted records.
2
+
3
+ Module Author(s): Eric J. South
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ from collections.abc import Mapping, Sequence
10
+ from enum import StrEnum
11
+ from types import MappingProxyType
12
+
13
+ _IUPAC_DNA = frozenset("ACGTRYSWKMBDHVN")
14
+ _SHA256_LENGTH = 64
15
+
16
+
17
+ def required_text(value: object, *, field_name: str) -> str:
18
+ """Return nonblank text without coercing identities."""
19
+ if not isinstance(value, str):
20
+ msg = f"{field_name} must be a non-empty string"
21
+ raise TypeError(msg)
22
+ if not value.strip():
23
+ msg = f"{field_name} must be a non-empty string"
24
+ raise ValueError(msg)
25
+ return value
26
+
27
+
28
+ def normalized_dna(value: object, *, field_name: str) -> str:
29
+ """Return uppercase DNA after checking the IUPAC alphabet."""
30
+ sequence = required_text(value, field_name=field_name).strip().upper()
31
+ invalid = sorted(set(sequence) - _IUPAC_DNA)
32
+ if invalid:
33
+ msg = f"{field_name} contains non-IUPAC DNA symbols: {invalid}"
34
+ raise ValueError(msg)
35
+ return sequence
36
+
37
+
38
+ def integer(value: object, *, field_name: str, minimum: int | None = None) -> int:
39
+ """Return an integer within its declared domain, excluding booleans."""
40
+ if isinstance(value, bool) or not isinstance(value, int):
41
+ msg = f"{field_name} must be an integer"
42
+ raise TypeError(msg)
43
+ if minimum is not None and value < minimum:
44
+ msg = f"{field_name} must be an integer >= {minimum}"
45
+ raise ValueError(msg)
46
+ return value
47
+
48
+
49
+ def enum_value[T: StrEnum](value: object, enum: type[T], *, field_name: str) -> T:
50
+ """Return a supported enum value from an enum or exact string."""
51
+ required_text(value, field_name=field_name)
52
+ return enum(value)
53
+
54
+
55
+ def digest(value: object, *, field_name: str) -> str:
56
+ """Return a canonical SHA-256 hexadecimal digest."""
57
+ text = required_text(value, field_name=field_name).strip().lower()
58
+ if len(text) != _SHA256_LENGTH or any(
59
+ char not in "0123456789abcdef" for char in text
60
+ ):
61
+ msg = f"{field_name} must be a SHA-256 hex digest"
62
+ raise ValueError(msg)
63
+ return text
64
+
65
+
66
+ def records[T](
67
+ value: object, record_type: type[T], *, field_name: str
68
+ ) -> tuple[T, ...]:
69
+ """Freeze a sequence of records after checking every member's type."""
70
+ if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
71
+ msg = f"{field_name} must be a sequence of {record_type.__name__} records"
72
+ raise TypeError(msg)
73
+ result = tuple(value)
74
+ if any(not isinstance(item, record_type) for item in result):
75
+ msg = f"{field_name} must contain only {record_type.__name__} records"
76
+ raise TypeError(msg)
77
+ return result
78
+
79
+
80
+ def _freeze_json(value: object, ancestors: frozenset[int]) -> object:
81
+ if value is None or isinstance(value, (str, bool, int)):
82
+ return value
83
+ if isinstance(value, float):
84
+ if math.isfinite(value):
85
+ return value
86
+ msg = "JSON provenance numbers must be finite"
87
+ raise ValueError(msg)
88
+ if id(value) in ancestors:
89
+ msg = "JSON provenance must not contain cyclic containers"
90
+ raise ValueError(msg)
91
+ parents = ancestors | {id(value)}
92
+ if isinstance(value, Mapping):
93
+ if any(not isinstance(key, str) for key in value):
94
+ msg = "JSON provenance object keys must be strings"
95
+ raise TypeError(msg)
96
+ return MappingProxyType(
97
+ {key: _freeze_json(item, parents) for key, item in value.items()}
98
+ )
99
+ if isinstance(value, (list, tuple)):
100
+ return tuple(_freeze_json(item, parents) for item in value)
101
+ msg = "provenance and metadata must contain only JSON values"
102
+ raise TypeError(msg)
103
+
104
+
105
+ def immutable_json_mapping(value: object) -> Mapping[str, object]:
106
+ """Return a recursively immutable, detached JSON object snapshot."""
107
+ if not isinstance(value, Mapping):
108
+ msg = "provenance and metadata must be JSON objects"
109
+ raise TypeError(msg)
110
+ return _freeze_json(value, frozenset())
111
+
112
+
113
+ def mutable_json(value: object) -> object:
114
+ """Return independent JSON dictionaries and arrays from a frozen snapshot."""
115
+ if isinstance(value, Mapping):
116
+ return {key: mutable_json(item) for key, item in value.items()}
117
+ if isinstance(value, tuple):
118
+ return [mutable_json(item) for item in value]
119
+ return value
120
+
121
+
122
+ def validate_placement_sequence(
123
+ *, placement_id: str, start: int, end: int, sequence: str, realized_sequence: str
124
+ ) -> None:
125
+ """Check placement bounds and exact alignment with the realized sequence."""
126
+ if end > len(realized_sequence):
127
+ msg = (
128
+ f"placement {placement_id!r} ends at {end}, "
129
+ f"beyond sequence length {len(realized_sequence)}"
130
+ )
131
+ raise ValueError(msg)
132
+ observed = realized_sequence[start:end]
133
+ if observed != sequence:
134
+ msg = (
135
+ f"placement {placement_id!r} is sequence-inconsistent: "
136
+ f"expected {sequence!r}, observed {observed!r}"
137
+ )
138
+ raise ValueError(msg)