diff --git a/src/ndi/cloud/admin/ndi_matlab_python_bridge.yaml b/src/ndi/cloud/admin/ndi_matlab_python_bridge.yaml new file mode 100644 index 0000000..adaa7d3 --- /dev/null +++ b/src/ndi/cloud/admin/ndi_matlab_python_bridge.yaml @@ -0,0 +1,219 @@ +# ndi_matlab_python_bridge.yaml — src/ndi/cloud/admin/ +# The Primary Contract for the ndi.cloud.admin namespace. +# +# Covers: doi.py, crossref.py + +project_metadata: + bridge_version: "1.1" + naming_policy: "Strict MATLAB Mirror" + indexing_policy: "N/A (no user-facing counting)" + +# ============================================================================= +# Classes — crossref.py +# Maps to: +cloud/+admin/+crossref/Constants.m +# ============================================================================= +classes: + + - name: CrossrefConstants + type: dataclass + matlab_path: "+ndi/+cloud/+admin/+crossref/Constants.m" + python_path: "ndi/cloud/admin/crossref.py" + python_class: "CrossrefConstants" + properties: + - name: DOI_PREFIX + type_python: "str" + default: "'10.63884'" + - name: DATABASE_DOI + type_python: "str" + default: "'10.63884/ndic.00000'" + - name: DATABASE_URL + type_python: "str" + default: "'https://ndi-cloud.com'" + - name: DATABASE_TITLE + type_python: "str" + default: "'NDI Cloud Open Datasets'" + - name: DATASET_BASE_URL + type_python: "str" + default: "'https://www.ndi-cloud.com/datasets/'" + - name: DEPOSIT_URL + type_python: "str" + default: "'https://doi.crossref.org/servlet/deposit'" + - name: TEST_DEPOSIT_URL + type_python: "str" + default: "'https://test.crossref.org/servlet/deposit'" + decision_log: > + MATLAB uses a Constants class with constant properties. + Python uses a frozen dataclass. Field names exact match. + +# ============================================================================= +# Functions — doi.py +# Maps to: +cloud/+admin/ +# ============================================================================= +functions: + + - name: createNewDOI + matlab_path: "+ndi/+cloud/+admin/createNewDOI.m" + python_path: "ndi/cloud/admin/doi.py" + input_arguments: + - name: prefix + type_matlab: "char" + type_python: "str" + default: "''" + output_arguments: + - name: doi + type_python: "str" + decision_log: "Exact match." + + - name: registerDatasetDOI + matlab_path: "+ndi/+cloud/+admin/registerDatasetDOI.m" + python_path: "ndi/cloud/admin/doi.py" + input_arguments: + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: use_test + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: checkSubmission + matlab_path: "+ndi/+cloud/+admin/checkSubmission.m" + python_path: "ndi/cloud/admin/doi.py" + input_arguments: + - name: filename + type_matlab: "char" + type_python: "str" + - name: data_type + type_matlab: "char" + type_python: "str" + default: "'result'" + - name: use_test + type_matlab: "logical" + type_python: "bool" + default: "False" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + # --- crossref.py functions --- + - name: createDoiBatchSubmission + matlab_path: "+ndi/+cloud/+admin/+crossref/createDoiBatchSubmission.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: doi + type_matlab: "char" + type_python: "str" + output_arguments: + - name: xml + type_python: "str" + decision_log: "Exact match." + + - name: convertCloudDatasetToCrossrefDataset + matlab_path: "+ndi/+cloud/+admin/+crossref/convertCloudDatasetToCrossrefDataset.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + output_arguments: + - name: crossref_metadata + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: convertContributors + matlab_path: "+ndi/+cloud/+admin/+crossref/+conversion/convertContributors.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + output_arguments: + - name: contributors + type_python: "list[dict[str, Any]]" + decision_log: > + MATLAB places this in +crossref/+conversion. Python places all + conversion functions in crossref.py. Exact name match. + + - name: convertDatasetDate + matlab_path: "+ndi/+cloud/+admin/+crossref/+conversion/convertDatasetDate.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + output_arguments: + - name: date_info + type_python: "dict[str, str]" + decision_log: > + MATLAB places this in +crossref/+conversion. Python places it + in crossref.py. Exact name match. + + - name: convertFunding + matlab_path: "+ndi/+cloud/+admin/+crossref/+conversion/convertFunding.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + output_arguments: + - name: funding + type_python: "list[dict[str, str]]" + decision_log: > + MATLAB places this in +crossref/+conversion. Python places it + in crossref.py. Exact name match. + + - name: convertLicense + matlab_path: "+ndi/+cloud/+admin/+crossref/+conversion/convertLicense.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + output_arguments: + - name: license_info + type_python: "dict[str, str]" + decision_log: > + MATLAB places this in +crossref/+conversion. Python places it + in crossref.py. Exact name match. + + - name: convertRelatedPublications + matlab_path: "+ndi/+cloud/+admin/+crossref/+conversion/convertRelatedPublications.m" + python_path: "ndi/cloud/admin/crossref.py" + input_arguments: + - name: dataset_metadata + type_matlab: "struct" + type_python: "dict[str, Any]" + output_arguments: + - name: publications + type_python: "list[dict[str, Any]]" + decision_log: > + MATLAB places this in +crossref/+conversion. Python places it + in crossref.py. Exact name match. + +# ============================================================================= +# Not-yet-ported MATLAB functions +# ============================================================================= +not_yet_ported: + + - name: createDatabaseMetadata + matlab_path: "+ndi/+cloud/+admin/+crossref/createDatabaseMetadata.m" + status: not_yet_ported + decision_log: "MATLAB function to create NDI Cloud database-level metadata for Crossref." + + - name: createDoiBatchHeadElement + matlab_path: "+ndi/+cloud/+admin/+crossref/createDoiBatchHeadElement.m" + status: not_yet_ported + decision_log: > + MATLAB helper for creating the XML head element in DOI batch submissions. + May be inlined within createDoiBatchSubmission in Python. diff --git a/src/ndi/cloud/api/ndi_matlab_python_bridge.yaml b/src/ndi/cloud/api/ndi_matlab_python_bridge.yaml new file mode 100644 index 0000000..d7393e0 --- /dev/null +++ b/src/ndi/cloud/api/ndi_matlab_python_bridge.yaml @@ -0,0 +1,904 @@ +# ndi_matlab_python_bridge.yaml — src/ndi/cloud/api/ +# The Primary Contract for the ndi.cloud.api namespace. +# +# Covers: datasets.py, documents.py, files.py, users.py, compute.py, _validators.py + +project_metadata: + bridge_version: "1.1" + naming_policy: "Strict MATLAB Mirror" + indexing_policy: "N/A (no user-facing counting)" + +# ============================================================================= +# Infrastructure (Python-only) +# ============================================================================= +infrastructure: + - name: _validators.py + type: module + python_path: "ndi/cloud/api/_validators.py" + decision_log: > + Python-only Pydantic validators (CloudId, NonEmptyStr, PageNumber, + PageSize, Scope, FilePath). MATLAB uses arguments blocks for validation. + + - name: _resolve_org_id + type: internal_helper + python_path: "ndi/cloud/api/datasets.py" + decision_log: "Python-only helper to resolve org_id from client config." + + - name: _coerce_search_structure + type: internal_helper + python_path: "ndi/cloud/api/documents.py" + decision_log: "Python-only helper to normalize search structures." + +# ============================================================================= +# Functions — datasets.py +# Maps to: +cloud/+api/+datasets/ +# ============================================================================= +functions: + + # --- datasets.py --- + - name: getDataset + matlab_path: "+ndi/+cloud/+api/+datasets/getDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: dataset + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: createDataset + matlab_path: "+ndi/+cloud/+api/+datasets/createDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: org_id + type_matlab: "char" + type_python: "str | None" + default: "None" + - name: name + type_matlab: "char" + type_python: "str" + default: "''" + - name: description + type_matlab: "char" + type_python: "str" + default: "''" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: dataset + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: updateDataset + matlab_path: "+ndi/+cloud/+api/+datasets/updateDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + - name: "**fields" + type_python: "Any" + output_arguments: + - name: dataset + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: deleteDataset + matlab_path: "+ndi/+cloud/+api/+datasets/deleteDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: when + type_matlab: "char" + type_python: "str" + default: "'7d'" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: listDatasets + matlab_path: "+ndi/+cloud/+api/+datasets/listDatasets.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: org_id + type_matlab: "char" + type_python: "str | None" + default: "None" + - name: page + type_matlab: "double" + type_python: "PageNumber" + default: "1" + - name: page_size + type_matlab: "double" + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: listAllDatasets + matlab_path: "N/A" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: org_id + type_python: "str | None" + default: "None" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "APIResponse" + decision_log: > + Python-only auto-paginator that wraps listDatasets. + No direct MATLAB equivalent. + + - name: getPublished + matlab_path: "+ndi/+cloud/+api/+datasets/getPublished.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: page + type_python: "PageNumber" + default: "1" + - name: page_size + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: publishDataset + matlab_path: "+ndi/+cloud/+api/+datasets/publishDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: unpublishDataset + matlab_path: "+ndi/+cloud/+api/+datasets/unpublishDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: submitDataset + matlab_path: "+ndi/+cloud/+api/+datasets/submitDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: createDatasetBranch + matlab_path: "+ndi/+cloud/+api/+datasets/createDatasetBranch.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: getBranches + matlab_path: "+ndi/+cloud/+api/+datasets/getBranches.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: branches + type_python: "list[dict[str, Any]]" + decision_log: "Exact match." + + - name: getUnpublished + matlab_path: "+ndi/+cloud/+api/+datasets/getUnpublished.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: page + type_python: "PageNumber" + default: "1" + - name: page_size + type_python: "PageSize" + default: "20" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: undeleteDataset + matlab_path: "+ndi/+cloud/+api/+datasets/undeleteDataset.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: listDeletedDatasets + matlab_path: "+ndi/+cloud/+api/+datasets/listDeletedDatasets.m" + python_path: "ndi/cloud/api/datasets.py" + input_arguments: + - name: page + type_python: "PageNumber" + default: "1" + - name: page_size + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + # --- documents.py --- + - name: getDocument + matlab_path: "+ndi/+cloud/+api/+documents/getDocument.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: document_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: document + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: addDocument + matlab_path: "+ndi/+cloud/+api/+documents/addDocument.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: doc_json + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: updateDocument + matlab_path: "+ndi/+cloud/+api/+documents/updateDocument.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: document_id + type_matlab: "string" + type_python: "CloudId" + - name: doc_json + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: deleteDocument + matlab_path: "+ndi/+cloud/+api/+documents/deleteDocument.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: document_id + type_matlab: "string" + type_python: "CloudId" + - name: when + type_matlab: "char" + type_python: "str" + default: "'7d'" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: listDatasetDocuments + matlab_path: "+ndi/+cloud/+api/+documents/listDatasetDocuments.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: page + type_matlab: "double" + type_python: "PageNumber" + default: "1" + - name: page_size + type_matlab: "double" + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: listDatasetDocumentsAll + matlab_path: "+ndi/+cloud/+api/+documents/listDatasetDocumentsAll.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: page_size + type_matlab: "double" + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "APIResponse" + decision_log: "Exact match." + + - name: countDocuments + matlab_path: "+ndi/+cloud/+api/+documents/countDocuments.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: count + type_python: "int" + decision_log: > + Exact match. MATLAB also has documentCount.m which is a duplicate/alias. + + - name: bulkUpload + matlab_path: "N/A" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_python: "CloudId" + - name: zip_path + type_python: "str" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: > + Python convenience that combines getBulkUploadURL + putFiles. + MATLAB does these as separate steps. + + - name: getBulkUploadURL + matlab_path: "+ndi/+cloud/+api/+documents/getBulkUploadURL.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: url + type_python: "str" + decision_log: "Exact match." + + - name: getBulkDownloadURL + matlab_path: "+ndi/+cloud/+api/+documents/getBulkDownloadURL.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: doc_ids + type_matlab: "string array" + type_python: "list[str] | None" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: url + type_python: "str" + decision_log: "Exact match." + + - name: bulkDeleteDocuments + matlab_path: "+ndi/+cloud/+api/+documents/bulkDeleteDocuments.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: doc_ids + type_matlab: "string array" + type_python: "list[str]" + - name: when + type_matlab: "char" + type_python: "str" + default: "'7d'" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: ndiquery + matlab_path: "+ndi/+cloud/+api/+documents/ndiquery.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: scope + type_matlab: "char" + type_python: "Scope" + - name: search_structure + type_matlab: "struct" + type_python: "Any" + - name: page + type_python: "PageNumber" + default: "1" + - name: page_size + type_python: "PageSize" + default: "20" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: ndiqueryAll + matlab_path: "+ndi/+cloud/+api/+documents/ndiqueryAll.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: scope + type_python: "Scope" + - name: search_structure + type_python: "Any" + - name: page_size + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "APIResponse" + decision_log: "Exact match." + + - name: listDeletedDocuments + matlab_path: "+ndi/+cloud/+api/+documents/listDeletedDocuments.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: page + type_python: "PageNumber" + default: "1" + - name: page_size + type_python: "PageSize" + default: "1000" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: addDocumentAsFile + matlab_path: "+ndi/+cloud/+api/+documents/addDocumentAsFile.m" + python_path: "ndi/cloud/api/documents.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: file_path + type_matlab: "char" + type_python: "FilePath" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + # --- files.py --- + - name: getFileUploadURL + matlab_path: "+ndi/+cloud/+api/+files/getFileUploadURL.m" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: org_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: file_uid + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: url + type_python: "str" + decision_log: "Exact match." + + - name: getBulkUploadURL + matlab_path: "+ndi/+cloud/+api/+files/getBulkUploadURL.m" # Note: also in +documents + python_path: "ndi/cloud/api/files.py" + python_qualified: "ndi.cloud.api.files.getBulkUploadURL" + input_arguments: + - name: org_id + type_python: "NonEmptyStr" + - name: dataset_id + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: url + type_python: "str" + decision_log: > + Disambiguated from documents.getBulkUploadURL by full module path. + Exact name match. + + - name: putFiles + matlab_path: "+ndi/+cloud/+api/+files/putFiles.m" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: url + type_matlab: "char" + type_python: "NonEmptyStr" + - name: file_path + type_matlab: "char" + type_python: "FilePath" + - name: timeout + type_python: "int" + default: "120" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: putFileBytes + matlab_path: "N/A" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: url + type_python: "NonEmptyStr" + - name: data + type_python: "bytes" + - name: timeout + type_python: "int" + default: "120" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Python-only for uploading in-memory bytes." + + - name: getFile + matlab_path: "+ndi/+cloud/+api/+files/getFile.m" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: url + type_matlab: "char" + type_python: "NonEmptyStr" + - name: target_path + type_matlab: "char" + type_python: "str | Path" + - name: timeout + type_python: "int" + default: "120" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: listFiles + matlab_path: "+ndi/+cloud/+api/+files/listFiles.m" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "APIResponse" + decision_log: "Exact match." + + - name: getFileDetails + matlab_path: "+ndi/+cloud/+api/+files/getFileDetails.m" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "CloudId" + - name: file_uid + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: getFileCollectionUploadURL + matlab_path: "+ndi/+cloud/+api/+files/getFileCollectionUploadURL.m" + python_path: "ndi/cloud/api/files.py" + input_arguments: + - name: org_id + type_python: "NonEmptyStr" + - name: dataset_id + type_python: "CloudId" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: url + type_python: "str" + decision_log: "Exact match." + + # --- users.py --- + - name: createUser + matlab_path: "+ndi/+cloud/+api/+users/createUser.m" + python_path: "ndi/cloud/api/users.py" + input_arguments: + - name: email + type_matlab: "char" + type_python: "NonEmptyStr" + - name: name + type_matlab: "char" + type_python: "NonEmptyStr" + - name: password + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: me + matlab_path: "+ndi/+cloud/+api/+users/me.m" + python_path: "ndi/cloud/api/users.py" + input_arguments: + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: user + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: GetUser + matlab_path: "+ndi/+cloud/+api/+users/GetUser.m" + python_path: "ndi/cloud/api/users.py" + input_arguments: + - name: user_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: user + type_python: "dict[str, Any]" + decision_log: > + MATLAB uses PascalCase 'GetUser'. Python preserves this exactly + per strict naming policy. + + # --- compute.py --- + - name: startSession + matlab_path: "+ndi/+cloud/+api/+compute/startSession.m" + python_path: "ndi/cloud/api/compute.py" + input_arguments: + - name: pipeline_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: input_params + type_matlab: "struct" + type_python: "dict[str, Any] | None" + default: "None" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: getSessionStatus + matlab_path: "+ndi/+cloud/+api/+compute/getSessionStatus.m" + python_path: "ndi/cloud/api/compute.py" + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: status + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: triggerStage + matlab_path: "+ndi/+cloud/+api/+compute/triggerStage.m" + python_path: "ndi/cloud/api/compute.py" + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: stage_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: finalizeSession + matlab_path: "+ndi/+cloud/+api/+compute/finalizeSession.m" + python_path: "ndi/cloud/api/compute.py" + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: abortSession + matlab_path: "+ndi/+cloud/+api/+compute/abortSession.m" + python_path: "ndi/cloud/api/compute.py" + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "NonEmptyStr" + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: listSessions + matlab_path: "+ndi/+cloud/+api/+compute/listSessions.m" + python_path: "ndi/cloud/api/compute.py" + input_arguments: + - name: client + type_python: "_Client" + default: "None" + output_arguments: + - name: sessions + type_python: "APIResponse" + decision_log: "Exact match." + +# ============================================================================= +# Not-yet-ported MATLAB functions +# ============================================================================= +not_yet_ported: + + - name: call + matlab_path: "+ndi/+cloud/+api/call.m" + status: not_applicable + decision_log: > + MATLAB generic HTTP dispatcher. Python uses CloudClient._request() + which serves the same purpose. + + - name: url + matlab_path: "+ndi/+cloud/+api/url.m" + status: not_applicable + decision_log: > + MATLAB URL builder. Python uses CloudClient._build_url() + which serves the same purpose. + + - name: documentCount + matlab_path: "+ndi/+cloud/+api/+documents/documentCount.m" + status: not_applicable + decision_log: > + MATLAB duplicate/alias for countDocuments. Python uses countDocuments + which is the canonical name. diff --git a/src/ndi/cloud/ndi_matlab_python_bridge.yaml b/src/ndi/cloud/ndi_matlab_python_bridge.yaml new file mode 100644 index 0000000..b209331 --- /dev/null +++ b/src/ndi/cloud/ndi_matlab_python_bridge.yaml @@ -0,0 +1,1029 @@ +# ndi_matlab_python_bridge.yaml — src/ndi/cloud/ +# The Primary Contract for the ndi.cloud root namespace. +# +# Covers: auth.py, config.py, client.py, exceptions.py, orchestration.py, +# download.py, upload.py, filehandler.py, internal.py + +project_metadata: + bridge_version: "1.1" + naming_policy: "Strict MATLAB Mirror" + indexing_policy: "N/A (no user-facing counting)" + +# ============================================================================= +# Infrastructure (Python-only — no MATLAB equivalent) +# ============================================================================= +infrastructure: + - name: CloudConfig + type: class + python_path: "ndi/cloud/config.py" + decision_log: > + Python-only configuration container. MATLAB uses environment variables + and ndi_globals directly; Python wraps them in a dataclass. + + - name: CloudClient + type: class + python_path: "ndi/cloud/client.py" + decision_log: > + Python-only HTTP client. MATLAB uses webread/webwrite/call.m; + Python wraps requests.Session for all REST calls. + + - name: APIResponse + type: class + python_path: "ndi/cloud/client.py" + decision_log: > + Python-only response wrapper. MATLAB returns raw structs/cells + from webread; Python normalizes into APIResponse. + + - name: _auto_client + type: decorator + python_path: "ndi/cloud/client.py" + decision_log: > + Python-only decorator making the 'client' parameter optional. + MATLAB functions implicitly use the environment for auth. + + - name: CloudError + type: exception + python_path: "ndi/cloud/exceptions.py" + decision_log: "Python-only base exception class for cloud operations." + + - name: CloudAuthError + type: exception + python_path: "ndi/cloud/exceptions.py" + decision_log: "Python-only. MATLAB raises generic errors." + + - name: CloudAPIError + type: exception + python_path: "ndi/cloud/exceptions.py" + decision_log: "Python-only. MATLAB raises generic errors with status codes in message." + + - name: CloudNotFoundError + type: exception + python_path: "ndi/cloud/exceptions.py" + decision_log: "Python-only 404 exception." + + - name: CloudSyncError + type: exception + python_path: "ndi/cloud/exceptions.py" + decision_log: "Python-only sync exception." + + - name: CloudUploadError + type: exception + python_path: "ndi/cloud/exceptions.py" + decision_log: "Python-only upload exception." + +# ============================================================================= +# Functions — Auth (auth.py) +# Maps to: +cloud/+api/+auth/ and +cloud/+internal/ +# ============================================================================= +functions: + + # --- auth.py functions --- + - name: authenticate + matlab_path: "+ndi/+cloud/authenticate.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: config + type_matlab: "N/A (uses env)" + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: token + type_python: "str" + decision_log: > + MATLAB authenticate returns [token, organizationID]. + Python returns token only; org_id is stored in config. + + - name: login + matlab_path: "+ndi/+cloud/+api/+auth/login.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: email + type_matlab: "char" + type_python: "str | None" + - name: password + type_matlab: "char" + type_python: "str | None" + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: config + type_python: "CloudConfig" + decision_log: "Exact match." + + - name: logout + matlab_path: "+ndi/+cloud/logout.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: [] + decision_log: "Exact match." + + - name: changePassword + matlab_path: "+ndi/+cloud/+api/+auth/changePassword.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: old_password + type_matlab: "char" + type_python: "str" + - name: new_password + type_matlab: "char" + type_python: "str" + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: resetPassword + matlab_path: "+ndi/+cloud/+api/+auth/resetPassword.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: email + type_matlab: "char" + type_python: "str" + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: verifyUser + matlab_path: "+ndi/+cloud/+api/+auth/verifyUser.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: email + type_matlab: "char" + type_python: "str" + - name: confirmation_code + type_matlab: "char" + type_python: "str" + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: resendConfirmation + matlab_path: "+ndi/+cloud/+api/+auth/resendConfirmation.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: email + type_matlab: "char" + type_python: "str" + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Exact match." + + - name: decodeJwt + matlab_path: "+ndi/+cloud/+internal/decodeJwt.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: token + type_matlab: "char" + type_python: "str" + output_arguments: + - name: payload + type_python: "dict" + decision_log: > + MATLAB places this in +internal; Python places it in auth.py + since it is tightly coupled with authentication logic. Name exact match. + + - name: getTokenExpiration + matlab_path: "+ndi/+cloud/+internal/getTokenExpiration.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: token + type_matlab: "char" + type_python: "str" + output_arguments: + - name: expiration + type_python: "datetime" + decision_log: > + MATLAB places this in +internal; Python places it in auth.py. + Name exact match. + + - name: verifyToken + matlab_path: "N/A" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: token + type_python: "str" + output_arguments: + - name: valid + type_python: "bool" + decision_log: "Python-only convenience. No MATLAB equivalent." + + - name: getActiveToken + matlab_path: "+ndi/+cloud/+internal/getActiveToken.m" + python_path: "ndi/cloud/auth.py" + input_arguments: + - name: config + type_python: "CloudConfig | None" + default: "None" + output_arguments: + - name: token + type_python: "str" + - name: org_id + type_python: "str" + decision_log: > + MATLAB places this in +internal; Python places it in auth.py. + Name exact match. + + # --- orchestration.py functions --- + - name: downloadDataset + matlab_path: "+ndi/+cloud/downloadDataset.m" + python_path: "ndi/cloud/orchestration.py" + input_arguments: + - name: cloud_dataset_id + type_matlab: "string" + type_python: "str" + - name: target_folder + type_matlab: "string" + type_python: "str" + - name: sync_files + type_matlab: "logical (via SyncOptions)" + type_python: "bool" + default: "False" + - name: verbose + type_matlab: "logical (via SyncOptions)" + type_python: "bool" + default: "False" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: ndi_dataset + type_python: "Any" + decision_log: > + MATLAB takes (cloudDatasetId, targetFolder, syncOptions). + Python unpacks SyncOptions into keyword args for convenience. + Name exact match. + + - name: uploadDataset + matlab_path: "+ndi/+cloud/uploadDataset.m" + python_path: "ndi/cloud/orchestration.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: upload_as_new + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: remote_name + type_matlab: "char" + type_python: "str" + default: "''" + - name: sync_files + type_matlab: "logical (via SyncOptions)" + type_python: "bool" + default: "True" + - name: verbose + type_matlab: "logical (via SyncOptions)" + type_python: "bool" + default: "False" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + - name: cloud_dataset_id + type_python: "str" + - name: message + type_python: "str" + decision_log: > + MATLAB takes (ndiDataset, syncOptions, options.*). + Python unpacks into keyword args. Name exact match. + + - name: syncDataset + matlab_path: "+ndi/+cloud/syncDataset.m" + python_path: "ndi/cloud/orchestration.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: sync_mode + type_matlab: "ndi.cloud.sync.enum.SyncMode" + type_python: "str" + default: "'download_new'" + - name: sync_files + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: verbose + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: dry_run + type_python: "bool" + default: "False" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: newDataset + matlab_path: "+ndi/+cloud/+upload/newDataset.m" + python_path: "ndi/cloud/orchestration.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: name + type_matlab: "char" + type_python: "str" + default: "''" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: cloud_dataset_id + type_python: "str" + decision_log: > + MATLAB places this in +upload; Python places it in orchestration.py + as a high-level convenience. Name exact match. + + - name: load_dataset_from_json_dir + matlab_path: "N/A" + python_path: "ndi/cloud/orchestration.py" + input_arguments: + - name: json_dir + type_python: "str | Path" + - name: target_folder + type_python: "str | Path | None" + - name: verbose + type_python: "bool" + default: "False" + - name: cloud_dataset_id + type_python: "str | None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: dataset + type_python: "Any" + decision_log: > + Python-only convenience function. No MATLAB equivalent. + Snake_case is acceptable for Python-only functions. + + # --- download.py functions --- + - name: dataset + matlab_path: "+ndi/+cloud/+download/dataset.m" + python_path: "ndi/cloud/download.py" + python_qualified: "ndi.cloud.download.dataset" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: target_dir + type_matlab: "string" + type_python: "str | Path" + - name: include_files + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: progress + type_python: "Callable | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: downloadDocumentCollection + matlab_path: "+ndi/+cloud/+download/downloadDocumentCollection.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: doc_ids + type_matlab: "string array" + type_python: "list[str] | None" + - name: chunk_size + type_matlab: "double" + type_python: "int" + default: "2000" + - name: timeout + type_matlab: "double" + type_python: "float" + default: "20.0" + - name: retry_interval + type_python: "float" + default: "1.0" + - name: progress + type_python: "Callable | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: documents + type_python: "list[dict[str, Any]]" + decision_log: "Exact match." + + - name: downloadFilesForDocument + matlab_path: "N/A" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: dataset_id + type_python: "str" + - name: document + type_python: "dict[str, Any]" + - name: target_dir + type_python: "Path" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: paths + type_python: "list[Path]" + decision_log: "Python-only helper for single-document file download." + + - name: downloadDatasetFiles + matlab_path: "+ndi/+cloud/+download/downloadDatasetFiles.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: documents + type_matlab: "N/A" + type_python: "list[dict[str, Any]]" + - name: target_dir + type_matlab: "string" + type_python: "Path" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: > + MATLAB takes (cloudDatasetId, targetFolder, fileUuids, options). + Python takes (dataset_id, documents, target_dir). Exact name match. + + - name: jsons2documents + matlab_path: "+ndi/+cloud/+download/jsons2documents.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: doc_jsons + type_matlab: "cell" + type_python: "list[dict[str, Any]]" + output_arguments: + - name: documents + type_python: "list[Any]" + decision_log: "Exact match." + + - name: datasetDocuments + matlab_path: "+ndi/+cloud/+download/datasetDocuments.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: dataset_info + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: mode + type_matlab: "char" + type_python: "str" + default: "'local'" + - name: json_dir + type_matlab: "char" + type_python: "str | Path | None" + - name: files_dir + type_matlab: "char" + type_python: "str | Path | None" + - name: verbose + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + - name: message + type_python: "str" + decision_log: "Exact match." + + - name: downloadGenericFiles + matlab_path: "+ndi/+cloud/+download/downloadGenericFiles.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: ndi_dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: ndi_document_ids + type_matlab: "string" + type_python: "list[str]" + - name: target_folder + type_matlab: "string" + type_python: "str | Path" + - name: verbose + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: zip_result + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: naming_strategy + type_matlab: "string" + type_python: "str" + default: "'original'" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + - name: error_message + type_python: "str" + - name: report + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: setFileInfo + matlab_path: "+ndi/+cloud/+download/+internal/setFileInfo.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: doc_struct + type_matlab: "struct" + type_python: "dict[str, Any]" + - name: mode + type_matlab: "char" + type_python: "str" + - name: filepath + type_matlab: "char" + type_python: "str" + output_arguments: + - name: doc_struct + type_python: "dict[str, Any]" + decision_log: > + MATLAB places this in +download/+internal; Python places it + in download.py. Exact name match. + + - name: structsToNdiDocuments + matlab_path: "+ndi/+cloud/+download/+internal/structsToNdiDocuments.m" + python_path: "ndi/cloud/download.py" + input_arguments: + - name: ndi_document_structs + type_matlab: "cell" + type_python: "list[dict[str, Any]]" + output_arguments: + - name: documents + type_python: "list[Any]" + decision_log: > + MATLAB places this in +download/+internal; Python places it + in download.py. Exact name match. + + - name: downloadFullDataset + matlab_path: "N/A" + python_path: "ndi/cloud/download.py" + decision_log: "Python backward-compatible alias for download.dataset." + + # --- upload.py functions --- + - name: uploadDocumentCollection + matlab_path: "+ndi/+cloud/+upload/uploadDocumentCollection.m" + python_path: "ndi/cloud/upload.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: documents + type_matlab: "cell" + type_python: "list[dict[str, Any]]" + - name: only_missing + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: max_chunk + type_python: "int | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: zipForUpload + matlab_path: "+ndi/+cloud/+upload/zipForUpload.m" + python_path: "ndi/cloud/upload.py" + input_arguments: + - name: documents + type_matlab: "cell" + type_python: "list[dict[str, Any]]" + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: target_dir + type_python: "Path | None" + default: "None" + output_arguments: + - name: zip_path + type_python: "Path" + - name: doc_ids + type_python: "list[str]" + decision_log: "Exact match." + + - name: uploadFilesForDatasetDocuments + matlab_path: "+ndi/+cloud/+sync/+internal/uploadFilesForDatasetDocuments.m" + python_path: "ndi/cloud/upload.py" + input_arguments: + - name: org_id + type_matlab: "char" + type_python: "str" + - name: dataset_id + type_matlab: "char" + type_python: "str" + - name: documents + type_matlab: "cell" + type_python: "list[dict[str, Any]]" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in upload.py. + Exact name match. + + - name: uploadSingleFile + matlab_path: "+ndi/+cloud/uploadSingleFile.m" + python_path: "ndi/cloud/upload.py" + input_arguments: + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: file_uid + type_matlab: "string" + type_python: "str" + - name: file_path + type_matlab: "string" + type_python: "str" + - name: use_bulk_upload + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + - name: error_message + type_python: "str" + decision_log: "Exact match." + + - name: uploadToNDICloud + matlab_path: "+ndi/+cloud/+upload/uploadToNDICloud.m" + python_path: "ndi/cloud/upload.py" + input_arguments: + - name: dataset + type_matlab: "ndi.session" + type_python: "Any" + - name: dataset_id + type_matlab: "char" + type_python: "str" + - name: verbose + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + - name: message + type_python: "str" + decision_log: "Exact match." + + - name: scanForUpload + matlab_path: "+ndi/+cloud/+upload/scanForUpload.m" + python_path: "ndi/cloud/upload.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: dataset_id + type_matlab: "string" + type_python: "str" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: docs_to_upload + type_python: "list[dict]" + - name: files_to_upload + type_python: "list[dict]" + - name: upload_size + type_python: "float" + decision_log: "Exact match." + + # --- filehandler.py functions --- + - name: updateFileInfoForRemoteFiles + matlab_path: "+ndi/+cloud/+sync/+internal/updateFileInfoForRemoteFiles.m" + python_path: "ndi/cloud/filehandler.py" + input_arguments: + - name: doc_props + type_matlab: "struct" + type_python: "dict" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + output_arguments: [] + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + filehandler.py. Exact name match. Mutates doc_props in-place. + + - name: updateFileInfoForLocalFiles + matlab_path: "+ndi/+cloud/+sync/+internal/updateFileInfoForLocalFiles.m" + python_path: "ndi/cloud/filehandler.py" + input_arguments: + - name: doc_props + type_matlab: "struct" + type_python: "dict" + - name: file_directory + type_matlab: "char" + type_python: "str" + output_arguments: [] + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + filehandler.py. Exact name match. Mutates doc_props in-place. + + - name: parse_ndic_uri + matlab_path: "N/A" + python_path: "ndi/cloud/filehandler.py" + input_arguments: + - name: uri + type_python: "str" + output_arguments: + - name: dataset_id + type_python: "str" + - name: file_uid + type_python: "str" + decision_log: "Python-only utility for ndic:// URI parsing." + + - name: fetch_cloud_file + matlab_path: "N/A" + python_path: "ndi/cloud/filehandler.py" + input_arguments: + - name: ndic_uri + type_python: "str" + - name: target_path + type_python: "str | Path" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: success + type_python: "bool" + decision_log: "Python-only on-demand cloud file fetcher." + + - name: get_or_create_cloud_client + matlab_path: "N/A" + python_path: "ndi/cloud/filehandler.py" + output_arguments: + - name: client + type_python: "CloudClient" + decision_log: "Python-only convenience for lazy client creation." + + # --- internal.py functions --- + - name: listRemoteDocumentIds + matlab_path: "+ndi/+cloud/+sync/+internal/listRemoteDocumentIds.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: id_map + type_python: "dict[str, str]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + internal.py. Exact name match. + + - name: getCloudDatasetIdForLocalDataset + matlab_path: "+ndi/+cloud/+internal/getCloudDatasetIdForLocalDataset.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: cloud_dataset_id + type_python: "str" + - name: remote_doc + type_python: "dict | None" + decision_log: "Exact match." + + - name: createRemoteDatasetDoc + matlab_path: "+ndi/+cloud/+internal/createRemoteDatasetDoc.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + output_arguments: + - name: doc + type_python: "Any" + decision_log: "Exact match." + + - name: listLocalDocuments + matlab_path: "+ndi/+cloud/+sync/+internal/listLocalDocuments.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + output_arguments: + - name: documents + type_python: "list[Any]" + - name: doc_ids + type_python: "list[str]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + internal.py. Exact name match. + + - name: getFileUidsFromDocuments + matlab_path: "+ndi/+cloud/+sync/+internal/getFileUidsFromDocuments.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: documents + type_matlab: "cell" + type_python: "list[Any]" + output_arguments: + - name: file_uids + type_python: "list[str]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + internal.py. Exact name match. + + - name: filesNotYetUploaded + matlab_path: "+ndi/+cloud/+sync/+internal/filesNotYetUploaded.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: file_manifest + type_matlab: "struct array" + type_python: "list[dict[str, Any]]" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: missing_files + type_python: "list[dict[str, Any]]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + internal.py. Exact name match. + + - name: validateSync + matlab_path: "N/A" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: dataset + type_python: "Any" + - name: cloud_dataset_id + type_python: "str" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: > + Python-only wrapper around sync validation. MATLAB equivalent is + ndi.cloud.sync.validate which is mapped in the sync bridge file. + + - name: datasetSessionIdFromDocs + matlab_path: "+ndi/+cloud/+sync/+internal/datasetSessionIdFromDocs.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: documents + type_matlab: "cell" + type_python: "list[Any]" + output_arguments: + - name: session_id + type_python: "str" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + internal.py. Exact name match. + + - name: duplicateDocuments + matlab_path: "+ndi/+cloud/+internal/duplicateDocuments.m" + python_path: "ndi/cloud/internal.py" + input_arguments: + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: delete_duplicates + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: maximum_delete_batch_size + type_python: "int" + default: "1000" + - name: verbose + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: duplicates + type_python: "list[dict[str, Any]]" + - name: unique + type_python: "list[dict[str, Any]]" + decision_log: "Exact match." + +# ============================================================================= +# Not-yet-ported MATLAB functions +# ============================================================================= +not_yet_ported: + + - name: dropDuplicateDocsFromJsonDecode + matlab_path: "+ndi/+cloud/+internal/dropDuplicateDocsFromJsonDecode.m" + status: not_yet_ported + decision_log: "MATLAB internal utility. May not be needed in Python." + + - name: getUploadedDocumentIds + matlab_path: "+ndi/+cloud/+internal/getUploadedDocumentIds.m" + status: not_yet_ported + decision_log: > + MATLAB internal. Python uses listRemoteDocumentIds instead + which serves the same purpose. + + - name: getUploadedFileIds + matlab_path: "+ndi/+cloud/+internal/getUploadedFileIds.m" + status: not_yet_ported + decision_log: "MATLAB internal for file upload tracking." + + - name: getWeboptionsWithAuthHeader + matlab_path: "+ndi/+cloud/+internal/getWeboptionsWithAuthHeader.m" + status: not_applicable + decision_log: > + MATLAB-specific HTTP configuration. Python uses CloudClient + with requests.Session which handles auth headers internally. + + - name: createCloudMetadataStruct + matlab_path: "+ndi/+cloud/+utility/createCloudMetadataStruct.m" + status: not_yet_ported + decision_log: "MATLAB utility for metadata struct creation." + + - name: mustBeValidMetadata + matlab_path: "+ndi/+cloud/+utility/mustBeValidMetadata.m" + status: not_yet_ported + decision_log: "MATLAB validation function for metadata." + + - name: authenticateOriginal + matlab_path: "+ndi/+cloud/authenticateOriginal.m" + status: not_applicable + decision_log: "Deprecated MATLAB function. Not needed in Python." + + - name: uilogin + matlab_path: "+ndi/+cloud/uilogin.m" + status: not_applicable + decision_log: "MATLAB GUI login. No Python equivalent needed." + + - name: zip_documents_for_upload + matlab_path: "+ndi/+cloud/+upload/+internal/zip_documents_for_upload.m" + status: not_applicable + decision_log: > + MATLAB internal helper. Python uses zipForUpload which handles + this internally with zipfile module. diff --git a/src/ndi/cloud/sync/ndi_matlab_python_bridge.yaml b/src/ndi/cloud/sync/ndi_matlab_python_bridge.yaml new file mode 100644 index 0000000..391d734 --- /dev/null +++ b/src/ndi/cloud/sync/ndi_matlab_python_bridge.yaml @@ -0,0 +1,335 @@ +# ndi_matlab_python_bridge.yaml — src/ndi/cloud/sync/ +# The Primary Contract for the ndi.cloud.sync namespace. +# +# Covers: mode.py, operations.py, index.py + +project_metadata: + bridge_version: "1.1" + naming_policy: "Strict MATLAB Mirror" + indexing_policy: "N/A (no user-facing counting)" + +# ============================================================================= +# Classes — mode.py +# Maps to: +cloud/+sync/+enum/SyncMode.m and +cloud/+sync/SyncOptions.m +# ============================================================================= +classes: + + - name: SyncMode + type: enum + matlab_path: "+ndi/+cloud/+sync/+enum/SyncMode.m" + python_path: "ndi/cloud/sync/mode.py" + python_class: "SyncMode" + members: + - name: DOWNLOAD_NEW + value: "download_new" + - name: MIRROR_FROM_REMOTE + value: "mirror_from_remote" + - name: UPLOAD_NEW + value: "upload_new" + - name: MIRROR_TO_REMOTE + value: "mirror_to_remote" + - name: TWO_WAY_SYNC + value: "two_way_sync" + decision_log: > + MATLAB uses PascalCase enum values (DownloadNew, UploadNew, etc). + Python uses UPPER_SNAKE for enum members (DOWNLOAD_NEW) with + lowercase string values matching MATLAB's string representation. + + - name: SyncOptions + type: dataclass + matlab_path: "+ndi/+cloud/+sync/SyncOptions.m" + python_path: "ndi/cloud/sync/mode.py" + python_class: "SyncOptions" + properties: + - name: sync_files + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: verbose + type_matlab: "logical" + type_python: "bool" + default: "True" + - name: dry_run + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: file_upload_strategy + type_python: "str" + default: "'batch'" + decision_log: "Exact match for shared fields." + + - name: SyncIndex + type: dataclass + matlab_path: "+ndi/+cloud/+sync/+internal/+index/" + python_path: "ndi/cloud/sync/index.py" + python_class: "SyncIndex" + properties: + - name: local_doc_ids_last_sync + type_python: "list[str]" + - name: remote_doc_ids_last_sync + type_python: "list[str]" + - name: last_sync_timestamp + type_python: "str" + methods: + - name: read + kind: classmethod + matlab_equivalent: "readSyncIndex" + input_arguments: + - name: dataset_path + type_python: "Path" + output_arguments: + - name: index + type_python: "SyncIndex" + decision_log: > + MATLAB has readSyncIndex as standalone function. + Python wraps it as a classmethod on SyncIndex dataclass. + + - name: write + matlab_equivalent: "writeSyncIndex" + input_arguments: + - name: dataset_path + type_python: "Path" + output_arguments: [] + decision_log: > + MATLAB has writeSyncIndex as standalone function. + Python wraps it as an instance method. + + - name: update + matlab_equivalent: "updateSyncIndex" + input_arguments: + - name: local_ids + type_python: "list[str]" + - name: remote_ids + type_python: "list[str]" + output_arguments: [] + decision_log: > + MATLAB has updateSyncIndex as standalone function. + Python wraps it as an instance method. + +# ============================================================================= +# Functions — operations.py +# Maps to: +cloud/+sync/ +# ============================================================================= +functions: + + - name: uploadNew + matlab_path: "+ndi/+cloud/+sync/uploadNew.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset_path + type_matlab: "char" + type_python: "str" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: options + type_matlab: "ndi.cloud.sync.SyncOptions" + type_python: "SyncOptions | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: downloadNew + matlab_path: "+ndi/+cloud/+sync/downloadNew.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset_path + type_matlab: "char" + type_python: "str" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: options + type_matlab: "ndi.cloud.sync.SyncOptions" + type_python: "SyncOptions | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: mirrorToRemote + matlab_path: "+ndi/+cloud/+sync/mirrorToRemote.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset_path + type_matlab: "char" + type_python: "str" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: options + type_matlab: "ndi.cloud.sync.SyncOptions" + type_python: "SyncOptions | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: mirrorFromRemote + matlab_path: "+ndi/+cloud/+sync/mirrorFromRemote.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset_path + type_matlab: "char" + type_python: "str" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: options + type_matlab: "ndi.cloud.sync.SyncOptions" + type_python: "SyncOptions | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: twoWaySync + matlab_path: "+ndi/+cloud/+sync/twoWaySync.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset_path + type_matlab: "char" + type_python: "str" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: options + type_matlab: "ndi.cloud.sync.SyncOptions" + type_python: "SyncOptions | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: validate + matlab_path: "+ndi/+cloud/+sync/validate.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset + type_matlab: "ndi.dataset" + type_python: "Any" + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: "Exact match." + + - name: sync + matlab_path: "N/A" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: dataset_path + type_python: "str" + - name: cloud_dataset_id + type_python: "str" + - name: mode + type_python: "SyncMode" + - name: options + type_python: "SyncOptions | None" + default: "None" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: result + type_python: "dict[str, Any]" + decision_log: > + Python-only dispatcher that routes to the specific sync operation + based on SyncMode. MATLAB uses syncDataset at the top level. + + - name: deleteLocalDocuments + matlab_path: "+ndi/+cloud/+sync/+internal/deleteLocalDocuments.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: ds_path + type_matlab: "char" + type_python: "Path" + - name: doc_ids + type_matlab: "string array" + type_python: "set[str]" + output_arguments: + - name: deleted_ids + type_python: "list[str]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + operations.py. Exact name match. + + - name: downloadNdiDocuments + matlab_path: "+ndi/+cloud/+sync/+internal/downloadNdiDocuments.m" + python_path: "ndi/cloud/sync/operations.py" + input_arguments: + - name: cloud_dataset_id + type_matlab: "char" + type_python: "str" + - name: ndi_to_api + type_python: "dict[str, str]" + - name: ids_to_download + type_python: "set[str]" + - name: client + type_python: "CloudClient | None" + default: "None" + output_arguments: + - name: docs + type_python: "list[dict[str, Any]]" + - name: failed_ids + type_python: "list[str]" + decision_log: > + MATLAB places this in +sync/+internal. Python places it in + operations.py. Exact name match. + +# ============================================================================= +# Not-yet-ported MATLAB functions +# ============================================================================= +not_yet_ported: + + - name: Constants + matlab_path: "+ndi/+cloud/+sync/+internal/Constants.m" + status: not_yet_ported + decision_log: "MATLAB sync constants class. Values may be inlined in Python." + + - name: deleteRemoteDocuments + matlab_path: "+ndi/+cloud/+sync/+internal/deleteRemoteDocuments.m" + status: not_yet_ported + decision_log: > + MATLAB internal for removing remote documents during mirror operations. + Python sync operations handle this inline via API calls. + + - name: createSyncIndexStruct + matlab_path: "+ndi/+cloud/+sync/+internal/+index/createSyncIndexStruct.m" + status: not_applicable + decision_log: > + MATLAB struct constructor. Python uses SyncIndex dataclass + with default field values instead. + + - name: getIndexFilepath + matlab_path: "+ndi/+cloud/+sync/+internal/+index/getIndexFilepath.m" + status: not_applicable + decision_log: > + MATLAB path resolver. Python handles this inside + SyncIndex.read() and SyncIndex.write() methods. diff --git a/src/ndi/dataset/_dataset.py b/src/ndi/dataset/_dataset.py index c23026c..608e48d 100644 --- a/src/ndi/dataset/_dataset.py +++ b/src/ndi/dataset/_dataset.py @@ -215,7 +215,7 @@ def unlink_session( raise ValueError( f"Session with ID {session_id} is an INGESTED session, " f"not a linked session. Cannot unlink. Use " - f"delete_ingested_session() instead." + f"deleteIngestedSession() instead." ) self.removeSessionInfoFromDataset(self, session_id) @@ -395,6 +395,25 @@ def database_openbinarydoc( """Open a binary document file.""" return self._session.database_openbinarydoc(doc_or_id, filename) + def database_existbinarydoc( + self, + doc_or_id: Any, + filename: str, + ) -> tuple[bool, Path | None]: + """ + Check if a binary document file exists. + + MATLAB equivalent: ``ndi.dataset/database_existbinarydoc`` + + Args: + doc_or_id: Document or document ID. + filename: Name of the binary file. + + Returns: + Tuple of (exists, file_path). + """ + return self._session.database_existbinarydoc(doc_or_id, filename) + def database_closebinarydoc(self, fid: Any) -> None: """Close a binary document file.""" self._session.database_closebinarydoc(fid) @@ -403,7 +422,7 @@ def database_closebinarydoc(self, fid: Any) -> None: # Ingested Session Management # ========================================================================= - def delete_ingested_session( + def deleteIngestedSession( self, session_id: str, are_you_sure: bool = False, @@ -921,6 +940,32 @@ def _ensure_session_tracking(self) -> None: except Exception: logger.debug("Could not register session %s: skipping", sid) + @staticmethod + def dataset_erase(ndi_dataset_dir_obj: DatasetDir, areyousure: str = "no") -> None: + """ + Delete the entire dataset database folder. + + MATLAB equivalent: ``ndi.dataset.dir.dataset_erase`` + + Use with care. If *areyousure* is ``'yes'`` the ``.ndi`` + directory inside the dataset path will be permanently removed. + + Args: + ndi_dataset_dir_obj: The DatasetDir instance to erase. + areyousure: Must be ``'yes'`` to proceed. + """ + import shutil + + if areyousure.lower() == "yes": + ndi_dir = ndi_dataset_dir_obj.getpath() / ".ndi" + if ndi_dir.exists(): + shutil.rmtree(ndi_dir) + else: + logger.info( + "Not erasing dataset directory folder because " + "user did not indicate they are sure." + ) + @staticmethod def _dataset_session_id_from_docs(documents: list[Document]) -> str: """Extract the dataset session ID from a list of documents. diff --git a/src/ndi/dataset/ndi_matlab_python_bridge.yaml b/src/ndi/dataset/ndi_matlab_python_bridge.yaml new file mode 100644 index 0000000..3a78621 --- /dev/null +++ b/src/ndi/dataset/ndi_matlab_python_bridge.yaml @@ -0,0 +1,325 @@ +# ndi_matlab_python_bridge.yaml — src/ndi/dataset/ +# The Primary Contract for the ndi.dataset namespace. + +project_metadata: + bridge_version: "1.1" + naming_policy: "Strict MATLAB Mirror" + indexing_policy: "Semantic Parity (1-based for user concepts, 0-based for internal data)" + +classes: + + # ========================================================================= + # ndi.dataset (base class) + # ========================================================================= + - name: dataset + type: class + matlab_path: "+ndi/dataset.m" + python_path: "ndi/dataset/_dataset.py" + python_class: "Dataset" + + properties: + - name: session + type_matlab: "ndi.session" + type_python: "Any" + access: "protected" + decision_log: "Stored as _session in Python. Access=protected in MATLAB." + + - name: session_info + type_matlab: "struct" + type_python: "list[dict[str, Any]]" + access: "protected" + decision_log: "Stored as _session_info in Python." + + - name: session_array + type_matlab: "struct array" + type_python: "list[dict[str, Any]]" + access: "protected" + decision_log: "Stored as _session_array in Python." + + methods: + # --- Constructor --- + - name: dataset + kind: constructor + input_arguments: + - name: reference + type_matlab: "char" + type_python: "str" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: "Python __init__ takes no arguments; session is set by subclass." + + # --- Public instance methods --- + - name: id + input_arguments: [] + output_arguments: + - name: identifier + type_python: "str" + decision_log: "Delegates to self._session.id()." + + - name: reference + kind: property + type_matlab: "char" + type_python: "str" + decision_log: "Implemented as a @property in Python, matching MATLAB getter." + + - name: getpath + input_arguments: [] + output_arguments: + - name: p + type_python: "Path" + decision_log: "Delegates to self._session.getpath()." + + - name: add_linked_session + input_arguments: + - name: ndi_session_obj + type_matlab: "ndi.session" + type_python: "Any" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: "MATLAB name uses underscores. Exact match." + + - name: add_ingested_session + input_arguments: + - name: ndi_session_obj + type_matlab: "ndi.session" + type_python: "Any" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: "MATLAB name uses underscores. Exact match." + + - name: deleteIngestedSession + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "str" + - name: are_you_sure + type_matlab: "logical" + type_python: "bool" + default: "False" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: > + MATLAB name is deleteIngestedSession (camelCase). + Python was previously delete_ingested_session; renamed to match MATLAB. + + - name: unlink_session + input_arguments: + - name: ndi_session_id + type_matlab: "char" + type_python: "str" + - name: are_you_sure + type_matlab: "logical" + type_python: "bool" + default: "False" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: "MATLAB name uses underscores. Exact match." + + - name: open_session + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "str" + output_arguments: + - name: ndi_session_obj + type_python: "Any | None" + decision_log: "MATLAB name uses underscores. Exact match." + + - name: session_list + input_arguments: [] + output_arguments: + - name: ref_list + type_python: "list[str]" + - name: id_list + type_python: "list[str]" + - name: session_doc_ids + type_python: "list[str]" + - name: dataset_session_doc_id + type_python: "str" + decision_log: "Returns a 4-tuple matching the MATLAB [ref_list, id_list, session_doc_ids, dataset_session_doc_id]." + + - name: database_add + input_arguments: + - name: document + type_matlab: "ndi.document | cell" + type_python: "Document | list[Document]" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: "Exact match." + + - name: database_rm + input_arguments: + - name: doc_unique_id + type_matlab: "ndi.document | char | cell" + type_python: "Document | str | list" + - name: error_if_not_found + type_matlab: "logical (name-value)" + type_python: "bool" + default: "False" + output_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + decision_log: "Exact match." + + - name: database_search + input_arguments: + - name: searchparameters + type_matlab: "ndi.query" + type_python: "Query" + output_arguments: + - name: ndi_document_obj + type_python: "list[Document]" + decision_log: "Exact match." + + - name: database_openbinarydoc + input_arguments: + - name: ndi_document_or_id + type_matlab: "ndi.document | char" + type_python: "Any" + - name: filename + type_matlab: "char" + type_python: "str" + output_arguments: + - name: ndi_binarydoc_obj + type_python: "Any" + decision_log: "Exact match." + + - name: database_existbinarydoc + input_arguments: + - name: ndi_document_or_id + type_matlab: "ndi.document | char" + type_python: "Any" + - name: filename + type_matlab: "char" + type_python: "str" + output_arguments: + - name: tf + type_python: "bool" + - name: file_path + type_python: "Path | None" + decision_log: "Delegates to self._session.database_existbinarydoc(). Exact match." + + - name: database_closebinarydoc + input_arguments: + - name: ndi_binarydoc_obj + type_matlab: "ndi_binarydoc_obj" + type_python: "Any" + output_arguments: [] + decision_log: "Exact match." + + - name: document_session + input_arguments: + - name: ndi_document_obj + type_matlab: "ndi.document" + type_python: "Document" + output_arguments: + - name: ndi_session_obj + type_python: "Any | None" + decision_log: "Exact match." + + # --- Protected methods --- + - name: build_session_info + kind: protected + input_arguments: [] + output_arguments: [] + decision_log: "Exact match." + + - name: open_linked_sessions + kind: protected + input_arguments: [] + output_arguments: [] + decision_log: "Python uses _open_linked_sessions (leading underscore for protected convention)." + + # --- Static methods --- + - name: repairDatasetSessionInfo + kind: static + input_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + - name: doc + type_python: "list[Document]" + output_arguments: + - name: new_docs + type_python: "list[Document]" + decision_log: "MATLAB camelCase preserved exactly." + + - name: addSessionInfoToDataset + kind: static + input_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + - name: session_info + type_python: "dict[str, Any]" + output_arguments: + - name: new_doc + type_python: "Document" + decision_log: "MATLAB camelCase preserved exactly." + + - name: removeSessionInfoFromDataset + kind: static + input_arguments: + - name: ndi_dataset_obj + type_python: "Dataset" + - name: session_id + type_python: "str" + output_arguments: [] + decision_log: "MATLAB camelCase preserved exactly." + + # ========================================================================= + # ndi.dataset.dir (directory-backed subclass) + # ========================================================================= + - name: dir + type: class + matlab_path: "+ndi/+dataset/dir.m" + python_path: "ndi/dataset/_dataset.py" + python_class: "DatasetDir" + inherits: "ndi.dataset" + + properties: + - name: path + type_matlab: "char" + type_python: "Path" + access: "GetAccess=public, SetAccess=protected" + decision_log: "Stored as _path in Python." + + methods: + - name: dir + kind: constructor + input_arguments: + - name: reference + type_matlab: "char" + type_python: "str | Path" + - name: path_name + type_matlab: "char" + type_python: "str | Path | None" + - name: docs + type_matlab: "cell (hidden)" + type_python: "list[Document] | None" + output_arguments: + - name: ndi_dataset_dir_obj + type_python: "DatasetDir" + decision_log: > + Python constructor supports MATLAB calling conventions: + dir(path), dir(reference, path), dir(reference, path, docs). + + static_methods: + - name: dataset_erase + input_arguments: + - name: ndi_dataset_dir_obj + type_matlab: "ndi.dataset.dir" + type_python: "DatasetDir" + - name: areyousure + type_matlab: "char" + type_python: "str" + default: "'no'" + output_arguments: [] + decision_log: > + Removes the .ndi directory from the dataset path. + MATLAB also calls delete(obj); Python does not destroy the object + (no equivalent of MATLAB handle delete). diff --git a/src/ndi/session/dir.py b/src/ndi/session/dir.py index c95b2bb..7277e32 100644 --- a/src/ndi/session/dir.py +++ b/src/ndi/session/dir.py @@ -206,7 +206,7 @@ def creator_args(self) -> list[Any]: """ return [self._reference, str(self._path), self._identifier] - def delete_session_data_structures( + def deleteSessionDataStructures( self, are_you_sure: bool = False, ask_user: bool = True, diff --git a/src/ndi/session/ndi_matlab_python_bridge.yaml b/src/ndi/session/ndi_matlab_python_bridge.yaml new file mode 100644 index 0000000..b51a21d --- /dev/null +++ b/src/ndi/session/ndi_matlab_python_bridge.yaml @@ -0,0 +1,655 @@ +# ndi_matlab_python_bridge.yaml — src/ndi/session/ +# The Primary Contract for the ndi.session namespace. + +project_metadata: + bridge_version: "1.1" + naming_policy: "Strict MATLAB Mirror" + indexing_policy: "Semantic Parity (1-based for user concepts, 0-based for internal data)" + +# ========================================================================= +# Standalone functions +# ========================================================================= +functions: + + - name: empty_id + type: function + matlab_path: "+ndi/+session/empty_id.m" + python_path: "ndi/session/session_base.py" + input_arguments: [] + output_arguments: + - name: id + type_python: "str" + decision_log: "Exact match. Returns '0000000000000000_0000000000000000'." + +# ========================================================================= +# Classes +# ========================================================================= +classes: + + # ========================================================================= + # ndi.session (abstract base class) + # ========================================================================= + - name: session + type: class + matlab_path: "+ndi/session.m" + python_path: "ndi/session/session_base.py" + python_class: "Session" + + properties: + - name: reference + type_matlab: "string" + type_python: "str" + access: "GetAccess=public, SetAccess=protected" + decision_log: "Implemented as @property in Python." + + - name: identifier + type_matlab: "unique identifier" + type_python: "str" + access: "GetAccess=public, SetAccess=protected" + decision_log: "Implemented as @property in Python." + + - name: syncgraph + type_matlab: "ndi.time.syncgraph" + type_python: "SyncGraph | None" + access: "GetAccess=public, SetAccess=protected, Transient" + decision_log: "Implemented as @property in Python." + + - name: cache + type_matlab: "ndi.cache" + type_python: "Cache" + access: "GetAccess=public, SetAccess=protected, Transient" + decision_log: "Implemented as @property in Python." + + - name: database + type_matlab: "ndi.database" + type_python: "Database | None" + access: "GetAccess={?session, ?ndi.dataset}, SetAccess=protected, Transient" + decision_log: "Implemented as @property in Python." + + methods: + # --- Constructor --- + - name: session + kind: constructor + input_arguments: + - name: reference + type_matlab: "char" + type_python: "str" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + # --- Public instance methods --- + - name: id + input_arguments: [] + output_arguments: + - name: identifier + type_python: "str" + decision_log: "Exact match." + + - name: unique_reference_string + input_arguments: [] + output_arguments: + - name: refstr + type_python: "str" + decision_log: > + Deprecated in MATLAB (warns to use id() instead). + Python port mirrors this by issuing a DeprecationWarning. + + - name: daqsystem_add + input_arguments: + - name: dev + type_matlab: "ndi.daq.system" + type_python: "Any" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: daqsystem_rm + input_arguments: + - name: dev + type_matlab: "ndi.daq.system" + type_python: "Any" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: daqsystem_load + input_arguments: + - name: name + type_matlab: "char (name-value)" + type_python: "str | None" + - name: kwargs + type_matlab: "varargin (name-value pairs)" + type_python: "**kwargs" + output_arguments: + - name: dev + type_python: "list[Any] | Any | None" + decision_log: "MATLAB uses varargin name-value pairs; Python uses keyword arguments." + + - name: daqsystem_clear + input_arguments: [] + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: newdocument + input_arguments: + - name: document_type + type_matlab: "char" + type_python: "str" + default: "'base'" + - name: properties + type_matlab: "varargin" + type_python: "**properties" + output_arguments: + - name: ndi_document_obj + type_python: "Document" + decision_log: "Exact match." + + - name: searchquery + input_arguments: [] + output_arguments: + - name: sq + type_python: "Query" + decision_log: "Exact match." + + - name: database_add + input_arguments: + - name: document + type_matlab: "ndi.document | cell" + type_python: "Document | list[Document]" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: database_rm + input_arguments: + - name: doc_unique_id + type_matlab: "ndi.document | char | cell" + type_python: "Document | str | list[Document | str]" + - name: error_if_not_found + type_matlab: "logical (name-value)" + type_python: "bool" + default: "False" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: database_search + input_arguments: + - name: searchparameters + type_matlab: "ndi.query" + type_python: "Query" + output_arguments: + - name: ndi_document_obj + type_python: "list[Document]" + decision_log: "Exact match." + + - name: database_clear + input_arguments: + - name: areyousure + type_matlab: "char" + type_python: "str" + output_arguments: [] + decision_log: "Exact match." + + - name: validate_documents + input_arguments: + - name: document + type_matlab: "ndi.document | cell" + type_python: "Document | list[Document]" + output_arguments: + - name: b + type_python: "bool" + - name: errmsg + type_python: "str" + decision_log: "Exact match. Returns (bool, str) tuple." + + - name: database_openbinarydoc + input_arguments: + - name: ndi_document_or_id + type_matlab: "ndi.document | char" + type_python: "Document | str" + - name: filename + type_matlab: "char" + type_python: "str" + output_arguments: + - name: ndi_binarydoc_obj + type_python: "Any" + decision_log: > + MATLAB has an optional 'autoClose' name-value pair. + Python omits this; auto-close is not implemented. + + - name: database_existbinarydoc + input_arguments: + - name: ndi_document_or_id + type_matlab: "ndi.document | char" + type_python: "Document | str" + - name: filename + type_matlab: "char" + type_python: "str" + output_arguments: + - name: tf + type_python: "bool" + - name: file_path + type_python: "Path | None" + decision_log: "Exact match. Returns (bool, Path | None) tuple." + + - name: database_closebinarydoc + input_arguments: + - name: ndi_binarydoc_obj + type_python: "Any" + output_arguments: [] + decision_log: "Exact match." + + - name: syncgraph_addrule + input_arguments: + - name: rule + type_matlab: "ndi.time.syncrule" + type_python: "SyncRule" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: syncgraph_rmrule + input_arguments: + - name: index + type_matlab: "integer" + type_python: "int" + output_arguments: + - name: ndi_session_obj + type_python: "Session" + decision_log: "Exact match." + + - name: ingest + input_arguments: [] + output_arguments: + - name: b + type_python: "bool" + - name: errmsg + type_python: "str" + decision_log: "Exact match. Returns (bool, str) tuple." + + - name: get_ingested_docs + input_arguments: [] + output_arguments: + - name: d + type_python: "list[Document]" + decision_log: "Exact match." + + - name: is_fully_ingested + input_arguments: [] + output_arguments: + - name: b + type_python: "bool" + decision_log: "Exact match." + + - name: isIngestedInDataset + input_arguments: [] + output_arguments: + - name: b + type_python: "bool" + decision_log: > + Searches for session_in_a_dataset documents with is_linked=0. + Exact match to MATLAB logic. + + - name: getprobes + input_arguments: + - name: classmatch + type_matlab: "char (optional)" + type_python: "str | None" + - name: kwargs + type_matlab: "varargin (property-value pairs)" + type_python: "**kwargs" + output_arguments: + - name: probes + type_python: "list[Any]" + decision_log: "Exact match." + + - name: getelements + input_arguments: + - name: kwargs + type_matlab: "varargin (property-value pairs)" + type_python: "**kwargs" + output_arguments: + - name: elements + type_python: "list[Any]" + decision_log: "Exact match." + + - name: getpath + kind: abstract + input_arguments: [] + output_arguments: + - name: p + type_python: "Path | None" + decision_log: "Exact match. Abstract in both." + + - name: findexpobj + input_arguments: + - name: obj_name + type_matlab: "string" + type_python: "str" + - name: obj_classname + type_matlab: "string" + type_python: "str" + output_arguments: + - name: obj + type_python: "Any | None" + decision_log: > + MATLAB uses vlt.matlab.isa_text for class matching. + Python uses type(obj).__name__ comparison. + Returns None instead of empty array when not found. + + - name: eq + input_arguments: + - name: other + type_python: "Any" + output_arguments: + - name: b + type_python: "bool" + decision_log: "Mapped to Python __eq__ dunder method." + + - name: creator_args + kind: abstract + input_arguments: [] + output_arguments: + - name: inputs + type_python: "list[Any]" + decision_log: "Exact match. Abstract in both." + + # --- Static methods --- + static_methods: + - name: docinput2docs + input_arguments: + - name: ndi_session_obj + type_python: "Session" + - name: doc_input + type_python: "str | Document | list[str | Document]" + output_arguments: + - name: doc_list + type_python: "list[Document]" + decision_log: > + MATLAB is a static method. Python implements as instance method + _docinput2docs (protected convention). Functionality is equivalent. + + # --- Protected methods --- + protected_methods: + - name: update_syncgraph_in_db + input_arguments: [] + output_arguments: [] + decision_log: "Python uses _update_syncgraph_in_db (leading underscore)." + + # ========================================================================= + # ndi.session.dir (directory-based session) + # ========================================================================= + - name: dir + type: class + matlab_path: "+ndi/+session/dir.m" + python_path: "ndi/session/dir.py" + python_class: "DirSession" + inherits: "ndi.session" + + properties: + - name: path + type_matlab: "char" + type_python: "Path" + access: "GetAccess=public, SetAccess=protected" + decision_log: "Implemented as @property in Python." + + methods: + - name: dir + kind: constructor + input_arguments: + - name: reference + type_matlab: "char" + type_python: "str | Path" + - name: path + type_matlab: "char" + type_python: "str | Path | None" + - name: session_id + type_matlab: "(undocumented)" + type_python: "str | None" + output_arguments: + - name: ndi_session_dir_obj + type_python: "DirSession" + decision_log: > + Supports MATLAB calling conventions: dir(path) and dir(reference, path). + session_id is an undocumented internal parameter in both languages. + + - name: getpath + input_arguments: [] + output_arguments: + - name: p + type_python: "Path" + decision_log: "Exact match." + + - name: ndipathname + input_arguments: [] + output_arguments: + - name: p + type_python: "Path" + decision_log: "Exact match." + + - name: eq + input_arguments: + - name: other + type_python: "Any" + output_arguments: + - name: b + type_python: "bool" + decision_log: "Mapped to Python __eq__ dunder method." + + - name: creator_args + input_arguments: [] + output_arguments: + - name: inputs + type_python: "list[Any]" + decision_log: "Exact match." + + - name: deleteSessionDataStructures + input_arguments: + - name: areYouSure + type_matlab: "logical" + type_python: "bool" + default: "False" + - name: askUserToConfirm + type_matlab: "logical" + type_python: "bool" + default: "True" + output_arguments: + - name: obj_out + type_python: "DirSession | None" + decision_log: > + MATLAB name is deleteSessionDataStructures (camelCase). + Python was previously delete_session_data_structures; renamed to match MATLAB. + + static_methods: + - name: exists + input_arguments: + - name: path + type_matlab: "char" + type_python: "str | Path" + output_arguments: + - name: exists + type_python: "bool" + decision_log: "Exact match." + + - name: database_erase + input_arguments: + - name: ndi_session_dir_obj + type_python: "DirSession" + - name: areyousure + type_matlab: "char" + type_python: "str" + default: "'no'" + output_arguments: [] + decision_log: "Exact match." + + # ========================================================================= + # ndi.session.sessiontable + # ========================================================================= + - name: sessiontable + type: class + matlab_path: "+ndi/+session/sessiontable.m" + python_path: "ndi/session/sessiontable.py" + python_class: "SessionTable" + + methods: + - name: sessiontable + kind: constructor + input_arguments: + - name: table_path + type_python: "Path | None" + output_arguments: + - name: ndi_sessiontable_obj + type_python: "SessionTable" + decision_log: "Exact match." + + - name: getsessiontable + input_arguments: [] + output_arguments: + - name: t + type_matlab: "struct" + type_python: "list[dict[str, str]]" + decision_log: > + MATLAB name is getsessiontable (all lowercase, no separators). + Python was previously get_session_table; renamed to match MATLAB. + + - name: getsessionpath + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "str" + output_arguments: + - name: thepath + type_python: "str | None" + decision_log: > + MATLAB name is getsessionpath (all lowercase, no separators). + Python was previously get_session_path; renamed to match MATLAB. + + - name: addtableentry + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "str" + - name: path + type_matlab: "char" + type_python: "str" + output_arguments: [] + decision_log: > + MATLAB name is addtableentry (all lowercase, no separators). + Python was previously add_entry; renamed to match MATLAB. + + - name: removetableentry + input_arguments: + - name: session_id + type_matlab: "char" + type_python: "str" + output_arguments: [] + decision_log: > + MATLAB name is removetableentry (all lowercase, no separators). + Python was previously remove_entry; renamed to match MATLAB. + + - name: checktable + input_arguments: [] + output_arguments: + - name: b + type_python: "bool" + - name: results + type_python: "list[dict[str, Any]]" + decision_log: > + MATLAB name is checktable (all lowercase, no separators). + Python was previously check_table; renamed to match MATLAB. + + - name: isvalidtable + input_arguments: + - name: t + type_matlab: "struct (optional)" + type_python: "list[dict[str, str]] | None" + output_arguments: + - name: b + type_python: "bool" + - name: msg + type_python: "str" + decision_log: > + MATLAB name is isvalidtable (all lowercase, no separators). + Python was previously is_valid_table; renamed to match MATLAB. + + - name: backupsessiontable + input_arguments: [] + output_arguments: + - name: backup_path + type_python: "Path | None" + decision_log: > + MATLAB name is backupsessiontable (all lowercase, no separators). + Python was previously backup; renamed to match MATLAB. + Python returns the backup path; MATLAB returns nothing. + + - name: backupfilelist + input_arguments: [] + output_arguments: + - name: f + type_python: "list[Path]" + decision_log: > + MATLAB name is backupfilelist (all lowercase, no separators). + Python was previously backup_file_list; renamed to match MATLAB. + + - name: clearsessiontable + input_arguments: + - name: makebackup + type_matlab: "logical (optional)" + type_python: "bool" + default: "False" + output_arguments: [] + decision_log: > + MATLAB name is clearsessiontable (all lowercase, no separators). + Python was previously clear; renamed to match MATLAB. + + static_methods: + - name: localtablefilename + input_arguments: [] + output_arguments: + - name: f + type_python: "Path" + decision_log: > + MATLAB name is localtablefilename (all lowercase, no separators). + Python was previously local_table_filename; renamed to match MATLAB. + + protected_methods: + - name: writesessiontable + input_arguments: + - name: t + type_python: "list[dict[str, str]]" + output_arguments: [] + decision_log: > + MATLAB name is writesessiontable (all lowercase, no separators). + Python was previously _write_table; renamed to _writesessiontable. + + # ========================================================================= + # ndi.session.mock + # ========================================================================= + - name: mock + type: class + matlab_path: "+ndi/+session/mock.m" + python_path: "ndi/session/mock.py" + python_class: "MockSession" + inherits: "ndi.session.dir" + + methods: + - name: mock + kind: constructor + input_arguments: [] + output_arguments: + - name: ndi_session_mock_obj + type_python: "MockSession" + decision_log: > + MATLAB mock() takes no arguments; Python constructor adds + reference, prefix, and cleanup parameters for flexibility. + This is a Python-specific enhancement documented here. diff --git a/src/ndi/session/session_base.py b/src/ndi/session/session_base.py index 0bcf289..982cac1 100644 --- a/src/ndi/session/session_base.py +++ b/src/ndi/session/session_base.py @@ -97,6 +97,30 @@ def id(self) -> str: """ return self._identifier + def unique_reference_string(self) -> str: + """ + Return the unique reference string for this session. + + MATLAB equivalent: ``ndi.session/unique_reference_string`` + + Returns a combination of the reference and identifier joined + with ``'_'``. + + .. deprecated:: + Use :meth:`id` instead. + + Returns: + String of the form ``'{reference}_{identifier}'`` + """ + import warnings + + warnings.warn( + "unique_reference_string is deprecated, use id() instead.", + DeprecationWarning, + stacklevel=2, + ) + return f"{self._reference}_{self._identifier}" + @property def syncgraph(self) -> SyncGraph | None: """Get the session's syncgraph.""" @@ -749,6 +773,38 @@ def is_fully_ingested(self) -> bool: return False return True + def isIngestedInDataset(self) -> bool: + """ + Check if the session is ingested in a dataset. + + MATLAB equivalent: ``ndi.session/isIngestedInDataset`` + + Searches for a ``session_in_a_dataset`` document in the database + that matches this session's ID and has ``is_linked=0`` (i.e. the + session was ingested, not merely linked). + + Returns: + True if the session is ingested in a dataset, False otherwise. + """ + if self._database is None: + return False + + q = Query("").isa("session_in_a_dataset") & ( + Query("session_in_a_dataset.session_id") == self.id() + ) + docs = self._database.search(q) + + for doc in docs: + props = doc.document_properties + sia = props.get("session_in_a_dataset", {}) + is_linked = sia.get("is_linked", True) + if isinstance(is_linked, (int, float)): + is_linked = bool(is_linked) + if not is_linked: + return True + + return False + # ========================================================================= # Probe and Element Methods # ========================================================================= @@ -895,6 +951,63 @@ def getelements(self, **kwargs) -> list[Any]: return elements + def findexpobj(self, obj_name: str, obj_classname: str) -> Any | None: + """ + Search the session for a specific object by name and class. + + MATLAB equivalent: ``ndi.session/findexpobj`` + + Examines the DAQ system list and probe list for an object with + the given *obj_name* and *obj_classname*. + + Args: + obj_name: Name of the object to find. + obj_classname: Class name to match (e.g. ``'Probe'``, + ``'DAQSystem'``). + + Returns: + The matching object, or None if not found. + """ + # Determine where to search based on classname + classname_lower = obj_classname.lower() + + # Check DAQ systems + if "daqsystem" in classname_lower or "daq" in classname_lower: + obj = self.daqsystem_load(name=obj_name) + if obj is not None: + if not isinstance(obj, list): + obj = [obj] + for o in obj: + if type(o).__name__ == obj_classname: + return o + return None + + # Check probes + if "probe" in classname_lower: + probes = self.getprobes() + for p in probes: + p_name = p.name if hasattr(p, "name") else "" + if type(p).__name__ == obj_classname and p_name == obj_name: + return p + return None + + # Fallback: search probes then DAQ systems + probes = self.getprobes() + for p in probes: + p_name = p.name if hasattr(p, "name") else "" + if type(p).__name__ == obj_classname and p_name == obj_name: + return p + + obj = self.daqsystem_load(name=obj_name) + if obj is not None: + if not isinstance(obj, list): + obj = [obj] + for o in obj: + if type(o).__name__ == obj_classname: + return o + + return None + # ========================================================================= # Document Service Methods # ========================================================================= diff --git a/src/ndi/session/sessiontable.py b/src/ndi/session/sessiontable.py index 3317fd8..aa86506 100644 --- a/src/ndi/session/sessiontable.py +++ b/src/ndi/session/sessiontable.py @@ -28,8 +28,8 @@ class SessionTable: Example: >>> table = SessionTable() - >>> table.add_entry('abc123', '/data/experiment1') - >>> table.get_session_path('abc123') + >>> table.addtableentry('abc123', '/data/experiment1') + >>> table.getsessionpath('abc123') '/data/experiment1' """ @@ -41,25 +41,28 @@ def __init__(self, table_path: Path | None = None): Args: table_path: Override the default table file location. - If None, uses ``local_table_filename()``. + If None, uses ``localtablefilename()``. """ - self._table_path = ( - Path(table_path) if table_path is not None else self.local_table_filename() - ) + self._table_path = Path(table_path) if table_path is not None else self.localtablefilename() @staticmethod - def local_table_filename() -> Path: - """Return the default session table file path.""" + def localtablefilename() -> Path: + """Return the default session table file path. + + MATLAB equivalent: ``ndi.session.sessiontable.localtablefilename`` + """ return Path.home() / ".ndi" / "preferences" / "local_sessiontable.txt" # ------------------------------------------------------------------ # Read operations # ------------------------------------------------------------------ - def get_session_table(self) -> list[dict[str, str]]: + def getsessiontable(self) -> list[dict[str, str]]: """ Read and return the session table. + MATLAB equivalent: ``ndi.session.sessiontable/getsessiontable`` + Returns: List of dicts with keys ``session_id`` and ``path``. Returns an empty list if the file does not exist or is empty. @@ -85,17 +88,19 @@ def get_session_table(self) -> list[dict[str, str]]: except Exception: return [] - def get_session_path(self, session_id: str) -> str | None: + def getsessionpath(self, session_id: str) -> str | None: """ Look up the filesystem path for *session_id*. + MATLAB equivalent: ``ndi.session.sessiontable/getsessionpath`` + Args: session_id: The session identifier to search for. Returns: Path string if found, None otherwise. """ - for entry in self.get_session_table(): + for entry in self.getsessiontable(): if entry["session_id"] == session_id: return entry["path"] return None @@ -104,10 +109,12 @@ def get_session_path(self, session_id: str) -> str | None: # Write operations # ------------------------------------------------------------------ - def add_entry(self, session_id: str, path: str) -> None: + def addtableentry(self, session_id: str, path: str) -> None: """ Add or replace an entry in the session table. + MATLAB equivalent: ``ndi.session.sessiontable/addtableentry`` + If *session_id* already exists it is replaced with the new *path*. Args: @@ -123,48 +130,54 @@ def add_entry(self, session_id: str, path: str) -> None: raise ValueError("path must be a non-empty string") # Remove existing entry for this ID (if any), then append - self.remove_entry(session_id) - entries = self.get_session_table() + self.removetableentry(session_id) + entries = self.getsessiontable() entries.append({"session_id": session_id, "path": path}) - self._write_table(entries) + self._writesessiontable(entries) - def remove_entry(self, session_id: str) -> None: + def removetableentry(self, session_id: str) -> None: """ Remove the entry with the given *session_id*. + MATLAB equivalent: ``ndi.session.sessiontable/removetableentry`` + Does nothing if the ID is not present. """ - entries = self.get_session_table() + entries = self.getsessiontable() filtered = [e for e in entries if e["session_id"] != session_id] if len(filtered) != len(entries): - self._write_table(filtered) + self._writesessiontable(filtered) - def clear(self, make_backup: bool = False) -> None: + def clearsessiontable(self, make_backup: bool = False) -> None: """ Remove all entries from the session table. + MATLAB equivalent: ``ndi.session.sessiontable/clearsessiontable`` + Args: make_backup: If True, create a backup before clearing. """ if make_backup: - self.backup() - self._write_table([]) + self.backupsessiontable() + self._writesessiontable([]) # ------------------------------------------------------------------ # Validation # ------------------------------------------------------------------ - def check_table(self) -> tuple[bool, list[dict[str, Any]]]: + def checktable(self) -> tuple[bool, list[dict[str, Any]]]: """ Validate the session table and check path accessibility. + MATLAB equivalent: ``ndi.session.sessiontable/checktable`` + Returns: Tuple of (valid, results): - valid: True if the table has the correct format - results: List of dicts with ``exists`` key for each entry """ - entries = self.get_session_table() - valid, _ = self.is_valid_table(entries) + entries = self.getsessiontable() + valid, _ = self.isvalidtable(entries) if not valid: return False, [] @@ -179,13 +192,15 @@ def check_table(self) -> tuple[bool, list[dict[str, Any]]]: ) return True, results - def is_valid_table( + def isvalidtable( self, entries: list[dict[str, str]] | None = None, ) -> tuple[bool, str]: """ Check whether the session table has the correct fields. + MATLAB equivalent: ``ndi.session.sessiontable/isvalidtable`` + Args: entries: Table entries to validate. If None, reads from file. @@ -193,7 +208,7 @@ def is_valid_table( Tuple of (valid, message). Message is empty if valid. """ if entries is None: - entries = self.get_session_table() + entries = self.getsessiontable() for i, entry in enumerate(entries): if "path" not in entry: @@ -210,10 +225,12 @@ def is_valid_table( # Backup # ------------------------------------------------------------------ - def backup(self) -> Path | None: + def backupsessiontable(self) -> Path | None: """ Create a numbered backup of the table file. + MATLAB equivalent: ``ndi.session.sessiontable/backupsessiontable`` + Returns: Path to the backup file, or None if no file to back up. """ @@ -235,8 +252,11 @@ def backup(self) -> Path | None: shutil.copy2(self._table_path, backup_path) return backup_path - def backup_file_list(self) -> list[Path]: - """Return a list of existing backup files.""" + def backupfilelist(self) -> list[Path]: + """Return a list of existing backup files. + + MATLAB equivalent: ``ndi.session.sessiontable/backupfilelist`` + """ if not self._table_path.parent.is_dir(): return [] @@ -249,9 +269,12 @@ def backup_file_list(self) -> list[Path]: # Internal # ------------------------------------------------------------------ - def _write_table(self, entries: list[dict[str, str]]) -> None: - """Write entries to the table file (atomic-ish via parent mkdir).""" - valid, msg = self.is_valid_table(entries) + def _writesessiontable(self, entries: list[dict[str, str]]) -> None: + """Write entries to the table file (atomic-ish via parent mkdir). + + MATLAB equivalent: ``ndi.session.sessiontable/writesessiontable`` (protected) + """ + valid, msg = self.isvalidtable(entries) if not valid: raise ValueError(f"Invalid session table: {msg}") diff --git a/tests/matlab_tests/test_dataset.py b/tests/matlab_tests/test_dataset.py index 484983f..edad3b8 100644 --- a/tests/matlab_tests/test_dataset.py +++ b/tests/matlab_tests/test_dataset.py @@ -208,14 +208,14 @@ def test_session_list_outputs(self, build_dataset): # # NOTE: Python API differences: # - MATLAB: deleteIngestedSession(id, 'areYouSure', true, 'askUserToConfirm', false) -# - Python: delete_ingested_session(id, are_you_sure=True) +# - Python: deleteIngestedSession(id, are_you_sure=True) # - MATLAB raises specific error IDs; Python raises ValueError # - MATLAB raises on nonexistent session; Python returns self silently # =========================================================================== class TestDeleteIngestedSession: - """Test the delete_ingested_session method of Dataset.""" + """Test the deleteIngestedSession method of Dataset.""" def test_delete_success(self, build_dataset): """Delete ingested session and verify it's removed. @@ -235,7 +235,7 @@ def test_delete_success(self, build_dataset): assert len(docs) > 0, "Session documents should exist" # Delete the session - dataset.delete_ingested_session(session_id, are_you_sure=True) + dataset.deleteIngestedSession(session_id, are_you_sure=True) # Verify session is removed from list refs_after, ids_after, *_ = dataset.session_list() @@ -255,7 +255,7 @@ def test_delete_not_confirmed(self, build_dataset): session_id = session.id() with pytest.raises(ValueError): - dataset.delete_ingested_session(session_id, are_you_sure=False) + dataset.deleteIngestedSession(session_id, are_you_sure=False) # Verify session still exists refs, session_ids, *_ = dataset.session_list() @@ -284,7 +284,7 @@ def test_delete_linked_session_error(self, build_dataset, tmp_path): # Attempt to delete a linked session — should fail with pytest.raises(ValueError, match="linked"): - dataset.delete_ingested_session(linked_session.id(), are_you_sure=True) + dataset.deleteIngestedSession(linked_session.id(), are_you_sure=True) def test_delete_nonexistent_session(self, build_dataset): """Deleting a nonexistent session raises ValueError. @@ -294,7 +294,7 @@ def test_delete_nonexistent_session(self, build_dataset): """ dataset, _ = build_dataset with pytest.raises(ValueError, match="not found"): - dataset.delete_ingested_session("fake_id_xyz", are_you_sure=True) + dataset.deleteIngestedSession("fake_id_xyz", are_you_sure=True) # =========================================================================== diff --git a/tests/matlab_tests/test_session.py b/tests/matlab_tests/test_session.py index f913f0f..547727a 100644 --- a/tests/matlab_tests/test_session.py +++ b/tests/matlab_tests/test_session.py @@ -38,7 +38,7 @@ def test_delete_no_confirm(self, tmp_path): assert ndi_dir.exists(), ".ndi directory should exist before delete" # Not confirmed — should not delete - result = session.delete_session_data_structures(are_you_sure=False, ask_user=False) + result = session.deleteSessionDataStructures(are_you_sure=False, ask_user=False) assert ndi_dir.exists(), ".ndi directory should still exist after unconfirmed delete" assert result is not None, "Should return self (not deleted)" @@ -55,7 +55,7 @@ def test_delete_confirm(self, tmp_path): ndi_dir = session_dir / ".ndi" assert ndi_dir.exists(), ".ndi directory should exist before delete" - session.delete_session_data_structures(are_you_sure=True, ask_user=False) + session.deleteSessionDataStructures(are_you_sure=True, ask_user=False) assert not ndi_dir.exists(), ".ndi directory should be gone after confirmed delete" @@ -72,7 +72,7 @@ def test_delete_preserves_data_files(self, tmp_path): data_file = session_dir / "my_data.txt" data_file.write_text("important data") - session.delete_session_data_structures(are_you_sure=True, ask_user=False) + session.deleteSessionDataStructures(are_you_sure=True, ask_user=False) assert data_file.exists(), "User data files should be preserved" assert data_file.read_text() == "important data" diff --git a/tests/test_batch_e.py b/tests/test_batch_e.py index 11bea5c..1879c1b 100644 --- a/tests/test_batch_e.py +++ b/tests/test_batch_e.py @@ -177,139 +177,139 @@ def test_init_custom_path(self, table_dir): assert table._table_path == table_dir def test_empty_table_on_new(self, table): - result = table.get_session_table() + result = table.getsessiontable() assert result == [] def test_add_entry(self, table): - table.add_entry("sess1", "/data/experiment1") - entries = table.get_session_table() + table.addtableentry("sess1", "/data/experiment1") + entries = table.getsessiontable() assert len(entries) == 1 assert entries[0]["session_id"] == "sess1" assert entries[0]["path"] == "/data/experiment1" def test_add_multiple_entries(self, table): - table.add_entry("sess1", "/data/exp1") - table.add_entry("sess2", "/data/exp2") - entries = table.get_session_table() + table.addtableentry("sess1", "/data/exp1") + table.addtableentry("sess2", "/data/exp2") + entries = table.getsessiontable() assert len(entries) == 2 def test_add_replaces_existing(self, table): - table.add_entry("sess1", "/old/path") - table.add_entry("sess1", "/new/path") - entries = table.get_session_table() + table.addtableentry("sess1", "/old/path") + table.addtableentry("sess1", "/new/path") + entries = table.getsessiontable() assert len(entries) == 1 assert entries[0]["path"] == "/new/path" - def test_get_session_path_found(self, table): - table.add_entry("sess1", "/data/exp1") - assert table.get_session_path("sess1") == "/data/exp1" + def test_getsessionpath_found(self, table): + table.addtableentry("sess1", "/data/exp1") + assert table.getsessionpath("sess1") == "/data/exp1" - def test_get_session_path_not_found(self, table): - assert table.get_session_path("nonexistent") is None + def test_getsessionpath_not_found(self, table): + assert table.getsessionpath("nonexistent") is None def test_remove_entry(self, table): - table.add_entry("sess1", "/data/exp1") - table.add_entry("sess2", "/data/exp2") - table.remove_entry("sess1") - entries = table.get_session_table() + table.addtableentry("sess1", "/data/exp1") + table.addtableentry("sess2", "/data/exp2") + table.removetableentry("sess1") + entries = table.getsessiontable() assert len(entries) == 1 assert entries[0]["session_id"] == "sess2" def test_remove_nonexistent_entry(self, table): - table.add_entry("sess1", "/data/exp1") - table.remove_entry("nonexistent") - entries = table.get_session_table() + table.addtableentry("sess1", "/data/exp1") + table.removetableentry("nonexistent") + entries = table.getsessiontable() assert len(entries) == 1 def test_clear(self, table): - table.add_entry("sess1", "/data/exp1") - table.add_entry("sess2", "/data/exp2") - table.clear() - entries = table.get_session_table() + table.addtableentry("sess1", "/data/exp1") + table.addtableentry("sess2", "/data/exp2") + table.clearsessiontable() + entries = table.getsessiontable() assert len(entries) == 0 def test_clear_with_backup(self, table): - table.add_entry("sess1", "/data/exp1") - table.clear(make_backup=True) - entries = table.get_session_table() + table.addtableentry("sess1", "/data/exp1") + table.clearsessiontable(make_backup=True) + entries = table.getsessiontable() assert len(entries) == 0 # Backup file should exist - backups = table.backup_file_list() + backups = table.backupfilelist() assert len(backups) == 1 def test_backup(self, table): - table.add_entry("sess1", "/data/exp1") - backup_path = table.backup() + table.addtableentry("sess1", "/data/exp1") + backup_path = table.backupsessiontable() assert backup_path is not None assert backup_path.exists() assert "_bkup001" in backup_path.name def test_backup_numbered(self, table): - table.add_entry("sess1", "/data/exp1") - table.backup() - table.backup() - backups = table.backup_file_list() + table.addtableentry("sess1", "/data/exp1") + table.backupsessiontable() + table.backupsessiontable() + backups = table.backupfilelist() assert len(backups) == 2 assert "_bkup001" in backups[0].name assert "_bkup002" in backups[1].name def test_backup_no_file(self, table): - result = table.backup() + result = table.backupsessiontable() assert result is None - def test_check_table_valid(self, table, tmp_path): + def test_checktable_valid(self, table, tmp_path): # Use a real existing directory as the path - table.add_entry("sess1", str(tmp_path)) - valid, results = table.check_table() + table.addtableentry("sess1", str(tmp_path)) + valid, results = table.checktable() assert valid is True assert len(results) == 1 assert results[0]["exists"] is True - def test_check_table_nonexistent_path(self, table): - table.add_entry("sess1", "/nonexistent/path") - valid, results = table.check_table() + def test_checktable_nonexistent_path(self, table): + table.addtableentry("sess1", "/nonexistent/path") + valid, results = table.checktable() assert valid is True assert len(results) == 1 assert results[0]["exists"] is False - def test_is_valid_table(self, table): + def test_isvalidtable(self, table): entries = [{"session_id": "a", "path": "/b"}] - valid, msg = table.is_valid_table(entries) + valid, msg = table.isvalidtable(entries) assert valid is True assert msg == "" - def test_is_valid_table_missing_path(self, table): + def test_isvalidtable_missing_path(self, table): entries = [{"session_id": "a"}] - valid, msg = table.is_valid_table(entries) + valid, msg = table.isvalidtable(entries) assert valid is False assert "path" in msg - def test_is_valid_table_missing_session_id(self, table): + def test_isvalidtable_missing_session_id(self, table): entries = [{"path": "/b"}] - valid, msg = table.is_valid_table(entries) + valid, msg = table.isvalidtable(entries) assert valid is False assert "session_id" in msg def test_add_empty_session_id_raises(self, table): with pytest.raises(ValueError, match="session_id"): - table.add_entry("", "/data/exp1") + table.addtableentry("", "/data/exp1") def test_add_empty_path_raises(self, table): with pytest.raises(ValueError, match="path"): - table.add_entry("sess1", "") + table.addtableentry("sess1", "") def test_persistence_across_instances(self, table_dir): t1 = SessionTable(table_path=table_dir) - t1.add_entry("sess1", "/data/exp1") + t1.addtableentry("sess1", "/data/exp1") t2 = SessionTable(table_path=table_dir) - assert t2.get_session_path("sess1") == "/data/exp1" + assert t2.getsessionpath("sess1") == "/data/exp1" def test_repr(self, table): assert "SessionTable" in repr(table) - def test_local_table_filename(self): - path = SessionTable.local_table_filename() + def test_localtablefilename(self): + path = SessionTable.localtablefilename() assert "local_sessiontable" in path.name assert path.suffix == ".txt" diff --git a/tests/test_phase8.py b/tests/test_phase8.py index 01f5f53..165af43 100644 --- a/tests/test_phase8.py +++ b/tests/test_phase8.py @@ -516,22 +516,22 @@ def test_database_rm(self, temp_dir): class TestDatasetIngestion: """Test Dataset ingestion and deletion.""" - def test_delete_ingested_session_requires_confirmation(self, temp_dir, session): + def test_deleteIngestedSession_requires_confirmation(self, temp_dir, session): """Test that deleting ingested session requires confirmation.""" ds = Dataset(temp_dir / "dataset1", "Test") ds.add_ingested_session(session) with pytest.raises(ValueError, match="are_you_sure"): - ds.delete_ingested_session(session.id()) + ds.deleteIngestedSession(session.id()) - def test_delete_ingested_session(self, temp_dir, session): + def test_deleteIngestedSession(self, temp_dir, session): """Test deleting an ingested session.""" ds = Dataset(temp_dir / "dataset1", "Test") ds.add_ingested_session(session) refs, session_ids, *_ = ds.session_list() assert len(session_ids) == 1 - ds.delete_ingested_session(session.id(), are_you_sure=True) + ds.deleteIngestedSession(session.id(), are_you_sure=True) refs, session_ids, *_ = ds.session_list() assert len(session_ids) == 0 @@ -541,7 +541,7 @@ def test_delete_linked_session_raises(self, temp_dir, session): ds.add_linked_session(session) with pytest.raises(ValueError, match="linked"): - ds.delete_ingested_session(session.id(), are_you_sure=True) + ds.deleteIngestedSession(session.id(), are_you_sure=True) # =========================================================================== diff --git a/tests/test_session.py b/tests/test_session.py index d48727d..999c943 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -471,7 +471,7 @@ def test_delete_session_data(self, temp_dir): ndi_dir = temp_dir / ".ndi" assert ndi_dir.exists() - result = session.delete_session_data_structures(are_you_sure=True) + result = session.deleteSessionDataStructures(are_you_sure=True) assert result is None assert not ndi_dir.exists() @@ -480,7 +480,7 @@ def test_delete_session_requires_confirmation(self, temp_dir): session = DirSession("Test", temp_dir) ndi_dir = temp_dir / ".ndi" - result = session.delete_session_data_structures(are_you_sure=False) + result = session.deleteSessionDataStructures(are_you_sure=False) assert result is session assert ndi_dir.exists()