Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
243 commits
Select commit Hold shift + click to select a range
94d7227
Initial ruff formatting and uv project init
bfoley12 May 28, 2026
3c79379
Added .venv and .env to gitignore
bfoley12 May 28, 2026
4d5c64f
Moved dev optional-dependencies to a dependency group
bfoley12 May 28, 2026
8ebb3f6
Synced lock file
bfoley12 May 28, 2026
98d867f
Example env vars
bfoley12 May 28, 2026
6558bc7
Moved to a src/proj style repo. Began creating fastapi server
bfoley12 May 28, 2026
04da662
Removed project-level uv ennvironment
bfoley12 May 28, 2026
b84f81f
Updated python version to 3.14
bfoley12 Jun 1, 2026
a3624de
Added projects router and reintroduced prefix
bfoley12 Jun 1, 2026
f31519b
Added Beanie init
bfoley12 Jun 1, 2026
4de7efd
Broke redis and kong settings into their own classes for modularity
bfoley12 Jun 1, 2026
48f19a6
Added shared types
bfoley12 Jun 1, 2026
4e44d03
Removed unnecessary files
bfoley12 Jun 1, 2026
c44dc0e
Added packages to uv
bfoley12 Jun 1, 2026
da8d2e5
Built out project domain for getting data
bfoley12 Jun 1, 2026
b130b33
Added shared dependencies for injection
bfoley12 Jun 1, 2026
c3004a0
Added AuthenticationError and GatewayError for more control over thro…
bfoley12 Jun 1, 2026
a5b5849
Ruff formatting and checking
bfoley12 Jun 1, 2026
74ac09f
PrefixedEmail now throws ValidationError instead of ValueError
bfoley12 Jun 1, 2026
6baffad
Changed ProjectResource for ProjectOut for consistency with ProjectIn…
bfoley12 Jun 1, 2026
d761d3a
Changed create_project to insert_project
bfoley12 Jun 1, 2026
bd2dbbb
Added Project patch for partial udpates
bfoley12 Jun 2, 2026
9271c5b
Added Project delete
bfoley12 Jun 2, 2026
0ac8204
Added Projet Upsert (put)
bfoley12 Jun 2, 2026
b8bbb4d
Added docstrings and removed dead code
bfoley12 Jun 2, 2026
617afad
Removed outdated runtime_checkable and awaited async function to get …
bfoley12 Jun 2, 2026
22ad07d
Created a parent class to abstract the handling of fields requested f…
bfoley12 Jun 2, 2026
a4d2330
modified docstrings to match function definition
bfoley12 Jun 2, 2026
5808483
Fixed issue where find_one syntax was used with get
bfoley12 Jun 2, 2026
2998c02
Removed dead ProjectSummary usages
bfoley12 Jun 2, 2026
41dd24b
Automatic projection processing now hanldes dot-paths and validates e…
bfoley12 Jun 2, 2026
95d3398
Initial Contributions route setup
bfoley12 Jun 2, 2026
8053215
Added stub methods
bfoley12 Jun 2, 2026
1d30533
Added base contributions class and ContributionPatch. Contribution no…
bfoley12 Jun 2, 2026
97f01bd
Added settings to Project to name proper collection and prevent nulls…
bfoley12 Jun 2, 2026
a2d31fb
Added contributions router
bfoley12 Jun 2, 2026
191c4c0
Sketched out endpoints and repo methods
bfoley12 Jun 2, 2026
96832e8
Mostly formatting and type checking changes. Added stub files for com…
bfoley12 Jun 3, 2026
25bdc30
Abstracted repository and models
bfoley12 Jun 3, 2026
6392fb2
Implemented classes and methods with new abstractions
bfoley12 Jun 3, 2026
960ab66
Implemented delete_contributions
bfoley12 Jun 3, 2026
de54f47
Added linked-document filtering for contributions
bfoley12 Jun 3, 2026
78f32b2
Anyonymous users can no longer be admin. Fixed imports
bfoley12 Jun 3, 2026
ee75871
Added unit tests
bfoley12 Jun 3, 2026
dbd664d
added check fix for normal fixes
bfoley12 Jun 3, 2026
71aaf9d
Various small fixes in function signatures and usages
bfoley12 Jun 3, 2026
573aeb5
Added integration tests
bfoley12 Jun 3, 2026
0f84dd4
Added test commands to justfile
bfoley12 Jun 3, 2026
8c0060b
Added tests to live dev db
bfoley12 Jun 3, 2026
d7b5b8b
Fixed importing from src
bfoley12 Jun 3, 2026
d3496ab
OpenAPI UI improvement. Added app info and tags to routers
bfoley12 Jun 3, 2026
4333511
Moved more openapi info dicts to _openapi
bfoley12 Jun 4, 2026
2a2d03e
Changed imports so they resolve
bfoley12 Jun 4, 2026
7a8181c
Removed old repo
bfoley12 Jun 4, 2026
0c45e9d
Fixed basedpyright not knowing proper import path. Removed old ignore…
bfoley12 Jun 4, 2026
38b2b14
Removed dependencies. Some temporarily
bfoley12 Jun 4, 2026
f21e781
Reordered deps
bfoley12 Jun 4, 2026
eb2ddf9
fields argument in routers now expects a list instead of comma delimi…
bfoley12 Jun 4, 2026
fb176b4
Fleshed out contribution repo methods
bfoley12 Jun 4, 2026
9d83970
Pulled repeated repo logic into parent class
bfoley12 Jun 4, 2026
792f83a
Changed logging middleware to allow contextvars being passed upwards
bfoley12 Jun 4, 2026
870456a
basedpyright ignores tests
bfoley12 Jun 4, 2026
ba08993
Infra changes for rewrite
bfoley12 Jun 4, 2026
c7d7bcf
Added healthcheck endpoint
bfoley12 Jun 4, 2026
5991392
Forward username and is_admin values for logging
bfoley12 Jun 5, 2026
eac368f
Convert ids coming into Contributions to PydanticObjectIds
bfoley12 Jun 5, 2026
d77cf4b
Fleshed out Contribution component models
bfoley12 Jun 5, 2026
7a5dbf8
Init component models
bfoley12 Jun 5, 2026
9a96716
Bulk upload of contributions with components now bulk inserts compone…
bfoley12 Jun 5, 2026
4d860f9
Made component repositories to pull insert logic out of contributions…
bfoley12 Jun 5, 2026
7435e7d
Moved cross-repo insert logic into service layer
bfoley12 Jun 5, 2026
ca962a4
Documents now handle decoding cursor to accomodate differing id types…
bfoley12 Jun 6, 2026
4bb92e2
Added docstrings
bfoley12 Jun 6, 2026
8276016
Inserting many contributions now uses multi-document transactions rat…
bfoley12 Jun 8, 2026
3725643
upsert_contributions uses semaphore
bfoley12 Jun 8, 2026
610ca6d
Added project_identifier index on ContributionsBase
bfoley12 Jun 8, 2026
9ab2a6a
clamp max_concurrent_transactions
bfoley12 Jun 8, 2026
d63edcb
Atomically upsert a contribution by identifiers rather than _id
bfoley12 Jun 8, 2026
2bce5ed
Contributions delete children on bulk delete
bfoley12 Jun 9, 2026
d3c8bf0
Deleting contribution by id deletes all children
bfoley12 Jun 9, 2026
6d6b4ed
Further parameterized the AsyncMongoClient
bfoley12 Jun 9, 2026
24fa834
Fixed spacing issues
bfoley12 Jun 9, 2026
8f965d0
Added table routes
bfoley12 Jun 9, 2026
c979f3d
Moved types to shared domains
bfoley12 Jun 9, 2026
ddb8b7f
Removed unused import
bfoley12 Jun 9, 2026
fa456ad
Added session to delete_by_id and made a DeleteResponse model
bfoley12 Jun 10, 2026
93d60e0
Fleshed out basic CRUD operations for Tables
bfoley12 Jun 10, 2026
b6278e0
Added missing update to patch endpoint
bfoley12 Jun 10, 2026
be8b7fd
Added missing colon in patch function def
bfoley12 Jun 10, 2026
b1977f8
Fleshed out streaming download to user with compression
bfoley12 Jun 10, 2026
f199f9f
Removed unused response paramter from download_table
bfoley12 Jun 10, 2026
dc8e53b
Added shared repository for components
bfoley12 Jun 10, 2026
2572d1d
Added Structures endpoint and refatored to use shared components repo
bfoley12 Jun 10, 2026
7277a99
Added Attachments endpoint
bfoley12 Jun 10, 2026
dd91fbc
Removed get and patch capabilities from attachment endpoint
bfoley12 Jun 10, 2026
b6c9753
Removed logic for inserting attachments during contribution insert
bfoley12 Jun 10, 2026
8cb3422
Moved _children to a property
bfoley12 Jun 10, 2026
3fb7e01
Added attachments, structures, and tables routers to main router
bfoley12 Jun 11, 2026
c844e0a
Made PolarsFrame type to handle coercion and serialization
bfoley12 Jun 11, 2026
4f7b57d
Removed 'components' tag from components routers
bfoley12 Jun 11, 2026
b776d2d
Added additional logging context to each request
bfoley12 Jun 11, 2026
73e5acf
Removed unused gateway secret check
bfoley12 Jun 11, 2026
724698a
Moved router tagging to the api router
bfoley12 Jun 11, 2026
c126522
Updated tests to account for new code
bfoley12 Jun 11, 2026
75d4bdb
Changed exception handling and modified variable names to be descript…
bfoley12 Jun 11, 2026
a934bdd
Removed old settings reference and scoped contribution access to proj…
bfoley12 Jun 11, 2026
79ef63f
DeleteResponse now consumes a DeleteResult to allow for easier expans…
bfoley12 Jun 11, 2026
93e6bf4
Removed vestigial GatewayError references
bfoley12 Jun 11, 2026
946e2e7
Contribution.data validation for depth <= 7
bfoley12 Jun 11, 2026
210b0e1
Disallow lists in Contribution.data
bfoley12 Jun 11, 2026
cb9b506
Added more validation to Contribution.data dict
bfoley12 Jun 11, 2026
39c032f
Removed useless thin wrapper method
bfoley12 Jun 12, 2026
3e21a4d
Inserting many components now validates by md5 hash and returns docum…
bfoley12 Jun 12, 2026
f415323
Component.insert_component delegates to insert_components
bfoley12 Jun 12, 2026
e92977f
Attachments document model now verifies extension of file. Modified i…
bfoley12 Jun 12, 2026
7e359ea
Greatly expanded unit testing
bfoley12 Jun 12, 2026
0413425
Fixed domain routers not prefixing '/'
bfoley12 Jun 12, 2026
d89e40c
Added more thorough integration test coverage
bfoley12 Jun 12, 2026
86ba711
Moved download logic to base shared repository
bfoley12 Jun 15, 2026
8e83e65
Added csv serialization for downloads
bfoley12 Jun 15, 2026
26d0398
Added tests to cover downloads
bfoley12 Jun 15, 2026
12340a6
Fixed gzip missing final packet; improved CSV serialization; improved…
bfoley12 Jun 15, 2026
381353a
Moved DataFormat decision to a match statement
bfoley12 Jun 15, 2026
ddcd41f
Removed module-level docstrings
bfoley12 Jun 15, 2026
b41a420
Setup app to use aioboto3 and load defaults from env
bfoley12 Jun 15, 2026
0264d35
Added dependencies for boto and S3
bfoley12 Jun 16, 2026
a69e64d
Changed filename to authz to clarify that it is authorization
bfoley12 Jun 16, 2026
bfb32c9
Added S3Dep and s3 reliance to download methods; modified auth import…
bfoley12 Jun 16, 2026
d4129b4
Updated auth import to authz
bfoley12 Jun 16, 2026
937be7d
Uncommented kong in settings
bfoley12 Jun 16, 2026
453845c
Modified S3Dep to return the client rather than a generator; moved ke…
bfoley12 Jun 16, 2026
2249c68
Removed kong settings, since they are unused
bfoley12 Jun 17, 2026
6906556
Made a shared ComponentService to ensure deletion only occurs when no…
bfoley12 Jun 17, 2026
6604e55
Added .claude/ to gitignore
bfoley12 Jun 17, 2026
79fd99c
Moved component repo and service logic into a shared service. Compone…
bfoley12 Jun 17, 2026
d2ca35c
Added download tests, removed config test raising on creating Setting…
bfoley12 Jun 17, 2026
5f75cf3
Reverted non-API changes to master versions
bfoley12 Jun 17, 2026
d79ac6f
upgrade dependencies for deployment
invalid-email-address Jun 2, 2026
fa4886c
Infra changes for rewrite
bfoley12 Jun 4, 2026
a5d543b
Stop tracking .claude/ directory
bfoley12 Jun 17, 2026
24d7b5d
Revert changes to deployment of other submodules
bfoley12 Jun 17, 2026
e9e1074
Switched to standard docker image. Allows decoupling python versions …
bfoley12 Jun 18, 2026
0ebd99c
Improved OTel instrumentation. Removed ddtrace references
bfoley12 Jun 18, 2026
53ed677
Fixed uv build wheel building from old directory style
bfoley12 Jun 18, 2026
f7fbff0
Added dependencies to routes that require users to be logged in
bfoley12 Jun 18, 2026
e48eb19
Requests to contributions or their components are now scoped by the u…
bfoley12 Jun 18, 2026
2077a49
Enforce contribution-scoped components at service level
bfoley12 Jun 18, 2026
fa292e1
Convert id str to objectid
bfoley12 Jun 18, 2026
7d617ef
Readded start_rq so supervisord doesnt fail
bfoley12 Jun 18, 2026
8c162ed
Moved kwargs to new spec
bfoley12 Jun 18, 2026
8cc2c23
Changed example kwargs to new specs
bfoley12 Jun 18, 2026
3acff74
Added back n start_rq.sh so build succeeds
bfoley12 Jun 18, 2026
48dce53
Added lines to ignore build artifacts
bfoley12 Jun 18, 2026
0280950
Modernized claude.md
bfoley12 Jun 18, 2026
23494a1
Pointed setuptools at the right repo - src
bfoley12 Jun 18, 2026
0862484
Added and fixed tests
bfoley12 Jun 18, 2026
0521e7c
Formatting
bfoley12 Jun 18, 2026
507c11e
Removing accidental commit of build artifacts
bfoley12 Jun 18, 2026
afbbee1
Unified deletion behavior by moving BaseDocumentWithInput to inherit …
bfoley12 Jun 18, 2026
19df929
Upsert now functions like bulk insert, allowing upserts to fail and r…
bfoley12 Jun 18, 2026
09c6a25
Improved sensitivity of dict key validation
bfoley12 Jun 18, 2026
4a48ef7
Simplified md5 check and improved patch_component_by_id
bfoley12 Jun 18, 2026
cb90bc7
Added a constant for how to handle encoding of polars.DataFrames to h…
bfoley12 Jun 18, 2026
1c7a420
Improved dataframe handling to read and write dataframes
bfoley12 Jun 18, 2026
57d906a
Made ComponentIn to clarify and unify input schema from storage schema
bfoley12 Jun 18, 2026
7992612
Modified tests to account for new handling
bfoley12 Jun 18, 2026
408b6a6
Added ty for typechecking
bfoley12 Jun 22, 2026
90eaca5
Removed ty - not mature enough
bfoley12 Jun 22, 2026
0d122bc
Changed docstring, imports, and added polars dep
bfoley12 Jun 22, 2026
10ce393
Commented out unused dependencies
bfoley12 Jun 23, 2026
dbb8c28
Added custom Access logging in line with previous format.
bfoley12 Jun 24, 2026
17e67f7
pass exc_info through to logger on upsert error
bfoley12 Jun 24, 2026
dd257b3
Added log_level to errors
bfoley12 Jun 24, 2026
0cf8e8f
Moved healthcheck router to healthcheck instead of health
bfoley12 Jun 24, 2026
e1096e5
Added 'version' to healthcheck for backwards compat
bfoley12 Jun 24, 2026
cb5d299
Added user to contribution service and threaded user into repo to che…
bfoley12 Jun 25, 2026
ad1eb94
Added tests for user-contribution validation and modified healthcheck…
bfoley12 Jun 25, 2026
a072a8a
Brought user group checking logic onto user
bfoley12 Jun 25, 2026
d87458b
Added redirects for old endpoints to their corresponding new ones, an…
bfoley12 Jun 25, 2026
87d018b
Added libsnappy back for snappy compression
bfoley12 Jun 25, 2026
7df8e04
Added supervisor and python-snappy to deps
bfoley12 Jun 25, 2026
5f400e4
Made the uv sync no-editable so .pth doesn't cause an error referenci…
bfoley12 Jun 25, 2026
7d91699
Added --workers parameterization
bfoley12 Jun 25, 2026
a6774b8
Brought supervisor up to current use practices
bfoley12 Jun 25, 2026
de403ee
Moved log headers into access log specification to avoid duplication …
bfoley12 Jun 25, 2026
4359ee8
Added jinja2 back to dependencies and sorted deps
bfoley12 Jun 26, 2026
016546b
Build uv venv outside of app so dependencies load properly
bfoley12 Jun 26, 2026
05811ea
Fixed env var names to match new format
bfoley12 Jun 26, 2026
7aee18e
Added shared BaseFilter to convert from Beanie's aliased 'id' to real…
bfoley12 Jun 29, 2026
8257384
Pagination cursor pads b64 encoded cursor to safely decode
bfoley12 Jun 29, 2026
b899b94
Removed response_model from router 'get_*' methods since that would o…
bfoley12 Jun 29, 2026
2c5cedb
Contributions are now versioned. The server derives integer versions …
bfoley12 Jun 30, 2026
8d29dab
Added model and router to expose limits that well-behaved clients sho…
bfoley12 Jul 1, 2026
ddac7eb
Added custom exceptions and middleware to immediately detect oversize…
bfoley12 Jul 1, 2026
88c6f54
Implementing limits endpoint, new exceptions & middleware, and added …
bfoley12 Jul 1, 2026
c625b3c
Added settings specific to MPContribs business logic
bfoley12 Jul 8, 2026
c1bb17e
Added Pint and Uncertainties
bfoley12 Jul 8, 2026
2abdc65
Added float_precision config for Contribution.data values
bfoley12 Jul 9, 2026
06163f1
Added UnitError for handling Pint unit coercion errors
bfoley12 Jul 9, 2026
8410f6b
beanie now drops indices that are not currently specified on the mode…
bfoley12 Jul 9, 2026
06c22f6
Formatting
bfoley12 Jul 9, 2026
14c21e1
Added units.py which specifies functions for handling units and conve…
bfoley12 Jul 9, 2026
cb904bf
Logic for 'pivoting' from a single contribution with various conditio…
bfoley12 Jul 9, 2026
98a312e
Bringing condition into contribution logic - aggregation, identifiers…
bfoley12 Jul 9, 2026
9f88403
Added testing
bfoley12 Jul 9, 2026
2f673df
Removed deprecated test file and fixed test case
bfoley12 Jul 9, 2026
34144a7
Added exception for Contribution.data key handling
bfoley12 Jul 10, 2026
a7cf414
Added types for handling various common restrictions on strings and a…
bfoley12 Jul 10, 2026
a916157
Strengthened annotated data formatting by introducing the AnnotatedDa…
bfoley12 Jul 10, 2026
cda2aac
Moved string parsing to types.py and implemented new types for string…
bfoley12 Jul 10, 2026
94408e8
Improved how contributions are 'pivoted'/'expanded' when multiple con…
bfoley12 Jul 10, 2026
69c5e68
Added path method to handle fanning out of contribution patches in th…
bfoley12 Jul 10, 2026
04cb898
Switched patch from repo to service layer
bfoley12 Jul 10, 2026
2bae6b1
Removed duplicated logic and dead methods. Added logic for conditions
bfoley12 Jul 10, 2026
cc7190a
Added additional strings for documenting fields
bfoley12 Jul 10, 2026
f25d978
Added tests
bfoley12 Jul 10, 2026
c94ee5d
Comments to ignore pyright warnings
bfoley12 Jul 10, 2026
b11e52b
Improved readme to describe contribution data
bfoley12 Jul 10, 2026
9fd6fd7
Corrected tests using outdated conditions
bfoley12 Jul 10, 2026
fae70ce
Added NFKCStr and applied new string types across domain models
bfoley12 Jul 10, 2026
167f177
Added comments
bfoley12 Jul 13, 2026
e5e68e4
Made AnnotatedData the source of data and unit handling. Implemented …
bfoley12 Jul 13, 2026
ed6380b
Synced identifier to be SearchStr instead of bare str in Filter
bfoley12 Jul 13, 2026
e4a8022
Allow manual specification of username and groups when in dev context
bfoley12 Jul 13, 2026
b58500a
Added tests
bfoley12 Jul 13, 2026
2a7672c
Added hypothesis package for testing annotated contribution data
bfoley12 Jul 13, 2026
ab6e41f
Contributions do not accept id for POST anymore
bfoley12 Jul 13, 2026
932d11f
Added models and logic to support updates of Contributions and Contri…
bfoley12 Jul 23, 2026
54e1882
Refactored project models and updated repo and tests to respect new f…
bfoley12 Jul 23, 2026
c69efca
Updated references to old 'patch_pivot_row' to 'update_contribution_b…
bfoley12 Jul 23, 2026
9beef12
Fixed bug where inserting a document via upsert_contribution_by_id ca…
bfoley12 Jul 23, 2026
03f6d56
Added can_write check on upsert_contribution_by_id to prevent user fr…
bfoley12 Jul 23, 2026
4d52ead
Removed display and added precision to Contribution.data leaves. Also…
bfoley12 Jul 24, 2026
8207cf7
Extracted Contributions.data specific functions to contributions/data
bfoley12 Jul 24, 2026
5d0a4b9
Updated tests to reference contributions/data
bfoley12 Jul 24, 2026
aa00697
Only add input_* fields to leaf if we modified fields from their inpu…
bfoley12 Jul 24, 2026
27d03b6
Set Project.owner to the caller that created the Project
bfoley12 Jul 27, 2026
e6212f7
_fields arguments in routers now accept None (default fields), [] (id…
bfoley12 Jul 28, 2026
644b6a5
Encoded Structure.lattice.matrix as a 3x3 matrix rather than a polars…
bfoley12 Jul 28, 2026
73acc5e
Removed 'needs_build' from Contributions
bfoley12 Jul 28, 2026
fab2f2b
Added BulkUpdateSummary to handle the bulk update of Contributions, w…
bfoley12 Jul 28, 2026
a17069c
Restored out-of-scope diffs to origin/dev
bfoley12 Jul 28, 2026
f28bd77
Merged dev into this branch to fix merge conflicts
bfoley12 Jul 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions mpcontribs-api/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,83 @@
- https://contribs-api.materialsproject.org
- https://ml-api.materialsproject.org
- https://lightsources-api.materialsproject.org

## Contribution `data`

A contribution's `data` is a hierarchical JSON object (max nesting depth 7). Lists are allowed, and
dictionaries inside them have their keys coerced and validated like any other key. The API applies
two write-time transforms that are important for clients to understand, because **the data you read
back can differ from the data you submitted**.

### Key coercion to `snake_case`

Every dictionary key in `data` is coerced to `snake_case` on write:

- casing is lowercased,
- `camelCase` / `PascalCase` boundaries are split (`bandGap` → `band_gap`),
- any run of spaces, hyphens, or punctuation collapses to a single underscore.

So `"bandGap"`, `"Band Gap"`, and `"band-gap"` are all stored as `"band_gap"`. Keys must be ASCII
and must not reduce to an empty string (e.g. `"***"` is rejected). Colliding keys after coercion
(`"Band Gap"` and `"band_gap"` in the same object) are rejected.

### Annotated keys: units and conditions

A **top-level** key may carry an annotation in parentheses:

```
"name (unit, condition1=value1, condition2=value2, ...)"
```

- The single token without an `=` is the **unit** (`eV`, `S/cm`, `K`, …). Units are stored verbatim
(not coerced) so they round-trip through [Pint](https://pint.readthedocs.io/).
- Each `k=v` token is a **condition**; condition names are coerced to `snake_case` like other keys.
- `name` may be a dotted path (`"transport.conductivity (S/cm)"`) to nest the value.
- A key with no parentheses is a plain, fully backward-compatible key.

Recognized units are canonicalized to SI base units on write. Magnitudes may carry uncertainty as
`"4.2(3)"`, `"4.2+/-0.3"`, or `"4.2±0.3"`.

A value submitted with a unit annotation is stored as an **annotated leaf**:

| field | meaning |
| --------------------------- | ---------------------------------------------------------------------------- |
| `value` / `unit` | SI-canonical form (or submitted form if the unit is unrecognized/dimensionless) |
| `input_value` / `input_unit`| the magnitude and unit exactly as submitted |
| `error` | SI-propagated standard deviation (only when an uncertainty was given) |
| `display` | human-readable rendering of the submitted magnitude/unit, e.g. `"4.2+/-0.3 eV"` |

### Pivoting on conditions

If any key carries conditions, a single submitted contribution is **expanded into one contribution
per distinct condition signature**. Each resulting contribution stores its conditions as ordinary
columns, plus its own measurements, plus every condition-less ("broadcast") column, and is stamped
with a server-computed `condition_key` identity. For example:

```jsonc
// submitted
{
"conductivity (S/cm, T=300K)": 1.2,
"conductivity (S/cm, T=400K)": 3.4,
"sample": "A" // broadcast to every pivoted row
}
// stored: two contributions, one per temperature, each with `t`, `conductivity`, and `sample`
```

Submissions that pivot on conditions may **not** include components (structures/tables/attachments):
insert the pivoted contributions first, then attach components to the rows you want.

**PATCH semantics.** `PATCH /contributions/{id}` runs the same expansion on its `data`:

- Condition-less `data` patches the addressed contribution in place (units are still annotated); its
`condition_key` is unchanged.
- `data` that carries conditions *fans out*: each condition signature updates the existing pivoted
row (under the same `project`/`identifier`/`version`) that already has the matching `condition_key`.
A PATCH never creates new rows or rewrites a `condition_key`, so a signature that matches no stored
row is rejected. Non-`data` fields on the patch apply to every row it touches. The response is the
list of updated contributions.

> The write path (key grammar, coercion, and validation) lives in
> `src/mpcontribs_api/domains/_shared/types.py`; expansion/pivoting lives in
> `src/mpcontribs_api/domains/contributions/pivot.py`; unit handling lives in
> `src/mpcontribs_api/domains/_shared/units.py`.
4 changes: 2 additions & 2 deletions mpcontribs-api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def start(program):
cluster = labels[f"{prefix}.cluster"].split("/", 1)[1]
family = labels[f"{prefix}.task-definition-family"]
version = int(labels[f"{prefix}.task-definition-version"])
task_arns = client.list_tasks(cluster=cluster, family=family).get("taskArns", [])
tasks = client.describe_tasks(cluster=cluster, tasks=task_arns).get("tasks", [])
task_arns = client.list_tasks(cluster=cluster, family=family).get("taskArns", []) # pyright: ignore[reportAttributeAccessIssue] - boto3 dynamically dispatches, this is valid with ECS
tasks = client.describe_tasks(cluster=cluster, tasks=task_arns).get("tasks", []) # pyright: ignore[reportAttributeAccessIssue] - boto3 dynamically dispatches, this is valid with ECS
ntasks = 0

for task in tasks:
Expand Down
3 changes: 3 additions & 0 deletions mpcontribs-api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ dependencies = [
"structlog>=25.5.0",
"supervisor>=4.3.0",
"types-aiobotocore[s3]>=3.7.0",
"pint>=0.25.3",
"uncertainties>=3.2.3",
]

[project.urls]
Expand All @@ -65,6 +67,7 @@ dev = [
"pytest-xdist>=3.8.0",
"httpx2>=0.28.0",
"pytest-asyncio>=1.4.0",
"hypothesis>=6.100.0",
]

[tool.pytest.ini_options]
Expand Down
86 changes: 85 additions & 1 deletion mpcontribs-api/src/mpcontribs_api/_openapi.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,84 @@
# Client-facing documentation for the Contribution ``data`` field. Referenced as the OpenAPI schema
# description on the input/output models (see ``contributions.models``) so it renders in the docs UI
# and generated clients. Keep these in sync with the behavior in ``contributions.pivot`` and
# ``_shared.units``.

CONTRIBUTION_DATA_INPUT_DESCRIPTION = """\
Hierarchical, JSON-object data for the contribution. Nesting deeper than 7 levels is rejected. Lists
are allowed; any dictionaries inside them have their keys coerced and validated like every other key.

**Key coercion (important):** every dictionary key is coerced to `snake_case` on write. Casing is
lowercased, `camelCase`/`PascalCase` boundaries are split, and any run of spaces/hyphens/punctuation
collapses to a single underscore. So `"bandGap"`, `"Band Gap"`, and `"band-gap"` are all stored as
`"band_gap"`, and the **keys you read back may differ from the keys you submitted**. Keys must be
ASCII and must not reduce to an empty string (e.g. `"***"` is rejected).

**Reserved keys:** `value`, `unit`, `input_value`, `input_unit`, `error`, `input_error`,
`precision`, and `display` are reserved for the stored value-leaf shape and may **not** be used as
data keys (a key that coerces to any of these is rejected).

**Values become structured leaves.** Any value that reads as a number is stored as a *quantity leaf*
(see the response schema) capturing its value, unit, uncertainty, and precision; anything that does
not (a word like `"cubic"`, a boolean, a list) is stored as-is. Scalars **inside lists** are left
as-is (a list is treated as array data, not a column of measurements).

**Two ways to give a unit (both accepted, they converge):**

1. In the **key**, via an annotation in parentheses:
`"name (unit, condition1=value1, condition2=value2, ...)"` — with the value a bare number
(`{"bandgap (eV)": 5}`).
2. In the **value** string itself (`{"bandgap": "5 eV"}`).

Annotation rules: the single token without an `=` is the **unit** (e.g. `eV`, `S/cm`, `K`), left
verbatim so it round-trips through Pint; each `k=v` token is a **condition** (names coerced to
`snake_case`); `name` may be a dotted path (`"transport.conductivity (S/cm)"`) to nest the value; a
key with no parentheses is a plain key. If a unit is given in **both** the key and the value and they
differ, the **key's unit wins** — the value is converted into it (a dimensional mismatch is rejected).

Recognized units are canonicalized to SI base units on write (the submitted form is preserved too —
see the response schema); an unrecognized unit is kept verbatim. Magnitudes may carry uncertainty as
`"4.2(3)"`, `"4.2+/-0.3"`, or `"4.2±0.3"`. Precision (significant figures) is captured from a string
magnitude such as `"1.000"`; a bare JSON number carries no trailing-zero information.

The server does **not** produce a formatted `display` string — the response returns the structured
fields (`input_value`/`input_unit`/`input_error`/`precision`) so clients render values however they
prefer. (`display` remains a reserved key for backward compatibility with older stored leaves.)

**Pivoting on conditions:** if any key carries conditions, the single submission is *expanded* into
one contribution per distinct condition signature. Each resulting contribution stores its conditions
as ordinary columns plus its own measurements plus every condition-less ("broadcast") column, and
gets a server-computed `condition_key` identity. Submissions that pivot on conditions may **not**
include components (structures/tables/attachments) — insert the pivoted contributions first, then
attach components.

**On PATCH:** the same expansion runs. Condition-less data patches the addressed contribution in
place (units still annotated). Data carrying conditions *fans out*: each signature updates the
existing pivoted row that already has the matching `condition_key` (a PATCH never creates rows or
changes a row's `condition_key`), so a signature with no matching stored row is rejected.
"""

CONTRIBUTION_DATA_OUTPUT_DESCRIPTION = """\
Hierarchical contribution data. Keys are stored in `snake_case` (see the write schema for the
coercion rules), so they may differ from the keys originally submitted.

Any value that reads as a number is stored as a **quantity leaf** object:

- `value` / `unit`: the SI-canonical magnitude and unit (or the submitted form when the unit is
unrecognized or dimensionless)
- `input_value` / `input_unit`: the magnitude and unit as submitted (omitted for a bare, unitless,
exact number that ``value`` already fully describes)
- `error`: the (SI-propagated) standard deviation — present only when the magnitude carried an
uncertainty; `input_error` is the same in the submitted unit
- `precision`: the number of significant figures the submission carried — present only when it is
derivable (a string magnitude such as `"1.000"`)

There is no server-rendered `display` string: `input_value`, `input_unit`, `input_error`, and
`precision` reproduce the submitted form exactly, so a client can format the value however it likes.

Values that are not numeric (words, booleans, lists) keep whatever JSON shape they were submitted
with (after `snake_case` key coercion).
"""

openapi_tags = [
{
"name": "projects",
Expand All @@ -15,7 +96,10 @@
"Each contribution uses `mp-id` or composition as identifier to associate its data with the according entries "
"on MP. Only admins or users on the project can create, update or delete contributions, and while unpublished, "
"retrieve its data or view it on the Portal. Contribution components (tables, structures, and attachments) are "
"deleted along with a contribution.",
"deleted along with a contribution. **Note:** `data` keys are coerced to `snake_case` on write and may carry "
"unit/condition annotations of the form `name (unit, cond=value, ...)`; keys with conditions cause the "
"submission to pivot into one contribution per condition signature. See the `data` field on the request and "
"response schemas for the full grammar and the annotated-value shape.",
},
{
"name": "structures",
Expand Down
3 changes: 1 addition & 2 deletions mpcontribs-api/src/mpcontribs_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ async def _setup_mongo(app: FastAPI, settings: Settings, stack: AsyncExitStack)
Structure,
Table,
],
allow_index_dropping=True, # allow old indices to be dropped from MongoDB when not specified in this package
)


Expand Down Expand Up @@ -132,8 +133,6 @@ def create_app(settings: Settings | None = None) -> FastAPI:
)

# Reject oversized request bodies before they're buffered into memory. Added before
# RequestContextMiddleware: Starlette inserts each added middleware at the front of the stack,
# so the later-added RequestContextMiddleware stays outermost and still access-logs rejections.
app.add_middleware(BodySizeLimitMiddleware, max_bytes=settings.mongo.max_request_bytes)
# Add request context to the logger
app.add_middleware(RequestContextMiddleware)
Expand Down
24 changes: 24 additions & 0 deletions mpcontribs-api/src/mpcontribs_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,27 @@ def _clamp_concurrency(self):
return self


class MPContribsSettings(BaseModel):
max_contrib_data_depth: int = Field(
default=7, description="The max number of levels allowed in a Contribution's data dictionary."
)
max_columns: int = Field(
default=160,
description="The maximum allowed number of columns for a contribution (len(Contribution.data)), "
"which also gets reflected in Project.columns",
)
max_components: int = Field(
default=10,
description="The maximum allowed number of a single Component type (Structure, Table, Attachment) on a single "
"Contribution",
)
float_precision: int = Field(
default=6,
description="The precision with which to store floats in MongoDB. "
"Primarily used to handle Contribution.data values.",
)


class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
Expand All @@ -170,6 +191,9 @@ class Settings(BaseSettings):
# requires uri and db_name
mongo: MongoSettings

# MPContribs_mpcontribs__*
mpcontribs: MPContribsSettings = Field(default_factory=MPContribsSettings)

# MPContribs_aws__*
aws: AwsSettings = Field(default_factory=AwsSettings)

Expand Down
11 changes: 11 additions & 0 deletions mpcontribs-api/src/mpcontribs_api/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,14 @@ def require_user(user: UserDep) -> User:
raise AuthenticationError("authentication required")
return user


# AuthedDep = Annotated[User, Depends(require_user)]


# def require_role(role: str):
# def checker(user: AuthedDep) -> User:
# if not user.has_role(role):
# raise PermissionError(required_role=role)
# return user

# return Annotated[User, Depends(checker)]
18 changes: 17 additions & 1 deletion mpcontribs-api/src/mpcontribs_api/domains/_shared/bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,27 @@ class BulkWriteSummary[T](BaseModel):
failed: list[BulkFailure]


class BulkDeleteSummary(BaseModel):
class BulkDeleteSummary[T](BaseModel):
num_deleted: int
num_children_deleted: int


class BulkUpdateSummary(BaseModel):
"""Result of a filtered bulk update.

No per-item result, reflects MongoDB's bulk update response.

Attributes:
matched: documents the (scoped) filter matched
modified: documents whose stored value actually changed
projects: the projects the update touched, so the caller can see its blast radius
"""

matched: int
modified: int
projects: list[str]


def bulk_failure_from_exception(index: int, identifier: dict[str, Any] | None, exc: BaseException) -> BulkFailure:
"""Translate any exception into a BulkFailure entry.

Expand Down
37 changes: 36 additions & 1 deletion mpcontribs-api/src/mpcontribs_api/domains/_shared/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,28 @@

from fastapi_filter.contrib.beanie import Filter

from mpcontribs_api.domains._shared.types import nfc_normalize


def _normalize_query_values(value: Any) -> Any:
"""Recursively NFC-normalize every string in a built query condition value.

fastapi-filter wraps a filter's value into operator dicts before it reaches us (``{"$ne": v}``,
``{"$in": [...]}``, ``{"$regex": ".*v.*", "$options": "i"}``), so a query term can be nested a
few levels deep. Normalizing every string — bare, inside operator dicts, and inside ``$in``/
``$nin`` lists — means a term typed with a canonically-equivalent spelling (e.g. the OHM SIGN
U+2126 vs the Greek omega) matches NFC-normalized stored data. NFC is a no-op on ASCII, so ids,
md5 hex, and regex metacharacters are untouched. Dict *keys* (field names, ``$``-operators) are
ASCII and left as-is.
"""
if isinstance(value, str):
return nfc_normalize(value)
if isinstance(value, Mapping):
return {key: _normalize_query_values(sub) for key, sub in value.items()}
if isinstance(value, list):
return [_normalize_query_values(item) for item in value]
return value


class BaseFilter(Filter):
"""Base filter that bridges Beanie's ``_id`` alias and fastapi-filter's raw field names.
Expand All @@ -13,11 +35,24 @@ class BaseFilter(Filter):
nothing, while a direct ``Document.id == x`` lookup (which Beanie resolves to ``_id``) succeeds.
Remapping the ``id`` key to ``_id`` here keeps the two read paths consistent.

String values are also NFC-normalized (see :func:`_normalize_query_values`) so queries match the
NFC-normalized units, labels, and other display strings stored on the write path.

Store/query normalization invariant: this NFC pass is a global catch-all and only lines up with
fields stored at NFC or lighter. A field normalized *more* aggressively on write (e.g. a
``SearchStr`` identifier that is NFKC-folded and casefolded, or an ``NFKCStr`` name) must declare
that same normalization type on its filter field so the query value is folded identically —
otherwise an exact/``__in``/``__neq`` lookup silently misses. See ``ContributionFilter.identifier``
(SearchStr) and the component ``name`` filters (NFKCStr) for the pattern.

Domain filters should subclass this instead of fastapi-filter's ``Filter`` directly.
"""

def _get_filter_conditions(self, nesting_depth: int = 1) -> list[tuple[Mapping[str, Any], Mapping[str, Any]]]:
return [
({"_id" if key == "id" else key: value for key, value in condition.items()}, options)
(
{("_id" if key == "id" else key): _normalize_query_values(value) for key, value in condition.items()},
options,
)
for condition, options in super()._get_filter_conditions(nesting_depth)
]
7 changes: 4 additions & 3 deletions mpcontribs-api/src/mpcontribs_api/domains/_shared/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pymongo.results import DeleteResult

from mpcontribs_api import pagination
from mpcontribs_api.domains._shared.types import MD5Hash
from mpcontribs_api.domains._shared.types import MD5Hash, NFKCStr
from mpcontribs_api.projection import SparseFieldsModel


Expand Down Expand Up @@ -84,7 +84,8 @@ class ComponentIn(BaseModel):
fields for their resource.
"""

name: str
# NFKC-normalized so compatibility-equivalent spellings of a name collapse to one stored form.
name: NFKCStr


class Component(BaseDocumentWithInput[PydanticObjectId]):
Expand All @@ -95,7 +96,7 @@ class Component(BaseDocumentWithInput[PydanticObjectId]):
never define a component's content identity.
"""

name: str
name: NFKCStr
# Server-computed; the placeholder default is overwritten by ``_recompute_md5`` on validation.
md5: MD5Hash = Field(default="0" * 32)

Expand Down
Loading