1#[cfg(feature = "python")]
7use pyo3::exceptions::PyValueError;
8#[cfg(feature = "python")]
9use pyo3::prelude::*;
10
11mod buf_gen;
12mod canary;
13mod column_gen;
14pub mod context;
15pub mod cpu_affinity;
16pub mod difficulty;
17mod entity_gen;
18mod fast_template;
19mod fk_remap;
20mod graph_gen;
21pub mod gt;
22mod hn_common;
23mod noise;
24pub mod pipeline;
25mod pool_lookup;
26pub mod rng;
27pub mod schema;
28
29pub use context::Context;
30pub use difficulty::DifficultyReport;
31pub use pipeline::{
32 PipelineConfig, PipelineOutput, PipelineStats, run_pipeline, run_pipeline_with_progress,
33};
34pub use schema::{
35 DomainSchema, EntitySchema, HnSchema, build_pipeline_config, chrono_now, load_schema,
36};
37
38#[cfg(feature = "python")]
42#[pyclass(skip_from_py_object)]
43#[derive(Clone)]
44pub struct GenerateResult {
45 #[pyo3(get)]
46 pub dataset: String,
47 #[pyo3(get)]
48 pub ground_truth: String,
49 #[pyo3(get)]
50 pub total_records: usize,
51 #[pyo3(get)]
52 pub exact_dups: usize,
53 #[pyo3(get)]
54 pub fuzzy_dups: usize,
55 #[pyo3(get)]
56 pub hard_negs: usize,
57 #[pyo3(get)]
58 pub uniques: usize,
59 #[pyo3(get)]
60 pub masters: usize,
61 #[pyo3(get)]
62 pub nodes: Option<String>,
63 #[pyo3(get)]
64 pub edges: Option<String>,
65}
66
67#[cfg(feature = "python")]
68#[pymethods]
69impl GenerateResult {
70 fn __repr__(&self) -> String {
71 format!(
72 "GenerateResult(dataset={:?}, ground_truth={:?}, total_records={}, exact_dups={}, fuzzy_dups={}, hard_negs={}, uniques={}, masters={})",
73 self.dataset,
74 self.ground_truth,
75 self.total_records,
76 self.exact_dups,
77 self.fuzzy_dups,
78 self.hard_negs,
79 self.uniques,
80 self.masters,
81 )
82 }
83
84 fn __str__(&self) -> String {
85 self.__repr__()
86 }
87}
88
89#[cfg(feature = "python")]
96#[pyfunction]
97fn default_singleton_master_fraction(difficulty: &str) -> f64 {
98 schema::default_singleton_master_fraction(difficulty)
99}
100
101#[cfg(feature = "python")]
102#[pyfunction]
103fn estimate_difficulty(
104 domain: &str,
105 size: usize,
106 seed: u64,
107 difficulty: &str,
108 schemas_dir: &str,
109 hard_neg_ratio: f64,
110) -> PyResult<String> {
111 let schema =
112 load_schema(domain, std::path::Path::new(schemas_dir)).map_err(PyValueError::new_err)?;
113 let report = crate::difficulty::estimate_difficulty(
114 domain,
115 size,
116 seed,
117 difficulty,
118 hard_neg_ratio,
119 &schema,
120 )
121 .map_err(PyValueError::new_err)?;
122 serde_json::to_string(&report).map_err(|e| PyValueError::new_err(e.to_string()))
123}
124
125#[cfg(feature = "python")]
126#[pyfunction]
127#[pyo3(signature = (
128 domain, size, seed, difficulty, output_dir, locale, pools_dir, schemas_dir,
129 output_format, hard_neg_ratio, singleton_master_fraction, generate_graph,
130 graph_format, only_entity=None, chunk_size=None,
131))]
132#[allow(clippy::too_many_arguments)]
133fn generate(
134 domain: &str,
135 size: usize,
136 seed: u64,
137 difficulty: &str,
138 output_dir: &str,
139 locale: &str,
140 pools_dir: &str,
141 schemas_dir: &str,
142 output_format: &str,
143 hard_neg_ratio: f64,
144 singleton_master_fraction: f64,
145 generate_graph: bool,
146 graph_format: &str,
147 only_entity: Option<&str>,
148 chunk_size: Option<usize>,
149) -> PyResult<GenerateResult> {
150 let schema =
151 load_schema(domain, std::path::Path::new(schemas_dir)).map_err(PyValueError::new_err)?;
152
153 if output_format != "ipc" && output_format != "parquet" {
154 return Err(PyValueError::new_err(format!(
155 "invalid output format '{output_format}'; expected 'ipc' or 'parquet'"
156 )));
157 }
158 if !schema::valid_difficulty_names().contains(&difficulty) {
159 return Err(PyValueError::new_err(format!(
160 "invalid difficulty '{difficulty}'; expected one of {:?}",
161 schema::valid_difficulty_names()
162 )));
163 }
164
165 let mut ctx = Context::new(domain, locale, pools_dir).map_err(PyValueError::new_err)?;
166
167 let run_id = schema::deterministic_run_id(
168 domain,
169 size,
170 seed,
171 difficulty,
172 hard_neg_ratio,
173 singleton_master_fraction,
174 locale,
175 only_entity,
176 chunk_size,
177 );
178
179 let output = match chunk_size {
180 Some(cs) if cs > 0 && cs < size => {
181 ctx.enable_watermark(domain, size, seed);
182 crate::pipeline::run_chunked(
183 &ctx,
184 domain,
185 size,
186 cs,
187 seed,
188 difficulty,
189 hard_neg_ratio,
190 singleton_master_fraction,
191 &schema,
192 &run_id,
193 output_format,
194 generate_graph,
195 graph_format,
196 only_entity,
197 output_dir,
198 None,
199 )
200 .map_err(PyValueError::new_err)?
201 }
202 _ => {
203 let config = build_pipeline_config(
204 domain,
205 size,
206 seed,
207 difficulty,
208 hard_neg_ratio,
209 singleton_master_fraction,
210 &schema,
211 &run_id,
212 output_format,
213 generate_graph,
214 graph_format,
215 only_entity,
216 )
217 .map_err(PyValueError::new_err)?;
218 ctx.enable_watermark(&config.domain, config.size, config.seed);
219 run_pipeline(&ctx, &config, output_dir).map_err(PyValueError::new_err)?
220 }
221 };
222
223 Ok(GenerateResult {
224 dataset: output.output_files.into_iter().next().unwrap_or_default(),
225 ground_truth: output.gt_file,
226 total_records: output.stats.total_records,
227 exact_dups: output.stats.exact_dups,
228 fuzzy_dups: output.stats.fuzzy_dups,
229 hard_negs: output.stats.hard_negs,
230 uniques: output.stats.uniques,
231 masters: output.stats.masters,
232 nodes: output.nodes,
233 edges: output.edges,
234 })
235}
236
237#[cfg(feature = "python")]
238#[pymodule]
239fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
240 m.add_function(wrap_pyfunction!(generate, m)?)?;
241 m.add_function(wrap_pyfunction!(estimate_difficulty, m)?)?;
242 m.add_function(wrap_pyfunction!(default_singleton_master_fraction, m)?)?;
243 m.add_class::<GenerateResult>()?;
244 Ok(())
245}