atomscale 0.8.2__tar.gz → 0.8.3__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.
- {atomscale-0.8.2 → atomscale-0.8.3}/PKG-INFO +1 -1
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/rheed_stream.pyi +1 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/src/initialize.rs +76 -3
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/src/lib.rs +34 -7
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale.egg-info/PKG-INFO +1 -1
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale.egg-info/SOURCES.txt +2 -0
- atomscale-0.8.3/tests/_mock_http_server.py +122 -0
- atomscale-0.8.3/tests/test_rheed_stream.py +279 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/.github/workflows/release.yml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/.github/workflows/testing.yml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/.github/workflows/upgrade_dependencies.yml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/.gitignore +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/.pre-commit-config.yaml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/CHANGELOG.md +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/LICENSE +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/MANIFEST.in +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/README.md +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/atomicds-shim-dist/pyproject.toml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/Makefile +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/_templates/custom-class-template.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/_templates/custom-module-template.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/conf.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/index.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/inspect-results.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/poll-timeseries.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/quickstart.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/search-data.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/stream-rheed.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/guides/upload-data.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/index.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/make.bat +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/docs/modules.rst +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/examples/general_use.ipynb +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/examples/rheed_streaming.ipynb +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/examples/timeseries_polling.ipynb +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/examples/vxwse2-placeholder/task1_films.ipynb +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/examples/vxwse2-placeholder/task1_sapphire.ipynb +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/examples/vxwse2-placeholder/task2_composition.ipynb +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/pyproject.toml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.10.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.10_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.11.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.11_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.12.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.12_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.9.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.9_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.10.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.10_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.11.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.11_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.12.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.12_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.9.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.9_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.10.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.10_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.11.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.11_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.12.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.12_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.9.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.9_extras.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/setup.cfg +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomicds/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/client.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/core/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/core/client.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/core/files.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/core/utils.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/group.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/metrology.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/optical.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/photoluminescence.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/raman.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/rheed_image.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/rheed_video.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/similarity_trajectory.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/unknown.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/results/xps.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/Cargo.lock +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/Cargo.toml +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/src/upload.rs +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/streaming/src/utils.rs +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/align.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/metrology.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/optical.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/polling.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/provider.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/registry.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/rheed.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/sample.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale/timeseries/similarity.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale.egg-info/dependency_links.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale.egg-info/requires.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/src/atomscale.egg-info/top_level.txt +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/__init__.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/conftest.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/data/test_rheed.mp4 +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_atomicds_alias.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_client.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_core.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_metrology.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_optical.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_photoluminescence.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_polling.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_raman.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_rheed_image.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_rheed_video.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_similarity_trajectory.py +0 -0
- {atomscale-0.8.2 → atomscale-0.8.3}/tests/test_xps.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use anyhow::{Context, Result};
|
|
2
2
|
use reqwest::Client;
|
|
3
3
|
use serde::{Deserialize, Serialize};
|
|
4
|
+
use serde_json::Value;
|
|
4
5
|
|
|
5
6
|
#[derive(Serialize, Debug)]
|
|
6
7
|
#[serde(rename_all = "snake_case")] // Ensures JSON fields are snake_case (e.g., data_id)
|
|
@@ -9,6 +10,20 @@ pub struct RHEEDStreamSettings {
|
|
|
9
10
|
pub rotational_period: f64,
|
|
10
11
|
pub rotations_per_min: f64,
|
|
11
12
|
pub fps_capture_rate: f64,
|
|
13
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
14
|
+
pub project_id: Option<String>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Project-related structs for GET /projects/ response
|
|
18
|
+
#[derive(Deserialize, Debug)]
|
|
19
|
+
struct DataConfiguration {
|
|
20
|
+
api_configuration: Option<Value>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[derive(Deserialize, Debug)]
|
|
24
|
+
struct ProjectSummary {
|
|
25
|
+
id: String,
|
|
26
|
+
configuration: Option<DataConfiguration>,
|
|
12
27
|
}
|
|
13
28
|
|
|
14
29
|
/// POST request to initialize a RHEED stream
|
|
@@ -58,10 +73,10 @@ pub async fn ensure_physical_sample_link(
|
|
|
58
73
|
api_key: &str,
|
|
59
74
|
data_id: &str,
|
|
60
75
|
sample_name: &str,
|
|
61
|
-
) -> Result<
|
|
76
|
+
) -> Result<String> {
|
|
62
77
|
let sample_name = sample_name.trim();
|
|
63
78
|
if sample_name.is_empty() {
|
|
64
|
-
|
|
79
|
+
anyhow::bail!("sample_name cannot be empty");
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
let list_url = format!("{base_endpoint}/physical_samples/");
|
|
@@ -102,7 +117,7 @@ pub async fn ensure_physical_sample_link(
|
|
|
102
117
|
let link_url = format!("{base_endpoint}/data_entries/physical_sample");
|
|
103
118
|
let link_body = LinkPhysicalSampleRequest {
|
|
104
119
|
data_ids: vec![data_id.to_string()],
|
|
105
|
-
physical_sample_id: sample_id,
|
|
120
|
+
physical_sample_id: sample_id.clone(),
|
|
106
121
|
};
|
|
107
122
|
|
|
108
123
|
client
|
|
@@ -115,5 +130,63 @@ pub async fn ensure_physical_sample_link(
|
|
|
115
130
|
.error_for_status()
|
|
116
131
|
.context("physical sample link returned error status")?;
|
|
117
132
|
|
|
133
|
+
Ok(sample_id)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Updates the project's tracking_physical_sample_id in its configuration.
|
|
137
|
+
/// Fetches current configuration, updates the tracking sample, and POSTs back.
|
|
138
|
+
pub async fn update_project_tracking_sample(
|
|
139
|
+
client: &Client,
|
|
140
|
+
base_endpoint: &str,
|
|
141
|
+
api_key: &str,
|
|
142
|
+
project_id: &str,
|
|
143
|
+
physical_sample_id: &str,
|
|
144
|
+
) -> Result<()> {
|
|
145
|
+
// GET /projects/ to find the project and its current configuration
|
|
146
|
+
let projects_url = format!("{base_endpoint}/projects/");
|
|
147
|
+
let projects: Vec<ProjectSummary> = client
|
|
148
|
+
.get(&projects_url)
|
|
149
|
+
.header("X-API-KEY", api_key)
|
|
150
|
+
.send()
|
|
151
|
+
.await
|
|
152
|
+
.context("failed to request projects")?
|
|
153
|
+
.error_for_status()
|
|
154
|
+
.context("projects list returned error status")?
|
|
155
|
+
.json()
|
|
156
|
+
.await
|
|
157
|
+
.context("failed to deserialize projects list")?;
|
|
158
|
+
|
|
159
|
+
// Find the project by ID
|
|
160
|
+
let project = projects
|
|
161
|
+
.into_iter()
|
|
162
|
+
.find(|p| p.id == project_id)
|
|
163
|
+
.ok_or_else(|| anyhow::anyhow!("project with id {} not found", project_id))?;
|
|
164
|
+
|
|
165
|
+
// Build updated configuration, preserving existing fields
|
|
166
|
+
let mut config = match project.configuration {
|
|
167
|
+
Some(data_config) => data_config.api_configuration.unwrap_or_else(|| Value::Object(Default::default())),
|
|
168
|
+
None => Value::Object(Default::default()),
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Update tracking_physical_sample_id in the configuration
|
|
172
|
+
if let Value::Object(ref mut map) = config {
|
|
173
|
+
map.insert(
|
|
174
|
+
"tracking_physical_sample_id".to_string(),
|
|
175
|
+
Value::String(physical_sample_id.to_string()),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// POST /projects/{project_id}/configuration with the updated config
|
|
180
|
+
let config_url = format!("{base_endpoint}/projects/{project_id}/configuration");
|
|
181
|
+
client
|
|
182
|
+
.post(&config_url)
|
|
183
|
+
.header("X-API-KEY", api_key)
|
|
184
|
+
.json(&config)
|
|
185
|
+
.send()
|
|
186
|
+
.await
|
|
187
|
+
.context("failed to update project configuration")?
|
|
188
|
+
.error_for_status()
|
|
189
|
+
.context("project configuration update returned error status")?;
|
|
190
|
+
|
|
118
191
|
Ok(())
|
|
119
192
|
}
|
|
@@ -13,7 +13,10 @@ mod utils;
|
|
|
13
13
|
use utils::{generic_post, init_tracing_once};
|
|
14
14
|
|
|
15
15
|
mod initialize;
|
|
16
|
-
use initialize::{
|
|
16
|
+
use initialize::{
|
|
17
|
+
ensure_physical_sample_link, post_for_initialization, update_project_tracking_sample,
|
|
18
|
+
RHEEDStreamSettings,
|
|
19
|
+
};
|
|
17
20
|
|
|
18
21
|
mod upload;
|
|
19
22
|
use upload::{
|
|
@@ -109,7 +112,7 @@ impl RHEEDStreamer {
|
|
|
109
112
|
}
|
|
110
113
|
|
|
111
114
|
////Initialize stream
|
|
112
|
-
/// initialize(self,
|
|
115
|
+
/// initialize(self, fps: float, rotations_per_min: float, chunk_size: int, stream_name: Optional[str] = None, physical_sample: Optional[str] = None, project_id: Optional[str] = None) -> str
|
|
113
116
|
///
|
|
114
117
|
/// Creates a new **remote data item** for this stream and returns its `data_id`.
|
|
115
118
|
/// Also captures runtime configuration used for subsequent chunk uploads.
|
|
@@ -121,21 +124,24 @@ impl RHEEDStreamer {
|
|
|
121
124
|
/// After streaming via `run(...)` or `push(...)`, call `finalize(data_id)` to mark the stream as complete.
|
|
122
125
|
///
|
|
123
126
|
/// Args:
|
|
124
|
-
/// stream_name (Optional[str]): Human-readable name shown in the platform. If `None` or an empty string,
|
|
125
|
-
/// a default like `"RHEED Stream @ 1:23PM"` is used.
|
|
126
127
|
/// fps (float): Capture rate in frames per second.
|
|
127
128
|
/// rotations_per_min (float): Wafer/crystal rotations per minute; use `0.0` for stationary operation.
|
|
128
129
|
/// chunk_size (int): The **intended** number of frames per chunk you will send with `run(...)` or `push(...)`.
|
|
130
|
+
/// stream_name (Optional[str]): Human-readable name shown in the platform. If `None` or an empty string,
|
|
131
|
+
/// a default like `"RHEED Stream @ 1:23PM"` is used.
|
|
129
132
|
/// physical_sample (Optional[str]): Name of a physical sample to associate with the data item; matched case-insensitively or created if missing.
|
|
133
|
+
/// project_id (Optional[str]): UUID of a project to associate with the stream. When provided along with
|
|
134
|
+
/// `physical_sample`, the project's `tracking_physical_sample_id` configuration is automatically updated
|
|
135
|
+
/// to link the physical sample to the project for growth monitoring.
|
|
130
136
|
///
|
|
131
137
|
/// Returns:
|
|
132
138
|
/// str: The created `data_id` for this stream.
|
|
133
139
|
///
|
|
134
140
|
/// Raises:
|
|
135
141
|
/// RuntimeError: If the initialization POST fails.
|
|
136
|
-
#[pyo3(signature = (fps, rotations_per_min, chunk_size, stream_name=None, physical_sample=None))]
|
|
142
|
+
#[pyo3(signature = (fps, rotations_per_min, chunk_size, stream_name=None, physical_sample=None, project_id=None))]
|
|
137
143
|
#[pyo3(
|
|
138
|
-
text_signature = "(fps, rotations_per_min, chunk_size, stream_name=None, physical_sample=None)"
|
|
144
|
+
text_signature = "(fps, rotations_per_min, chunk_size, stream_name=None, physical_sample=None, project_id=None)"
|
|
139
145
|
)]
|
|
140
146
|
fn initialize(
|
|
141
147
|
&mut self,
|
|
@@ -144,6 +150,7 @@ impl RHEEDStreamer {
|
|
|
144
150
|
chunk_size: usize,
|
|
145
151
|
stream_name: Option<String>,
|
|
146
152
|
physical_sample: Option<String>,
|
|
153
|
+
project_id: Option<String>,
|
|
147
154
|
) -> PyResult<String> {
|
|
148
155
|
// Guard: chunk_size must be >= ceil(2 * fps)
|
|
149
156
|
let min_chunk = (2.0 * fps).ceil() as usize;
|
|
@@ -165,6 +172,10 @@ impl RHEEDStreamer {
|
|
|
165
172
|
.map(|s| s.trim().to_string())
|
|
166
173
|
.filter(|s| !s.is_empty());
|
|
167
174
|
|
|
175
|
+
let project_id = project_id
|
|
176
|
+
.map(|s| s.trim().to_string())
|
|
177
|
+
.filter(|s| !s.is_empty());
|
|
178
|
+
|
|
168
179
|
let fpr = (fps * 60.0) / rotations_per_min;
|
|
169
180
|
|
|
170
181
|
#[allow(clippy::redundant_field_names)]
|
|
@@ -173,6 +184,7 @@ impl RHEEDStreamer {
|
|
|
173
184
|
rotational_period: fpr,
|
|
174
185
|
rotations_per_min,
|
|
175
186
|
fps_capture_rate: fps,
|
|
187
|
+
project_id,
|
|
176
188
|
};
|
|
177
189
|
|
|
178
190
|
let base_endpoint = self.endpoint.clone();
|
|
@@ -192,9 +204,24 @@ impl RHEEDStreamer {
|
|
|
192
204
|
&data_id,
|
|
193
205
|
&sample_name,
|
|
194
206
|
);
|
|
195
|
-
self
|
|
207
|
+
let sample_id = self
|
|
208
|
+
.rt
|
|
196
209
|
.block_on(physical_sample_fut)
|
|
197
210
|
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
|
|
211
|
+
|
|
212
|
+
// If project_id was provided, update the project's tracking_physical_sample_id
|
|
213
|
+
if let Some(ref proj_id) = settings.project_id {
|
|
214
|
+
let update_project_fut = update_project_tracking_sample(
|
|
215
|
+
&self.client,
|
|
216
|
+
&base_endpoint,
|
|
217
|
+
&self.api_key,
|
|
218
|
+
proj_id,
|
|
219
|
+
&sample_id,
|
|
220
|
+
);
|
|
221
|
+
self.rt
|
|
222
|
+
.block_on(update_project_fut)
|
|
223
|
+
.map_err(|e| PyRuntimeError::new_err(e.to_string()))?;
|
|
224
|
+
}
|
|
198
225
|
}
|
|
199
226
|
|
|
200
227
|
self.fps = Some(fps);
|
|
@@ -95,6 +95,7 @@ src/atomscale/timeseries/rheed.py
|
|
|
95
95
|
src/atomscale/timeseries/sample.py
|
|
96
96
|
src/atomscale/timeseries/similarity.py
|
|
97
97
|
tests/__init__.py
|
|
98
|
+
tests/_mock_http_server.py
|
|
98
99
|
tests/conftest.py
|
|
99
100
|
tests/test_atomicds_alias.py
|
|
100
101
|
tests/test_client.py
|
|
@@ -105,6 +106,7 @@ tests/test_photoluminescence.py
|
|
|
105
106
|
tests/test_polling.py
|
|
106
107
|
tests/test_raman.py
|
|
107
108
|
tests/test_rheed_image.py
|
|
109
|
+
tests/test_rheed_stream.py
|
|
108
110
|
tests/test_rheed_video.py
|
|
109
111
|
tests/test_similarity_trajectory.py
|
|
110
112
|
tests/test_xps.py
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Subprocess-based mock HTTP server for testing Rust HTTP clients.
|
|
2
|
+
|
|
3
|
+
This module is designed to be run as a subprocess to provide true process
|
|
4
|
+
isolation when testing Rust HTTP clients (like reqwest) from Python tests.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python -m tests._mock_http_server <port> <json_response>
|
|
8
|
+
python -m tests._mock_http_server <port> <json_routes_dict>
|
|
9
|
+
|
|
10
|
+
The server:
|
|
11
|
+
- Listens on 127.0.0.1:<port>
|
|
12
|
+
- Prints "READY:<port>" to stdout when ready
|
|
13
|
+
- For simple mode: handles one POST request, prints "BODY:<json>" to stdout
|
|
14
|
+
- For routes mode: handles multiple requests based on path matching
|
|
15
|
+
- Responds with the provided JSON response
|
|
16
|
+
"""
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CaptureHandler(BaseHTTPRequestHandler):
|
|
23
|
+
"""HTTP handler that captures request body and returns configured response."""
|
|
24
|
+
|
|
25
|
+
def _handle_request(self, method: str):
|
|
26
|
+
"""Common handler for all HTTP methods."""
|
|
27
|
+
# Read request body if present
|
|
28
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
29
|
+
body = self.rfile.read(length) if length > 0 else b""
|
|
30
|
+
|
|
31
|
+
path = self.path
|
|
32
|
+
routes = getattr(self.server, "routes", None)
|
|
33
|
+
|
|
34
|
+
if routes:
|
|
35
|
+
# Routes mode: find matching route
|
|
36
|
+
response_data = None
|
|
37
|
+
for route_path, route_response in routes.items():
|
|
38
|
+
if path.startswith(route_path):
|
|
39
|
+
response_data = route_response
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
if response_data is None:
|
|
43
|
+
# Default response for unmatched routes
|
|
44
|
+
response_data = '""'
|
|
45
|
+
|
|
46
|
+
# Print request info for debugging
|
|
47
|
+
print(f"REQUEST:{method}:{path}:{body.decode() if body else ''}", flush=True)
|
|
48
|
+
else:
|
|
49
|
+
# Simple mode: single response for all requests
|
|
50
|
+
response_data = self.server.response_data
|
|
51
|
+
print(f"BODY:{body.decode()}", flush=True)
|
|
52
|
+
|
|
53
|
+
# Send response
|
|
54
|
+
self.send_response(200)
|
|
55
|
+
self.send_header("Content-Type", "application/json")
|
|
56
|
+
self.send_header("Content-Length", len(response_data))
|
|
57
|
+
self.end_headers()
|
|
58
|
+
self.wfile.write(response_data.encode())
|
|
59
|
+
|
|
60
|
+
def do_GET(self):
|
|
61
|
+
self._handle_request("GET")
|
|
62
|
+
|
|
63
|
+
def do_POST(self):
|
|
64
|
+
self._handle_request("POST")
|
|
65
|
+
|
|
66
|
+
def do_PUT(self):
|
|
67
|
+
self._handle_request("PUT")
|
|
68
|
+
|
|
69
|
+
def log_message(self, format, *args):
|
|
70
|
+
"""Suppress default logging."""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class MultiRequestServer(HTTPServer):
|
|
75
|
+
"""HTTP server that can handle multiple requests."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, *args, max_requests: int = 1, **kwargs):
|
|
78
|
+
super().__init__(*args, **kwargs)
|
|
79
|
+
self.max_requests = max_requests
|
|
80
|
+
self.request_count = 0
|
|
81
|
+
|
|
82
|
+
def handle_requests(self):
|
|
83
|
+
"""Handle up to max_requests requests."""
|
|
84
|
+
while self.request_count < self.max_requests:
|
|
85
|
+
self.handle_request()
|
|
86
|
+
self.request_count += 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_server(port: int, response_data: str) -> None:
|
|
90
|
+
"""Run the mock HTTP server."""
|
|
91
|
+
# Try to parse as routes dict
|
|
92
|
+
try:
|
|
93
|
+
routes = json.loads(response_data)
|
|
94
|
+
if isinstance(routes, dict) and routes.get("__routes__"):
|
|
95
|
+
# Routes mode
|
|
96
|
+
del routes["__routes__"]
|
|
97
|
+
max_requests = routes.pop("__max_requests__", 10)
|
|
98
|
+
server = MultiRequestServer(
|
|
99
|
+
("127.0.0.1", port), CaptureHandler, max_requests=max_requests
|
|
100
|
+
)
|
|
101
|
+
server.routes = routes
|
|
102
|
+
print(f"READY:{port}", flush=True)
|
|
103
|
+
server.handle_requests()
|
|
104
|
+
return
|
|
105
|
+
except (json.JSONDecodeError, TypeError):
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
# Simple mode: single request with static response
|
|
109
|
+
server = HTTPServer(("127.0.0.1", port), CaptureHandler)
|
|
110
|
+
server.response_data = response_data
|
|
111
|
+
print(f"READY:{port}", flush=True)
|
|
112
|
+
server.handle_request()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
if len(sys.argv) != 3:
|
|
117
|
+
print(f"Usage: {sys.argv[0]} <port> <json_response>", file=sys.stderr)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
port = int(sys.argv[1])
|
|
121
|
+
response_data = sys.argv[2]
|
|
122
|
+
run_server(port, response_data)
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Integration tests for RHEEDStreamer.
|
|
2
|
+
|
|
3
|
+
These tests use a subprocess-based HTTP server because the RHEEDStreamer uses
|
|
4
|
+
a Rust HTTP client (reqwest) which doesn't interact well with Python's threading
|
|
5
|
+
model used by pytest-httpserver. The subprocess approach provides true process
|
|
6
|
+
isolation and works reliably across platforms.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import socket
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Callable
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
# Path to the mock server module
|
|
18
|
+
_MOCK_SERVER_MODULE = Path(__file__).parent / "_mock_http_server.py"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MockServer:
|
|
22
|
+
"""A mock HTTP server running in a subprocess."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, port: int, response_data: str):
|
|
25
|
+
self.port = port
|
|
26
|
+
self.response_data = response_data
|
|
27
|
+
self._proc: subprocess.Popen | None = None
|
|
28
|
+
self._captured_body: dict | None = None
|
|
29
|
+
|
|
30
|
+
def start(self) -> None:
|
|
31
|
+
"""Start the server subprocess."""
|
|
32
|
+
self._proc = subprocess.Popen(
|
|
33
|
+
[sys.executable, str(_MOCK_SERVER_MODULE), str(self.port), self.response_data],
|
|
34
|
+
stdout=subprocess.PIPE,
|
|
35
|
+
stderr=subprocess.PIPE,
|
|
36
|
+
text=True,
|
|
37
|
+
)
|
|
38
|
+
# Wait for server to signal it's ready
|
|
39
|
+
ready_line = self._proc.stdout.readline()
|
|
40
|
+
if not ready_line.startswith("READY:"):
|
|
41
|
+
self.stop()
|
|
42
|
+
raise RuntimeError(f"Server failed to start: {ready_line}")
|
|
43
|
+
|
|
44
|
+
def stop(self) -> None:
|
|
45
|
+
"""Stop the server subprocess."""
|
|
46
|
+
if self._proc:
|
|
47
|
+
self._proc.terminate()
|
|
48
|
+
self._proc.wait(timeout=5)
|
|
49
|
+
self._proc = None
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def endpoint(self) -> str:
|
|
53
|
+
"""Return the server endpoint URL (no trailing slash)."""
|
|
54
|
+
return f"http://127.0.0.1:{self.port}"
|
|
55
|
+
|
|
56
|
+
def get_captured_body(self) -> dict | None:
|
|
57
|
+
"""Read and return the captured request body from the server."""
|
|
58
|
+
if self._proc and self._captured_body is None:
|
|
59
|
+
for line in self._proc.stdout:
|
|
60
|
+
if line.startswith("BODY:"):
|
|
61
|
+
self._captured_body = json.loads(line[5:])
|
|
62
|
+
break
|
|
63
|
+
return self._captured_body
|
|
64
|
+
|
|
65
|
+
def __enter__(self) -> "MockServer":
|
|
66
|
+
self.start()
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def __exit__(self, *args) -> None:
|
|
70
|
+
self.stop()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_free_port() -> int:
|
|
74
|
+
"""Get an available port on localhost."""
|
|
75
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
76
|
+
s.bind(("127.0.0.1", 0))
|
|
77
|
+
return s.getsockname()[1]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.fixture
|
|
81
|
+
def mock_server_factory() -> Callable[[str], MockServer]:
|
|
82
|
+
"""Factory fixture to create mock servers with custom responses."""
|
|
83
|
+
servers: list[MockServer] = []
|
|
84
|
+
|
|
85
|
+
def create(response_data: str) -> MockServer:
|
|
86
|
+
server = MockServer(_get_free_port(), response_data)
|
|
87
|
+
server.start()
|
|
88
|
+
servers.append(server)
|
|
89
|
+
return server
|
|
90
|
+
|
|
91
|
+
yield create
|
|
92
|
+
|
|
93
|
+
for server in servers:
|
|
94
|
+
server.stop()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class TestRHEEDStreamerInitialize:
|
|
98
|
+
"""Tests for RHEEDStreamer.initialize() method."""
|
|
99
|
+
|
|
100
|
+
def test_initialize_accepts_project_id_parameter(self):
|
|
101
|
+
"""Verify initialize() signature includes project_id parameter."""
|
|
102
|
+
import inspect
|
|
103
|
+
|
|
104
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
105
|
+
|
|
106
|
+
sig = inspect.signature(RHEEDStreamer.initialize)
|
|
107
|
+
params = list(sig.parameters.keys())
|
|
108
|
+
|
|
109
|
+
assert "project_id" in params
|
|
110
|
+
assert sig.parameters["project_id"].default is None
|
|
111
|
+
|
|
112
|
+
def test_initialize_validates_chunk_size(self):
|
|
113
|
+
"""Verify chunk_size validation (must be >= 2 * fps)."""
|
|
114
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
115
|
+
|
|
116
|
+
streamer = RHEEDStreamer(
|
|
117
|
+
api_key="test-api-key",
|
|
118
|
+
endpoint="http://localhost:9999",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
with pytest.raises(ValueError, match="chunk_size must be at least 2×fps"):
|
|
122
|
+
streamer.initialize(
|
|
123
|
+
fps=30.0,
|
|
124
|
+
rotations_per_min=0.0,
|
|
125
|
+
chunk_size=30, # Invalid: less than 2 * 30 = 60
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def test_initialize_sends_project_id_in_request(self, mock_server_factory):
|
|
129
|
+
"""Verify project_id is included in POST body when provided."""
|
|
130
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
131
|
+
|
|
132
|
+
server = mock_server_factory('"test-data-id-123"')
|
|
133
|
+
|
|
134
|
+
streamer = RHEEDStreamer(
|
|
135
|
+
api_key="test-api-key",
|
|
136
|
+
endpoint=server.endpoint,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
project_uuid = "550e8400-e29b-41d4-a716-446655440000"
|
|
140
|
+
data_id = streamer.initialize(
|
|
141
|
+
fps=30.0,
|
|
142
|
+
rotations_per_min=0.0,
|
|
143
|
+
chunk_size=60,
|
|
144
|
+
project_id=project_uuid,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert data_id == "test-data-id-123"
|
|
148
|
+
|
|
149
|
+
body = server.get_captured_body()
|
|
150
|
+
assert body is not None
|
|
151
|
+
assert body.get("project_id") == project_uuid
|
|
152
|
+
assert "data_item_name" in body
|
|
153
|
+
assert body.get("fps_capture_rate") == 30.0
|
|
154
|
+
|
|
155
|
+
def test_initialize_omits_project_id_when_none(self, mock_server_factory):
|
|
156
|
+
"""Verify project_id is omitted from POST body when not provided."""
|
|
157
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
158
|
+
|
|
159
|
+
server = mock_server_factory('"test-data-id-456"')
|
|
160
|
+
|
|
161
|
+
streamer = RHEEDStreamer(
|
|
162
|
+
api_key="test-api-key",
|
|
163
|
+
endpoint=server.endpoint,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
data_id = streamer.initialize(
|
|
167
|
+
fps=30.0,
|
|
168
|
+
rotations_per_min=0.0,
|
|
169
|
+
chunk_size=60,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
assert data_id == "test-data-id-456"
|
|
173
|
+
|
|
174
|
+
body = server.get_captured_body()
|
|
175
|
+
assert body is not None
|
|
176
|
+
assert "project_id" not in body
|
|
177
|
+
|
|
178
|
+
def test_initialize_omits_project_id_when_empty_string(self, mock_server_factory):
|
|
179
|
+
"""Verify empty string project_id is treated as None (omitted)."""
|
|
180
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
181
|
+
|
|
182
|
+
server = mock_server_factory('"test-data-id-789"')
|
|
183
|
+
|
|
184
|
+
streamer = RHEEDStreamer(
|
|
185
|
+
api_key="test-api-key",
|
|
186
|
+
endpoint=server.endpoint,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
data_id = streamer.initialize(
|
|
190
|
+
fps=30.0,
|
|
191
|
+
rotations_per_min=0.0,
|
|
192
|
+
chunk_size=60,
|
|
193
|
+
project_id="",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
assert data_id == "test-data-id-789"
|
|
197
|
+
|
|
198
|
+
body = server.get_captured_body()
|
|
199
|
+
assert body is not None
|
|
200
|
+
assert "project_id" not in body
|
|
201
|
+
|
|
202
|
+
def test_initialize_returns_data_id(self, mock_server_factory):
|
|
203
|
+
"""Verify initialize() returns the data_id from the server."""
|
|
204
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
205
|
+
|
|
206
|
+
expected_data_id = "abc-123-xyz"
|
|
207
|
+
server = mock_server_factory(f'"{expected_data_id}"')
|
|
208
|
+
|
|
209
|
+
streamer = RHEEDStreamer(
|
|
210
|
+
api_key="test-api-key",
|
|
211
|
+
endpoint=server.endpoint,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
data_id = streamer.initialize(
|
|
215
|
+
fps=30.0,
|
|
216
|
+
rotations_per_min=0.0,
|
|
217
|
+
chunk_size=60,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
assert data_id == expected_data_id
|
|
221
|
+
|
|
222
|
+
def test_initialize_updates_project_config_when_physical_sample_and_project_id(
|
|
223
|
+
self, mock_server_factory
|
|
224
|
+
):
|
|
225
|
+
"""Verify project configuration is updated with tracking_physical_sample_id.
|
|
226
|
+
|
|
227
|
+
When both physical_sample and project_id are provided, the SDK should:
|
|
228
|
+
1. POST /rheed/stream/ to create the stream
|
|
229
|
+
2. GET /physical_samples/ to list existing samples
|
|
230
|
+
3. POST /physical_samples/ to create the sample (if not found)
|
|
231
|
+
4. POST /data_entries/physical_sample to link sample to data entry
|
|
232
|
+
5. GET /projects/ to get current project configuration
|
|
233
|
+
6. POST /projects/{id}/configuration to update tracking_physical_sample_id
|
|
234
|
+
"""
|
|
235
|
+
from atomscale.streaming.rheed_stream import RHEEDStreamer
|
|
236
|
+
|
|
237
|
+
project_uuid = "550e8400-e29b-41d4-a716-446655440000"
|
|
238
|
+
sample_uuid = "660e8400-e29b-41d4-a716-446655440001"
|
|
239
|
+
|
|
240
|
+
# Configure routes for the multi-request flow
|
|
241
|
+
# Note: /physical_samples/ is used for both GET (returns list) and POST (returns created sample)
|
|
242
|
+
# The mock returns the same response for both, which works because:
|
|
243
|
+
# - GET expects a list - we return a list with the sample already existing
|
|
244
|
+
# - This skips the POST /physical_samples/ call since sample already exists
|
|
245
|
+
routes = json.dumps({
|
|
246
|
+
"__routes__": True,
|
|
247
|
+
"__max_requests__": 6,
|
|
248
|
+
"/rheed/stream/": '"test-data-id-999"',
|
|
249
|
+
"/physical_samples/": json.dumps([{"id": sample_uuid, "name": "Test Sample"}]),
|
|
250
|
+
"/data_entries/physical_sample": '"OK"',
|
|
251
|
+
"/projects/": json.dumps([{
|
|
252
|
+
"id": project_uuid,
|
|
253
|
+
"name": "Test Project",
|
|
254
|
+
"configuration": {
|
|
255
|
+
"api_configuration": {
|
|
256
|
+
"reference_group_type": "categorical",
|
|
257
|
+
"onboarding_complete": True
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}]),
|
|
261
|
+
f"/projects/{project_uuid}/configuration": '"OK"',
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
server = mock_server_factory(routes)
|
|
265
|
+
|
|
266
|
+
streamer = RHEEDStreamer(
|
|
267
|
+
api_key="test-api-key",
|
|
268
|
+
endpoint=server.endpoint,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
data_id = streamer.initialize(
|
|
272
|
+
fps=30.0,
|
|
273
|
+
rotations_per_min=0.0,
|
|
274
|
+
chunk_size=60,
|
|
275
|
+
physical_sample="Test Sample",
|
|
276
|
+
project_id=project_uuid,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
assert data_id == "test-data-id-999"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.10_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.11_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-macos-latest_py3.12_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.10_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.11_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.12_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-ubuntu-latest_py3.9_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.10_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.11_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.12_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
{atomscale-0.8.2 → atomscale-0.8.3}/requirements/requirements-windows-latest_py3.9_extras.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|