Skip to main content

zccache_cli/
lib.rs

1#![cfg(feature = "python")]
2
3use std::path::PathBuf;
4
5use pyo3::exceptions::{PyOSError, PyRuntimeError};
6use pyo3::prelude::*;
7use pyo3::types::PyAny;
8
9use zccache::cli::{
10    build_download_request, client_download, client_download_exists, client_session_end,
11    client_session_start, client_session_stats, client_start, client_status, client_stop,
12    fingerprint_check, fingerprint_invalidate, fingerprint_mark_failure, fingerprint_mark_success,
13    run_ino_convert_cached, DownloadParams, DownloadSource, InoConvertOptions, WaitMode,
14};
15
16fn runtime_to_py_err(message: String) -> PyErr {
17    PyErr::new::<PyRuntimeError, _>(message)
18}
19
20fn parse_download_source(source: &Bound<'_, PyAny>) -> PyResult<DownloadSource> {
21    if let Ok(url) = source.extract::<String>() {
22        return Ok(DownloadSource::Url(url));
23    }
24    if let Ok(urls) = source.extract::<Vec<String>>() {
25        return Ok(DownloadSource::MultipartUrls(urls));
26    }
27    Err(PyErr::new::<PyRuntimeError, _>(
28        "source must be a URL string or a list of URL strings",
29    ))
30}
31
32#[pyclass(module = "zccache._native")]
33#[derive(Clone)]
34pub struct NativeDaemonStatus {
35    #[pyo3(get)]
36    version: String,
37    #[pyo3(get)]
38    artifact_count: u64,
39    #[pyo3(get)]
40    cache_size_bytes: u64,
41    #[pyo3(get)]
42    metadata_entries: u64,
43    #[pyo3(get)]
44    uptime_secs: u64,
45    #[pyo3(get)]
46    cache_hits: u64,
47    #[pyo3(get)]
48    cache_misses: u64,
49    #[pyo3(get)]
50    total_compilations: u64,
51    #[pyo3(get)]
52    non_cacheable: u64,
53    #[pyo3(get)]
54    compile_errors: u64,
55    #[pyo3(get)]
56    time_saved_ms: u64,
57    #[pyo3(get)]
58    total_links: u64,
59    #[pyo3(get)]
60    link_hits: u64,
61    #[pyo3(get)]
62    link_misses: u64,
63    #[pyo3(get)]
64    link_non_cacheable: u64,
65    #[pyo3(get)]
66    dep_graph_contexts: u64,
67    #[pyo3(get)]
68    dep_graph_files: u64,
69    #[pyo3(get)]
70    sessions_total: u64,
71    #[pyo3(get)]
72    sessions_active: u64,
73    #[pyo3(get)]
74    cache_dir: String,
75    #[pyo3(get)]
76    dep_graph_version: u32,
77    #[pyo3(get)]
78    dep_graph_disk_size: u64,
79    #[pyo3(get)]
80    dep_graph_persisted: bool,
81}
82
83impl From<zccache::protocol::DaemonStatus> for NativeDaemonStatus {
84    fn from(value: zccache::protocol::DaemonStatus) -> Self {
85        Self {
86            version: value.version,
87            artifact_count: value.artifact_count,
88            cache_size_bytes: value.cache_size_bytes,
89            metadata_entries: value.metadata_entries,
90            uptime_secs: value.uptime_secs,
91            cache_hits: value.cache_hits,
92            cache_misses: value.cache_misses,
93            total_compilations: value.total_compilations,
94            non_cacheable: value.non_cacheable,
95            compile_errors: value.compile_errors,
96            time_saved_ms: value.time_saved_ms,
97            total_links: value.total_links,
98            link_hits: value.link_hits,
99            link_misses: value.link_misses,
100            link_non_cacheable: value.link_non_cacheable,
101            dep_graph_contexts: value.dep_graph_contexts,
102            dep_graph_files: value.dep_graph_files,
103            sessions_total: value.sessions_total,
104            sessions_active: value.sessions_active,
105            cache_dir: value.cache_dir.display().to_string(),
106            dep_graph_version: value.dep_graph_version,
107            dep_graph_disk_size: value.dep_graph_disk_size,
108            dep_graph_persisted: value.dep_graph_persisted,
109        }
110    }
111}
112
113#[pyclass(module = "zccache._native")]
114#[derive(Clone)]
115pub struct NativeSessionStart {
116    #[pyo3(get)]
117    session_id: String,
118    #[pyo3(get)]
119    journal_path: Option<String>,
120}
121
122#[pyclass(module = "zccache._native")]
123#[derive(Clone)]
124pub struct NativeSessionStats {
125    #[pyo3(get)]
126    duration_ms: u64,
127    #[pyo3(get)]
128    compilations: u64,
129    #[pyo3(get)]
130    hits: u64,
131    #[pyo3(get)]
132    misses: u64,
133    #[pyo3(get)]
134    non_cacheable: u64,
135    #[pyo3(get)]
136    errors: u64,
137    #[pyo3(get)]
138    time_saved_ms: u64,
139    #[pyo3(get)]
140    unique_sources: u64,
141    #[pyo3(get)]
142    bytes_read: u64,
143    #[pyo3(get)]
144    bytes_written: u64,
145}
146
147impl From<zccache::protocol::SessionStats> for NativeSessionStats {
148    fn from(value: zccache::protocol::SessionStats) -> Self {
149        Self {
150            duration_ms: value.duration_ms,
151            compilations: value.compilations,
152            hits: value.hits,
153            misses: value.misses,
154            non_cacheable: value.non_cacheable,
155            errors: value.errors,
156            time_saved_ms: value.time_saved_ms,
157            unique_sources: value.unique_sources,
158            bytes_read: value.bytes_read,
159            bytes_written: value.bytes_written,
160        }
161    }
162}
163
164#[pyclass(module = "zccache._native")]
165#[derive(Clone)]
166pub struct NativeFingerprintCheck {
167    #[pyo3(get)]
168    decision: String,
169    #[pyo3(get)]
170    reason: Option<String>,
171    #[pyo3(get)]
172    changed_files: Vec<String>,
173}
174
175#[pyclass(module = "zccache._native")]
176#[derive(Clone)]
177pub struct NativeInoConvertResult {
178    #[pyo3(get)]
179    cache_hit: bool,
180    #[pyo3(get)]
181    skipped_write: bool,
182}
183
184#[pyclass(module = "zccache._native")]
185#[derive(Clone)]
186pub struct NativeDownloadStatus {
187    #[pyo3(get)]
188    phase: String,
189    #[pyo3(get)]
190    total_bytes: Option<u64>,
191    #[pyo3(get)]
192    downloaded_bytes: u64,
193    #[pyo3(get)]
194    percentage: Option<f32>,
195    #[pyo3(get)]
196    active_clients: u32,
197    #[pyo3(get)]
198    destination: String,
199    #[pyo3(get)]
200    source_url: String,
201    #[pyo3(get)]
202    error: Option<String>,
203}
204
205impl From<zccache::download::DownloadStatus> for NativeDownloadStatus {
206    fn from(value: zccache::download::DownloadStatus) -> Self {
207        Self {
208            phase: format!("{:?}", value.phase).to_lowercase(),
209            total_bytes: value.total_bytes,
210            downloaded_bytes: value.downloaded_bytes,
211            percentage: value.percentage,
212            active_clients: value.active_clients,
213            destination: value.destination.display().to_string(),
214            source_url: value.source_url,
215            error: value.error,
216        }
217    }
218}
219
220#[pyclass(module = "zccache._native")]
221#[derive(Clone)]
222pub struct NativeDownloadDaemonStatus {
223    #[pyo3(get)]
224    version: String,
225    #[pyo3(get)]
226    active_downloads: u64,
227    #[pyo3(get)]
228    connected_clients: u64,
229    #[pyo3(get)]
230    uptime_secs: u64,
231    #[pyo3(get)]
232    endpoint: String,
233}
234
235impl From<zccache::download::DownloadDaemonStatus> for NativeDownloadDaemonStatus {
236    fn from(value: zccache::download::DownloadDaemonStatus) -> Self {
237        Self {
238            version: value.version,
239            active_downloads: value.active_downloads,
240            connected_clients: value.connected_clients,
241            uptime_secs: value.uptime_secs,
242            endpoint: value.endpoint,
243        }
244    }
245}
246
247fn parse_archive_format(value: &str) -> zccache::download_client::ArchiveFormat {
248    match value.to_ascii_lowercase().as_str() {
249        "none" => zccache::download_client::ArchiveFormat::None,
250        "zst" => zccache::download_client::ArchiveFormat::Zst,
251        "zip" => zccache::download_client::ArchiveFormat::Zip,
252        "xz" => zccache::download_client::ArchiveFormat::Xz,
253        "tar.gz" | "targz" => zccache::download_client::ArchiveFormat::TarGz,
254        "tar.xz" | "tarxz" => zccache::download_client::ArchiveFormat::TarXz,
255        "tar.zst" | "tarzst" => zccache::download_client::ArchiveFormat::TarZst,
256        "7z" | "sevenz" => zccache::download_client::ArchiveFormat::SevenZip,
257        _ => zccache::download_client::ArchiveFormat::Auto,
258    }
259}
260
261#[pyclass(module = "zccache._native")]
262#[derive(Clone)]
263pub struct NativeFetchResult {
264    #[pyo3(get)]
265    status: String,
266    #[pyo3(get)]
267    cache_path: String,
268    #[pyo3(get)]
269    expanded_path: Option<String>,
270    #[pyo3(get)]
271    bytes: Option<u64>,
272    #[pyo3(get)]
273    sha256: String,
274}
275
276impl From<zccache::download_client::FetchResult> for NativeFetchResult {
277    fn from(value: zccache::download_client::FetchResult) -> Self {
278        Self {
279            status: format!("{:?}", value.status).to_lowercase(),
280            cache_path: value.cache_path.display().to_string(),
281            expanded_path: value.expanded_path.map(|path| path.display().to_string()),
282            bytes: value.bytes,
283            sha256: value.sha256,
284        }
285    }
286}
287
288#[pyclass(module = "zccache._native")]
289#[derive(Clone)]
290pub struct NativeFetchState {
291    #[pyo3(get)]
292    kind: String,
293    #[pyo3(get)]
294    cache_path: String,
295    #[pyo3(get)]
296    expanded_path: Option<String>,
297    #[pyo3(get)]
298    bytes: Option<u64>,
299    #[pyo3(get)]
300    sha256: Option<String>,
301    #[pyo3(get)]
302    reason: Option<String>,
303}
304
305impl From<zccache::download_client::FetchState> for NativeFetchState {
306    fn from(value: zccache::download_client::FetchState) -> Self {
307        Self {
308            kind: format!("{:?}", value.kind).to_lowercase(),
309            cache_path: value.cache_path.display().to_string(),
310            expanded_path: value.expanded_path.map(|path| path.display().to_string()),
311            bytes: value.bytes,
312            sha256: value.sha256,
313            reason: value.reason,
314        }
315    }
316}
317
318#[pyclass(module = "zccache._native")]
319pub struct NativeDownloadHandle {
320    handle: Option<zccache::download_client::DownloadHandle>,
321    initiator: bool,
322    download_id: String,
323}
324
325#[pymethods]
326impl NativeDownloadHandle {
327    #[getter]
328    fn initiator(&self) -> bool {
329        self.initiator
330    }
331
332    #[getter]
333    fn download_id(&self) -> String {
334        self.download_id.clone()
335    }
336
337    fn status(&mut self) -> PyResult<NativeDownloadStatus> {
338        let handle = self
339            .handle
340            .as_mut()
341            .ok_or_else(|| runtime_to_py_err("download handle is closed".to_string()))?;
342        handle
343            .status()
344            .map(NativeDownloadStatus::from)
345            .map_err(runtime_to_py_err)
346    }
347
348    #[pyo3(signature = (timeout_ms=None))]
349    fn wait(&mut self, timeout_ms: Option<u64>) -> PyResult<NativeDownloadStatus> {
350        let handle = self
351            .handle
352            .as_mut()
353            .ok_or_else(|| runtime_to_py_err("download handle is closed".to_string()))?;
354        handle
355            .wait(timeout_ms)
356            .map(NativeDownloadStatus::from)
357            .map_err(runtime_to_py_err)
358    }
359
360    fn cancel(&mut self) -> PyResult<NativeDownloadStatus> {
361        let handle = self
362            .handle
363            .as_mut()
364            .ok_or_else(|| runtime_to_py_err("download handle is closed".to_string()))?;
365        handle
366            .cancel()
367            .map(NativeDownloadStatus::from)
368            .map_err(runtime_to_py_err)
369    }
370
371    fn close(&mut self) -> PyResult<()> {
372        if let Some(handle) = self.handle.take() {
373            handle.close().map_err(runtime_to_py_err)?;
374        }
375        Ok(())
376    }
377}
378
379#[pyclass(module = "zccache._native")]
380pub struct NativeDownloadApi {
381    client: zccache::download_client::DownloadClient,
382}
383
384#[pymethods]
385impl NativeDownloadApi {
386    #[new]
387    #[pyo3(signature = (endpoint=None))]
388    fn new(endpoint: Option<String>) -> Self {
389        let client = zccache::download_client::DownloadClient::new(endpoint.clone());
390        Self { client }
391    }
392
393    fn start(&self) -> PyResult<()> {
394        self.client.start_daemon().map_err(runtime_to_py_err)
395    }
396
397    fn stop(&self) -> PyResult<bool> {
398        self.client.stop_daemon().map_err(runtime_to_py_err)
399    }
400
401    fn daemon_status(&self) -> PyResult<NativeDownloadDaemonStatus> {
402        self.client
403            .daemon_status()
404            .map(NativeDownloadDaemonStatus::from)
405            .map_err(runtime_to_py_err)
406    }
407
408    #[pyo3(signature = (
409        source_url,
410        destination,
411        force=false,
412        max_connections=None,
413        min_segment_size=None
414    ))]
415    fn download(
416        &self,
417        source_url: String,
418        destination: String,
419        force: bool,
420        max_connections: Option<usize>,
421        min_segment_size: Option<u64>,
422    ) -> PyResult<NativeDownloadHandle> {
423        let options = zccache::download::DownloadOptions {
424            force,
425            max_connections,
426            min_segment_size,
427        };
428        let handle = self
429            .client
430            .download(&source_url, PathBuf::from(destination).as_path(), options)
431            .map_err(runtime_to_py_err)?;
432        let initiator = handle.initiator();
433        let download_id = handle.download_id().to_string();
434        Ok(NativeDownloadHandle {
435            handle: Some(handle),
436            initiator,
437            download_id,
438        })
439    }
440
441    #[pyo3(signature = (
442        source,
443        destination=None,
444        expanded=None,
445        expected_sha256=None,
446        archive_format="auto".to_string(),
447        max_connections=None,
448        min_segment_size=None,
449        blocking=true,
450        dry_run=false,
451        force=false
452    ))]
453    fn fetch(
454        &self,
455        source: &Bound<'_, PyAny>,
456        destination: Option<String>,
457        expanded: Option<String>,
458        expected_sha256: Option<String>,
459        archive_format: String,
460        max_connections: Option<usize>,
461        min_segment_size: Option<u64>,
462        blocking: bool,
463        dry_run: bool,
464        force: bool,
465    ) -> PyResult<NativeFetchResult> {
466        let source = parse_download_source(source)?;
467        let request = build_download_request(DownloadParams {
468            source,
469            archive_path: destination.map(PathBuf::from),
470            unarchive_path: expanded.map(PathBuf::from),
471            expected_sha256,
472            archive_format: parse_archive_format(&archive_format),
473            max_connections,
474            min_segment_size,
475            wait_mode: if blocking {
476                WaitMode::Block
477            } else {
478                WaitMode::NoWait
479            },
480            dry_run,
481            force,
482        });
483        self.client
484            .fetch(request)
485            .map(NativeFetchResult::from)
486            .map_err(runtime_to_py_err)
487    }
488
489    #[pyo3(signature = (
490        source,
491        destination=None,
492        expanded=None,
493        expected_sha256=None,
494        archive_format="auto".to_string()
495    ))]
496    fn exists(
497        &self,
498        source: &Bound<'_, PyAny>,
499        destination: Option<String>,
500        expanded: Option<String>,
501        expected_sha256: Option<String>,
502        archive_format: String,
503    ) -> PyResult<NativeFetchState> {
504        let source = parse_download_source(source)?;
505        let request = build_download_request(DownloadParams {
506            source,
507            archive_path: destination.map(PathBuf::from),
508            unarchive_path: expanded.map(PathBuf::from),
509            expected_sha256,
510            archive_format: parse_archive_format(&archive_format),
511            max_connections: None,
512            min_segment_size: None,
513            wait_mode: WaitMode::Block,
514            dry_run: false,
515            force: false,
516        });
517        self.client
518            .exists(&request)
519            .map(NativeFetchState::from)
520            .map_err(runtime_to_py_err)
521    }
522}
523
524#[pyclass(module = "zccache._native")]
525pub struct NativeClient {
526    endpoint: Option<String>,
527}
528
529#[pymethods]
530impl NativeClient {
531    #[new]
532    #[pyo3(signature = (endpoint=None))]
533    fn new(endpoint: Option<String>) -> Self {
534        Self { endpoint }
535    }
536
537    fn start(&self) -> PyResult<()> {
538        client_start(self.endpoint.as_deref()).map_err(runtime_to_py_err)
539    }
540
541    fn stop(&self) -> PyResult<bool> {
542        client_stop(self.endpoint.as_deref()).map_err(runtime_to_py_err)
543    }
544
545    fn status(&self) -> PyResult<NativeDaemonStatus> {
546        client_status(self.endpoint.as_deref())
547            .map(NativeDaemonStatus::from)
548            .map_err(runtime_to_py_err)
549    }
550
551    #[pyo3(signature = (
552        source,
553        destination=None,
554        expanded=None,
555        expected_sha256=None,
556        max_connections=None,
557        min_segment_size=None,
558        blocking=true,
559        dry_run=false,
560        force=false
561    ))]
562    fn download(
563        &self,
564        source: &Bound<'_, PyAny>,
565        destination: Option<String>,
566        expanded: Option<String>,
567        expected_sha256: Option<String>,
568        max_connections: Option<usize>,
569        min_segment_size: Option<u64>,
570        blocking: bool,
571        dry_run: bool,
572        force: bool,
573    ) -> PyResult<NativeFetchResult> {
574        let source = parse_download_source(source)?;
575        client_download(
576            self.endpoint.as_deref(),
577            DownloadParams {
578                source,
579                archive_path: destination.map(PathBuf::from),
580                unarchive_path: expanded.map(PathBuf::from),
581                expected_sha256,
582                archive_format: zccache::download_client::ArchiveFormat::Auto,
583                max_connections,
584                min_segment_size,
585                wait_mode: if blocking {
586                    WaitMode::Block
587                } else {
588                    WaitMode::NoWait
589                },
590                dry_run,
591                force,
592            },
593        )
594        .map(NativeFetchResult::from)
595        .map_err(runtime_to_py_err)
596    }
597
598    #[pyo3(signature = (
599        source,
600        destination=None,
601        expanded=None,
602        expected_sha256=None
603    ))]
604    fn download_exists(
605        &self,
606        source: &Bound<'_, PyAny>,
607        destination: Option<String>,
608        expanded: Option<String>,
609        expected_sha256: Option<String>,
610    ) -> PyResult<NativeFetchState> {
611        let source = parse_download_source(source)?;
612        client_download_exists(
613            self.endpoint.as_deref(),
614            DownloadParams {
615                source,
616                archive_path: destination.map(PathBuf::from),
617                unarchive_path: expanded.map(PathBuf::from),
618                expected_sha256,
619                archive_format: zccache::download_client::ArchiveFormat::Auto,
620                max_connections: None,
621                min_segment_size: None,
622                wait_mode: WaitMode::Block,
623                dry_run: false,
624                force: false,
625            },
626        )
627        .map(NativeFetchState::from)
628        .map_err(runtime_to_py_err)
629    }
630
631    #[pyo3(signature = (cwd, log_file=None, track_stats=false, journal_path=None))]
632    fn session_start(
633        &self,
634        cwd: String,
635        log_file: Option<String>,
636        track_stats: bool,
637        journal_path: Option<String>,
638    ) -> PyResult<NativeSessionStart> {
639        let cwd = PathBuf::from(cwd);
640        let log_file = log_file.map(PathBuf::from);
641        let journal_path = journal_path.map(PathBuf::from);
642        client_session_start(
643            self.endpoint.as_deref(),
644            cwd.as_path(),
645            log_file.as_deref(),
646            track_stats,
647            journal_path.as_deref(),
648        )
649        .map(|result| NativeSessionStart {
650            session_id: result.session_id,
651            journal_path: result.journal_path,
652        })
653        .map_err(runtime_to_py_err)
654    }
655
656    fn session_end(&self, session_id: String) -> PyResult<Option<NativeSessionStats>> {
657        client_session_end(self.endpoint.as_deref(), &session_id)
658            .map(|stats| stats.map(NativeSessionStats::from))
659            .map_err(runtime_to_py_err)
660    }
661
662    fn session_stats(&self, session_id: String) -> PyResult<Option<NativeSessionStats>> {
663        client_session_stats(self.endpoint.as_deref(), &session_id)
664            .map(|stats| stats.map(NativeSessionStats::from))
665            .map_err(runtime_to_py_err)
666    }
667
668    #[pyo3(signature = (
669        cache_file,
670        cache_type="two-layer".to_string(),
671        root=".".to_string(),
672        extensions=vec![],
673        include_globs=vec![],
674        exclude=vec![]
675    ))]
676    fn fingerprint_check(
677        &self,
678        cache_file: String,
679        cache_type: String,
680        root: String,
681        extensions: Vec<String>,
682        include_globs: Vec<String>,
683        exclude: Vec<String>,
684    ) -> PyResult<NativeFingerprintCheck> {
685        fingerprint_check(
686            self.endpoint.as_deref(),
687            PathBuf::from(cache_file).as_path(),
688            &cache_type,
689            PathBuf::from(root).as_path(),
690            &extensions,
691            &include_globs,
692            &exclude,
693        )
694        .map(|result| NativeFingerprintCheck {
695            decision: result.decision,
696            reason: result.reason,
697            changed_files: result.changed_files,
698        })
699        .map_err(runtime_to_py_err)
700    }
701
702    fn fingerprint_mark_success(&self, cache_file: String) -> PyResult<()> {
703        fingerprint_mark_success(
704            self.endpoint.as_deref(),
705            PathBuf::from(cache_file).as_path(),
706        )
707        .map_err(runtime_to_py_err)
708    }
709
710    fn fingerprint_mark_failure(&self, cache_file: String) -> PyResult<()> {
711        fingerprint_mark_failure(
712            self.endpoint.as_deref(),
713            PathBuf::from(cache_file).as_path(),
714        )
715        .map_err(runtime_to_py_err)
716    }
717
718    fn fingerprint_invalidate(&self, cache_file: String) -> PyResult<()> {
719        fingerprint_invalidate(
720            self.endpoint.as_deref(),
721            PathBuf::from(cache_file).as_path(),
722        )
723        .map_err(runtime_to_py_err)
724    }
725}
726
727#[pyfunction]
728#[pyo3(signature = (
729    input,
730    output,
731    clang_args=vec![],
732    inject_arduino_include=true
733))]
734fn convert_ino(
735    input: String,
736    output: String,
737    clang_args: Vec<String>,
738    inject_arduino_include: bool,
739) -> PyResult<NativeInoConvertResult> {
740    run_ino_convert_cached(
741        PathBuf::from(input).as_path(),
742        PathBuf::from(output).as_path(),
743        &InoConvertOptions {
744            clang_args,
745            inject_arduino_include,
746        },
747    )
748    .map(|result| NativeInoConvertResult {
749        cache_hit: result.cache_hit,
750        skipped_write: result.skipped_write,
751    })
752    .map_err(|e| PyErr::new::<PyOSError, _>(e.to_string()))
753}
754
755#[pyfunction]
756fn default_endpoint() -> String {
757    zccache::cli::resolve_endpoint(None)
758}
759
760#[pyfunction]
761fn check_running_daemon() -> Option<u32> {
762    zccache::ipc::check_running_daemon()
763}
764
765#[pymodule]
766fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
767    m.add_class::<NativeClient>()?;
768    m.add_class::<NativeDaemonStatus>()?;
769    m.add_class::<NativeSessionStart>()?;
770    m.add_class::<NativeSessionStats>()?;
771    m.add_class::<NativeFingerprintCheck>()?;
772    m.add_class::<NativeInoConvertResult>()?;
773    m.add_class::<NativeDownloadStatus>()?;
774    m.add_class::<NativeDownloadDaemonStatus>()?;
775    m.add_class::<NativeFetchResult>()?;
776    m.add_class::<NativeFetchState>()?;
777    m.add_class::<NativeDownloadHandle>()?;
778    m.add_class::<NativeDownloadApi>()?;
779    m.add_function(wrap_pyfunction!(convert_ino, m)?)?;
780    m.add_function(wrap_pyfunction!(default_endpoint, m)?)?;
781    m.add_function(wrap_pyfunction!(check_running_daemon, m)?)?;
782    Ok(())
783}