seqtree 1.1.0__tar.gz → 1.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 (54) hide show
  1. {seqtree-1.1.0 → seqtree-1.2.0}/CHANGELOG.md +18 -0
  2. {seqtree-1.1.0 → seqtree-1.2.0}/CMakeLists.txt +1 -0
  3. {seqtree-1.1.0 → seqtree-1.2.0}/PKG-INFO +12 -1
  4. {seqtree-1.1.0 → seqtree-1.2.0}/README.md +11 -0
  5. seqtree-1.2.0/RELEASE_AUDIT.md +12 -0
  6. {seqtree-1.1.0 → seqtree-1.2.0}/include/seqtree/seqtree.hpp +100 -8
  7. {seqtree-1.1.0 → seqtree-1.2.0}/pyproject.toml +1 -1
  8. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/gapblock.py +281 -18
  9. {seqtree-1.1.0 → seqtree-1.2.0}/src/_bindings.cpp +170 -21
  10. seqtree-1.2.0/src/alignment_max.cpp +177 -0
  11. {seqtree-1.1.0 → seqtree-1.2.0}/src/count_unit.hpp +40 -12
  12. {seqtree-1.1.0 → seqtree-1.2.0}/src/engine_seqtm.cpp +2 -2
  13. {seqtree-1.1.0 → seqtree-1.2.0}/src/engines.hpp +1 -1
  14. seqtree-1.2.0/src/gapblock.cpp +1210 -0
  15. {seqtree-1.1.0 → seqtree-1.2.0}/src/index.cpp +5 -3
  16. {seqtree-1.1.0 → seqtree-1.2.0}/src/pairwise.cpp +14 -3
  17. seqtree-1.2.0/src/pairwise_detail.hpp +8 -0
  18. {seqtree-1.1.0 → seqtree-1.2.0}/src/searcher.cpp +5 -1
  19. seqtree-1.1.0/src/gapblock.cpp +0 -499
  20. {seqtree-1.1.0 → seqtree-1.2.0}/.gitattributes +0 -0
  21. {seqtree-1.1.0 → seqtree-1.2.0}/.gitignore +0 -0
  22. {seqtree-1.1.0 → seqtree-1.2.0}/LICENSE +0 -0
  23. {seqtree-1.1.0 → seqtree-1.2.0}/include/seqtree/kmer_index.hpp +0 -0
  24. {seqtree-1.1.0 → seqtree-1.2.0}/include/seqtree/parallel.hpp +0 -0
  25. {seqtree-1.1.0 → seqtree-1.2.0}/include/seqtree/text_index.hpp +0 -0
  26. {seqtree-1.1.0 → seqtree-1.2.0}/include/seqtree/types.hpp +0 -0
  27. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/__init__.py +0 -0
  28. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/control.py +0 -0
  29. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/data/control_human_trb_aa.txt.gz +0 -0
  30. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/distance.py +0 -0
  31. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/evalue.py +0 -0
  32. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/layout.py +0 -0
  33. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/pairwise.py +0 -0
  34. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/pmhc.py +0 -0
  35. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/pmhc_evalue.py +0 -0
  36. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/py.typed +0 -0
  37. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/seeds.py +0 -0
  38. {seqtree-1.1.0 → seqtree-1.2.0}/python/seqtree/text_evalue.py +0 -0
  39. {seqtree-1.1.0 → seqtree-1.2.0}/src/atomic_write.hpp +0 -0
  40. {seqtree-1.1.0 → seqtree-1.2.0}/src/blosum45.inc +0 -0
  41. {seqtree-1.1.0 → seqtree-1.2.0}/src/blosum62.inc +0 -0
  42. {seqtree-1.1.0 → seqtree-1.2.0}/src/blosum80.inc +0 -0
  43. {seqtree-1.1.0 → seqtree-1.2.0}/src/codec.cpp +0 -0
  44. {seqtree-1.1.0 → seqtree-1.2.0}/src/distance.cpp +0 -0
  45. {seqtree-1.1.0 → seqtree-1.2.0}/src/engine_seqtrie.cpp +0 -0
  46. {seqtree-1.1.0 → seqtree-1.2.0}/src/kmer_index.cpp +0 -0
  47. {seqtree-1.1.0 → seqtree-1.2.0}/src/pam100.inc +0 -0
  48. {seqtree-1.1.0 → seqtree-1.2.0}/src/pam250.inc +0 -0
  49. {seqtree-1.1.0 → seqtree-1.2.0}/src/positional_matrix.cpp +0 -0
  50. {seqtree-1.1.0 → seqtree-1.2.0}/src/structural.inc +0 -0
  51. {seqtree-1.1.0 → seqtree-1.2.0}/src/substitution_matrix.cpp +0 -0
  52. {seqtree-1.1.0 → seqtree-1.2.0}/src/text_index.cpp +0 -0
  53. {seqtree-1.1.0 → seqtree-1.2.0}/src/trie.cpp +0 -0
  54. {seqtree-1.1.0 → seqtree-1.2.0}/src/trie.hpp +0 -0
@@ -3,6 +3,24 @@
3
3
  All notable changes to `seqtree`. Dates are release dates. From 1.0.0 the project follows semantic
4
4
  versioning: breaking changes need a **major** bump.
5
5
 
6
+ ## [1.2.0] — 2026-10-17
7
+
8
+ ### Fixed
9
+
10
+ - Exact exclusion masks matching hits without shrinking model populations or
11
+ supplied annotation counters, including withheld-label models.
12
+
13
+ ### Added
14
+
15
+ - Fixed query/reference partitions for native group audits.
16
+ - Bounded sparse grouped radius-hit output with deterministic ordering and explicit memory-cap failures.
17
+ - Optional grouped geometry compatible with the supported nanobind 2.5 floor.
18
+
19
+ - Generic float64 global-alignment target maxima with numeric annotation CSR,
20
+ group offsets, explicit matrix symbols and independent candidate edit budgets.
21
+ - Optional aggregate insertion/deletion limits for batched edit histograms;
22
+ omitted and redundant limits preserve the existing count paths.
23
+
6
24
  ## [1.1.0] — 2026-10-11
7
25
 
8
26
  ### Added
@@ -30,6 +30,7 @@ add_library(seqtree_core STATIC
30
30
  src/searcher.cpp
31
31
  src/gapblock.cpp
32
32
  src/pairwise.cpp
33
+ src/alignment_max.cpp
33
34
  src/distance.cpp
34
35
  )
35
36
  target_include_directories(seqtree_core PUBLIC include PRIVATE src)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: seqtree
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Fast fuzzy search over biological sequences (C++ core, Python bindings)
5
5
  Keywords: sequence-search,fuzzy-matching,CDR3,immunology,bioinformatics,trie
6
6
  Author-Email: ISALGO laboratory <mikhail.shugay@gmail.com>
@@ -289,3 +289,14 @@ This repo follows **git-flow**:
289
289
  Roadmap (affine gaps, position-specific matrices, succinct memory packing) lives in
290
290
  [docs/roadmap.rst](docs/roadmap.rst). Control-set E-values already ship — see the
291
291
  [E-value guide](https://antigenomics.github.io/seqtree/evalue.html).
292
+
293
+ ### Floating alignment reductions
294
+
295
+ `Index.alignment_max_batch` streams unit-cost search candidates through float64
296
+ global alignment and requested numeric-target maxima without a dense pair table.
297
+ It accepts explicit matrix symbols, small group-offset matrices and annotation CSR,
298
+ with separate alignment-selection and final scoring coefficients.
299
+ `Index.edit_histogram_batch(..., max_indels=...)` additionally supports an aggregate
300
+ insertion/deletion cap; its default preserves previous behavior. See the
301
+ [API reference](https://antigenomics.github.io/seqtree/api.html) for array contracts
302
+ and tie, count, exclusion and unsupported-value semantics.
@@ -257,3 +257,14 @@ This repo follows **git-flow**:
257
257
  Roadmap (affine gaps, position-specific matrices, succinct memory packing) lives in
258
258
  [docs/roadmap.rst](docs/roadmap.rst). Control-set E-values already ship — see the
259
259
  [E-value guide](https://antigenomics.github.io/seqtree/evalue.html).
260
+
261
+ ### Floating alignment reductions
262
+
263
+ `Index.alignment_max_batch` streams unit-cost search candidates through float64
264
+ global alignment and requested numeric-target maxima without a dense pair table.
265
+ It accepts explicit matrix symbols, small group-offset matrices and annotation CSR,
266
+ with separate alignment-selection and final scoring coefficients.
267
+ `Index.edit_histogram_batch(..., max_indels=...)` additionally supports an aggregate
268
+ insertion/deletion cap; its default preserves previous behavior. See the
269
+ [API reference](https://antigenomics.github.io/seqtree/api.html) for array contracts
270
+ and tie, count, exclusion and unsupported-value semantics.
@@ -0,0 +1,12 @@
1
+ # Release audit — 17 October 2026
2
+
3
+ The requested generic batch/search APIs are implemented. No new native algorithm is needed for the audited downstream requirements.
4
+
5
+ - Native batch search and edit histograms; aggregate insertion/deletion caps.
6
+ - Float64 global-alignment target reductions, separate candidate/final scoring semantics, fixed populations under exact exclusion.
7
+ - Grouped geometry, fixed audit partitions, bounded deterministic sparse radius hits and optional-geometry compatibility at the supported nanobind floor.
8
+ - Shared CPU allocation checks; one-thread inline execution and explicit over-budget rejection.
9
+
10
+ Verification on `0852ff5`: **379 passed, 1 Linux-only platform skip** across `test_batch_threads.py`, `test_alignment_max.py`, `test_edit_histogram.py`, `test_grouped_optional_geometry.py` and `test_grouped_sparse_hits.py`. An additional 280 checks cover grouped populations, withholding, paired lanes, offsets, score bins and partitions. Native budget was two CPUs; numerical libraries used one thread. The committed `examples/01_gapped_search.py` runs and its per-query cutoff/search output was inspected. Sphinx builds with `-W`.
11
+
12
+ The local `dev` commits `0126e00`, `858056b` and `0852ff5` were three commits ahead of the remote at audit start. They are preserved and included when this audited `dev` is pushed. The changelog now covers these capabilities; public API documentation already describes the alignment reductions. [Issue 18](https://github.com/antigenomics/seqtree/issues/18) tracks integration into the release head and the remaining exact-head cross-platform/wheel gate. Publishing remains the maintainer's action; this audit creates no tag or release. Focused checks do not substitute for complete release CI.
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <cstdint>
5
5
  #include <memory>
6
+ #include <optional>
6
7
  #include <string>
7
8
  #include <string_view>
8
9
  #include <tuple>
@@ -104,6 +105,14 @@ private:
104
105
  std::vector<uint8_t> masked_; // [width]
105
106
  };
106
107
 
108
+ struct AlignmentMaxResult {
109
+ std::vector<double> scores, raw_similarity, normalized_similarity, pfit, informativeness;
110
+ std::vector<uint64_t> counts, modeled_counts, aa_counts, exact_counts, candidate_counts, nw_pairs;
111
+ std::vector<int64_t> best_ref_ids, best_lane_ids;
112
+ std::vector<uint32_t> gap_counts;
113
+ std::vector<uint8_t> query_supported;
114
+ };
115
+
107
116
  // Immutable after build. Lock-free for concurrent reads; share one Index across
108
117
  // threads, give each thread its own Searcher.
109
118
  class Index {
@@ -128,7 +137,18 @@ public:
128
137
  // Exact edit-count bins 0..max_total_edits; one reusable hit buffer per worker.
129
138
  std::vector<std::vector<uint64_t>> edit_histogram_batch(
130
139
  const std::vector<std::string>& queries, const SearchParams&,
131
- int threads = 0, bool exclude_exact = false) const;
140
+ int threads = 0, bool exclude_exact = false, int max_indels = -1) const;
141
+
142
+ // Numeric requested-label maxima; float NW selection is independent of candidate scope.
143
+ AlignmentMaxResult alignment_max_batch(const std::vector<std::string>& queries,
144
+ const SearchParams&, const std::vector<std::vector<double>>& similarity, const std::string& symbols, int max_indels,
145
+ const std::vector<uint32_t>& query_target_offsets, const std::vector<uint32_t>& query_target_ids,
146
+ const std::vector<uint32_t>& reference_target_offsets, const std::vector<uint32_t>& reference_target_ids,
147
+ const std::vector<int32_t>& target_class_ids, const std::vector<uint32_t>& reference_class_offsets,
148
+ const std::vector<int32_t>& reference_class_ids, const std::vector<std::vector<int32_t>>& query_group_ids,
149
+ const std::vector<std::vector<int32_t>>& reference_group_ids, const std::vector<std::vector<std::vector<double>>>& group_offsets,
150
+ const std::vector<uint64_t>& reference_other_counts, const std::vector<uint64_t>& reference_multiplicities,
151
+ const std::vector<double>& coefficients, int threads=0, bool exclude_exact=false) const;
132
152
 
133
153
  // Per-query count of seqtm collisions: how often a reference was re-reached via a
134
154
  // different edit path during branch-and-bound (0 for the seqtrie engine / Hamming).
@@ -154,7 +174,7 @@ public:
154
174
  ~Searcher();
155
175
 
156
176
  std::vector<Hit> search(std::string_view query, const SearchParams&);
157
- void search_into(std::string_view query, const SearchParams&, std::vector<Hit>& out);
177
+ void search_into(std::string_view query, const SearchParams&, std::vector<Hit>& out, int max_indels = -1);
158
178
  bool search_top(std::string_view query, const SearchParams&, Hit& out);
159
179
 
160
180
  // seqtm collisions from the most recent search_into on this Searcher.
@@ -228,6 +248,20 @@ std::vector<int32_t> align_dist_matrix(const std::vector<std::string>& queries,
228
248
  // Both axes are required; these rows cannot combine with length profiles.
229
249
  // Optional group IDs index a nonnegative rectangular penalty table added per pair.
230
250
  // Exact exclusion remains encoded junction identity, independent of these groups.
251
+ // Factored nonnegative offsets: paired feature codes, one shared code cost table.
252
+ // Round weight * sum(max(head cost, tail cost)) / depth to nearest, ties even,
253
+ // then multiply by scale. Input descriptors are copied into immutable batch state.
254
+ struct PairedFeatureOffsets {
255
+ std::vector<std::vector<int32_t>> query_codes, reference_codes, costs;
256
+ double weight;
257
+ int32_t scale;
258
+ PairedFeatureOffsets(std::vector<std::vector<int32_t>> q,
259
+ std::vector<std::vector<int32_t>> r,
260
+ std::vector<std::vector<int32_t>> c, double w = 1.0, int32_t s = 1)
261
+ : query_codes(std::move(q)), reference_codes(std::move(r)), costs(std::move(c)),
262
+ weight(w), scale(s) {}
263
+ };
264
+
231
265
  std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
232
266
  const std::vector<std::string>& refs, Alphabet,
233
267
  const SubstitutionMatrix* matrix, int32_t gap_open,
@@ -238,7 +272,8 @@ std::vector<int32_t> gapblock_matrix(const std::vector<std::string>& queries,
238
272
  const std::vector<int32_t>& reference_groups = {},
239
273
  const std::vector<std::vector<int32_t>>& group_distances = {},
240
274
  const std::vector<std::vector<int32_t>>& query_weights = {},
241
- const std::vector<std::vector<int32_t>>& reference_weights = {});
275
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
276
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt);
242
277
 
243
278
  // Exhaustive ball rows in reference order; hit edit fields are unset (zero).
244
279
  std::vector<std::vector<Hit>> gapblock_ball_batch(const std::vector<std::string>& queries,
@@ -250,7 +285,8 @@ std::vector<std::vector<Hit>> gapblock_ball_batch(const std::vector<std::string>
250
285
  const std::vector<int32_t>& reference_groups = {},
251
286
  const std::vector<std::vector<int32_t>>& group_distances = {},
252
287
  const std::vector<std::vector<int32_t>>& query_weights = {},
253
- const std::vector<std::vector<int32_t>>& reference_weights = {});
288
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
289
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt);
254
290
 
255
291
  // Exhaustive bounded reductions using the same gapblock cell. Top-k ties use
256
292
  // (penalty, ref_id); paired penalty is max(lane scores). Exact exclusion uses
@@ -267,7 +303,10 @@ gapblock_group_audit_batch(const std::vector<std::string>& queries,
267
303
  const std::vector<int32_t>& reference_groups = {},
268
304
  const std::vector<std::vector<int32_t>>& group_distances = {},
269
305
  const std::vector<std::vector<int32_t>>& query_weights = {},
270
- const std::vector<std::vector<int32_t>>& reference_weights = {});
306
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
307
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt,
308
+ const std::vector<int32_t>& query_partitions = {},
309
+ const std::vector<int32_t>& reference_partitions = {});
271
310
 
272
311
  std::vector<std::vector<Hit>> gapblock_topk_batch(const std::vector<std::string>& queries,
273
312
  const std::vector<std::string>& refs, uint32_t k, Alphabet alphabet,
@@ -287,7 +326,8 @@ std::vector<std::vector<uint64_t>> gapblock_count_batch(const std::vector<std::s
287
326
  const std::vector<int32_t>& reference_groups = {},
288
327
  const std::vector<std::vector<int32_t>>& group_distances = {},
289
328
  const std::vector<std::vector<int32_t>>& query_weights = {},
290
- const std::vector<std::vector<int32_t>>& reference_weights = {});
329
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
330
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt, bool shared_thresholds = false);
291
331
  std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>> gapblock_count_mass_batch(const std::vector<std::string>& queries,
292
332
  const std::vector<std::string>& refs, const std::vector<std::vector<int32_t>>& thresholds,
293
333
  Alphabet alphabet, const SubstitutionMatrix* matrix, int32_t go, int32_t ge,
@@ -297,7 +337,8 @@ std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>
297
337
  const std::vector<int32_t>& reference_groups = {},
298
338
  const std::vector<std::vector<int32_t>>& group_distances = {},
299
339
  const std::vector<std::vector<int32_t>>& query_weights = {},
300
- const std::vector<std::vector<int32_t>>& reference_weights = {});
340
+ const std::vector<std::vector<int32_t>>& reference_weights = {},
341
+ const std::optional<PairedFeatureOffsets>& feature_offsets = std::nullopt, bool shared_thresholds = false);
301
342
  std::vector<std::vector<uint64_t>> gapblock_paired_count_batch(
302
343
  const std::vector<std::string>& qa, const std::vector<std::string>& qb,
303
344
  const std::vector<std::string>& ra, const std::vector<std::string>& rb,
@@ -319,7 +360,9 @@ std::pair<std::vector<std::vector<uint64_t>>, std::vector<std::vector<uint64_t>>
319
360
  const std::vector<std::vector<int32_t>>& query_weights_alpha = {},
320
361
  const std::vector<std::vector<int32_t>>& query_weights_beta = {},
321
362
  const std::vector<std::vector<int32_t>>& reference_weights_alpha = {},
322
- const std::vector<std::vector<int32_t>>& reference_weights_beta = {});
363
+ const std::vector<std::vector<int32_t>>& reference_weights_beta = {},
364
+ const std::optional<PairedFeatureOffsets>& feature_offsets_alpha = std::nullopt,
365
+ const std::optional<PairedFeatureOffsets>& feature_offsets_beta = std::nullopt, bool shared_thresholds = false);
323
366
 
324
367
  // ---------------------------------------------------------------------------------------
325
368
  // Plain string edit distances on raw characters, unit costs. Deliberately alphabet-agnostic:
@@ -341,4 +384,53 @@ std::vector<int32_t> hamming_matrix(const std::vector<std::string>& a,
341
384
  std::vector<int32_t> levenshtein_matrix(const std::vector<std::string>& a,
342
385
  const std::vector<std::string>& b, int threads = 0);
343
386
 
387
+ // Generic label histograms over one prepared native query/reference population.
388
+ struct GroupedGeometry {
389
+ std::vector<PairedFeatureOffsets> additional_features;
390
+ std::optional<std::vector<int32_t>> gap_prior;
391
+ std::string alignment_mode = "gapblock";
392
+ int32_t alignment_scale = 1;
393
+ uint32_t trim_left = 0, trim_right = 0;
394
+ int32_t score_quantum = 1;
395
+ bool return_hits = false;
396
+ size_t max_hit_bytes = 64*1024*1024;
397
+ std::optional<std::vector<int32_t>> query_target_ids;
398
+ std::optional<std::vector<int64_t>> query_reference_ids;
399
+ std::vector<std::vector<int32_t>> weights, group_distances, query_weights, reference_weights;
400
+ std::vector<int32_t> query_groups, reference_groups;
401
+ std::optional<PairedFeatureOffsets> features;
402
+ };
403
+ struct CountBuffer {
404
+ std::vector<uint32_t> data;
405
+ std::vector<size_t> shape;
406
+ };
407
+ struct GroupedCountChunk {
408
+ size_t start = 0, stop = 0;
409
+ CountBuffer counts, sizes, exact_counts;
410
+ CountBuffer hits{{},{0,3}};
411
+ };
412
+ class GroupedCountBatch {
413
+ public:
414
+ GroupedCountBatch(const std::vector<std::string>& qa, const std::vector<std::string>& ra,
415
+ std::vector<int32_t> thresholds, std::vector<uint32_t> target_offsets,
416
+ std::vector<uint32_t> target_ids, std::vector<int32_t> target_peptides,
417
+ std::vector<int32_t> exclusive_peptides, std::vector<int32_t> query_genes,
418
+ std::vector<int32_t> reference_genes, size_t chunk_size, Alphabet alphabet,
419
+ const SubstitutionMatrix* matrix, int32_t go, int32_t ge, std::vector<int32_t> prior,
420
+ uint32_t width, int threads, bool exclude_exact, const GroupedGeometry& alpha,
421
+ const std::optional<GroupedGeometry>& beta = std::nullopt,
422
+ const std::vector<std::string>& qb = {}, const std::vector<std::string>& rb = {},
423
+ std::optional<std::vector<int32_t>> query_withheld_peptides = std::nullopt,
424
+ std::vector<uint32_t> reference_peptide_offsets = {},
425
+ std::vector<int32_t> reference_peptide_ids = {});
426
+ std::optional<GroupedCountChunk> next();
427
+ size_t query_count() const;
428
+ size_t reference_count() const;
429
+ size_t target_count() const;
430
+ size_t peptide_count() const;
431
+ private:
432
+ struct Impl;
433
+ std::shared_ptr<Impl> impl_;
434
+ };
435
+
344
436
  } // namespace seqtree
@@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build"
4
4
 
5
5
  [project]
6
6
  name = "seqtree"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  description = "Fast fuzzy search over biological sequences (C++ core, Python bindings)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"