-
Notifications
You must be signed in to change notification settings - Fork 4
Databricks artifact repo #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
e63182b
cb69e26
3af5d3b
38630d3
1cb98e9
870c0aa
10cf458
ecdce6a
116c5d0
11be341
0609a5b
3f1327d
98035b4
0b1af46
3d923cb
d3fab4a
368973d
b67e4dc
5e87cf7
72427a3
f82c607
30e3885
a03e099
50a5e28
8c75590
6d36297
d84fa21
ec85f06
70ca1d7
c6585d6
51ca61f
d1b9dfd
72ecab5
dfc7f60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| syntax = "proto2"; | ||
|
|
||
| package mlflow; | ||
|
|
||
| import "scalapb/scalapb.proto"; | ||
| import "databricks.proto"; | ||
|
|
||
| option java_package = "com.databricks.api.proto.mlflow"; | ||
| option java_generate_equals_and_hash = true; | ||
| option py_generic_services = true; | ||
| option (scalapb.options) = { | ||
| flat_package: true, | ||
| }; | ||
|
|
||
| service DatabricksMlflowArtifactsService { | ||
|
|
||
| // Fetch credentials to read from the specified MLflow artifact location | ||
| // | ||
| // Note: Even if no artifacts exist at the specified artifact location, this API will | ||
| // still provide read credentials as long as the format of the location is valid. | ||
| // Callers must subsequently check for the existence of the artifacts using the appropriate | ||
| // cloud storage APIs (as determined by the `ArtifactCredentialType` property of the response) | ||
| rpc getCredentialsForRead (GetCredentialsForRead) returns (GetCredentialsForRead.Response) { | ||
| option (rpc) = { | ||
| endpoints: [{ | ||
| method: "GET", | ||
| path: "/mlflow/artifacts/credentials-for-read" | ||
| since { major: 2, minor: 0 }, | ||
| }], | ||
| visibility: PUBLIC_UNDOCUMENTED, | ||
| }; | ||
| } | ||
|
|
||
| // Fetch credentials to write to the specified MLflow artifact location | ||
| rpc getCredentialsForWrite (GetCredentialsForWrite) returns (GetCredentialsForWrite.Response) { | ||
| option (rpc) = { | ||
| endpoints: [{ | ||
| method: "GET", | ||
| path: "/mlflow/artifacts/credentials-for-write" | ||
| since { major: 2, minor: 0 }, | ||
| }], | ||
| visibility: PUBLIC_UNDOCUMENTED, | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| // The type of a given artifact access credential | ||
| enum ArtifactCredentialType { | ||
|
|
||
| // The credential is an Azure Shared Access Signature URI. For more information, see | ||
| // https://docs.microsoft.com/en-us/azure/storage/common/storage-sas-overview | ||
| AZURE_SAS_URI = 1; | ||
|
|
||
| // The credential is an AWS Presigned URL. For more information, see | ||
| // https://docs.aws.amazon.com/AmazonS3/latest/dev/ShareObjectPreSignedURL.html | ||
| AWS_PRESIGNED_URL = 2; | ||
|
|
||
| } | ||
|
|
||
| message ArtifactCredentialInfo { | ||
|
|
||
| // The ID of the MLflow Run containing the artifact that can be accessed | ||
| // with the credential | ||
| optional string run_id = 1; | ||
|
|
||
| // The path, relative to the Run's artifact root location, of the artifact | ||
| // that can be accessed with the credential | ||
| optional string path = 2; | ||
|
|
||
| // The signed URI credential that provides access to the artifact | ||
| optional string signed_uri = 3; | ||
|
|
||
| // The type of the signed credential URI (e.g., an AWS presigned URL | ||
| // or an Azure Shared Access Signature URI) | ||
| optional ArtifactCredentialType type = 4; | ||
|
|
||
| } | ||
|
|
||
| message GetCredentialsForRead { | ||
| option (scalapb.message).extends = "com.databricks.rpc.RPC[$this.Response]"; | ||
|
|
||
| // The ID of the MLflow Run for which to fetch artifact read credentials | ||
| optional string run_id = 1; | ||
|
|
||
| // The artifact path, relative to the Run's artifact root location, for which to | ||
| // fetch artifact read credentials | ||
| optional string path = 2; | ||
|
|
||
| message Response { | ||
|
|
||
| // Credentials for reading from the specified artifact location | ||
| optional ArtifactCredentialInfo credentials = 1; | ||
|
|
||
| } | ||
| } | ||
|
|
||
| message GetCredentialsForWrite { | ||
| option (scalapb.message).extends = "com.databricks.rpc.RPC[$this.Response]"; | ||
|
|
||
| // The ID of the MLflow Run for which to fetch artifact write credentials | ||
| optional string run_id = 1; | ||
|
|
||
| // The artifact path, relative to the Run's artifact root location, for which to | ||
| // fetch artifact write credentials | ||
| optional string path = 2; | ||
|
|
||
| message Response { | ||
|
|
||
| // Credentials for writing to the specified artifacts location | ||
| optional ArtifactCredentialInfo credentials = 1; | ||
|
|
||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,94 @@ | ||||||
| from azure.storage.blob import BlobClient | ||||||
|
|
||||||
| import os | ||||||
| from mlflow.exceptions import MlflowException | ||||||
| from mlflow.store.artifact.artifact_repo import ArtifactRepository | ||||||
| from mlflow.utils.string_utils import strip_suffix | ||||||
| from mlflow.utils.file_utils import relative_path_to_artifact_path | ||||||
| from mlflow.utils.rest_utils import call_endpoint, extract_api_info_for_service | ||||||
| from mlflow.protos.databricks_artifacts_pb2 import DatabricksMlflowArtifactsService | ||||||
| from mlflow.protos.databricks_artifacts_pb2 import GetCredentialsForWrite, GetCredentialsForRead | ||||||
| from mlflow.utils.databricks_utils import get_databricks_host_creds | ||||||
| from mlflow.protos.service_pb2 import MlflowService, ListArtifacts | ||||||
|
|
||||||
| _PATH_PREFIX = "/api/2.0" | ||||||
|
|
||||||
|
|
||||||
| class DatabricksArtifactRepository(ArtifactRepository): | ||||||
| """ | ||||||
| SOMETHING : TYPING TILL IT WORKS LOL | ||||||
| """ | ||||||
|
|
||||||
| def __init__(self, artifact_uri): | ||||||
| super(DatabricksArtifactRepository, self).__init__(artifact_uri) | ||||||
|
|
||||||
| def _extract_run_id(self, artifact_uri): | ||||||
| return artifact_uri.lstrip('/').split('/')[4] | ||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @arjundc-db Awesome that we're extracting the run ID from the URI! A quick suggestion here - can we: 1. use the urlparse library to fetch the path component of the URI and 2. normalize the path before performing the splitting? E.g., The reasoning for #1 here is that DBFS URIs support empty hostnames; e.g., Similarly, I've confirmed that The reasoning for #2 here is that URI paths may contain redundant slashes. For example, For more information about the structure of URIs, https://en.wikipedia.org/wiki/Uniform_Resource_Identifier is an awesome reference. Let me know if you have any questions here. These kinds of URI edge cases would be great to handle via a unit test. Let's make sure we plan to add a unit test for this case before merging this functionality!
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once we correct the behavior here, can we also add an inline comment that clearly documents the assumptions we're making about the URI structure? E.g., URIs are assumed to be semantically equivalent to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, this was pretty cool to read about, thanks! |
||||||
|
|
||||||
| def _call_endpoint(self, service, api, json_body): | ||||||
| _METHOD_TO_INFO = extract_api_info_for_service(service, _PATH_PREFIX) | ||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This We can then fetch the info as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
| endpoint, method = _METHOD_TO_INFO[api] | ||||||
| response_proto = api.Response() | ||||||
| return call_endpoint(get_databricks_host_creds(), endpoint, method, json_body, response_proto) | ||||||
|
|
||||||
| def _create_json_body(self, run_id, path=None): | ||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @arjundc-db I think this is unused. Can we remove it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, thanks for the catch. |
||||||
| path = path or '.' | ||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit:
Suggested change
"" is more idiomatic for representing empty than "." There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
| return { | ||||||
| "run_id": run_id, | ||||||
| "path": path | ||||||
| } | ||||||
|
|
||||||
| def _get_azure_write_credentials(self, run_id, path=None): | ||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method calls an MLflow Artifacts Service endpoint that is not directly bound to Azure; it may also return AWS credentials. The type of credential is specified by the Callers of this method should check the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
| return self._call_endpoint(DatabricksMlflowArtifactsService, GetCredentialsForWrite, | ||||||
| self._create_json_body(run_id, path)) | ||||||
|
|
||||||
| def _get_azure_read_credentials(self, run_id, path=None): | ||||||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method calls an MLflow Artifacts Service endpoint that is not directly bound to Azure; it may also return AWS credentials. The type of credential is specified by the Callers of this method should check the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||
| return self._call_endpoint(DatabricksMlflowArtifactsService, GetCredentialsForRead, | ||||||
| self._create_json_body(run_id, path)) | ||||||
|
|
||||||
| def _upload_file(self, local_file, artifact_path): | ||||||
| run_id = self._extract_run_id(self.artifact_uri) | ||||||
| write_credentials = self._get_azure_write_credentials(run_id, artifact_path) | ||||||
| signed_write_uri = write_credentials.credentials.signed_uri | ||||||
| service = BlobClient.from_blob_url(blob_url=signed_write_uri, credential=None) | ||||||
| try: | ||||||
| with open(local_file, "rb") as data: | ||||||
| service.upload_blob(data, overwrite=True) | ||||||
| except Exception as err: | ||||||
| raise MlflowException(err) | ||||||
|
|
||||||
| def log_artifact(self, local_file, artifact_path=None): | ||||||
| self._upload_file(local_file, artifact_path) | ||||||
|
|
||||||
| def log_artifacts(self, local_dir, artifact_path=None): | ||||||
| artifact_path = artifact_path or '' | ||||||
| basename = os.path.basename(strip_suffix(local_dir, '/')) | ||||||
| for (dirpath, _, filenames) in os.walk(local_dir): | ||||||
| artifact_subdir = basename | ||||||
| if dirpath != local_dir: | ||||||
| rel_path = os.path.relpath(dirpath, local_dir) | ||||||
| rel_path = relative_path_to_artifact_path(rel_path) | ||||||
| artifact_subdir = os.path.join(artifact_subdir, rel_path) | ||||||
| for name in filenames: | ||||||
| local_file = os.path.join(dirpath, name) | ||||||
| artifact_location = os.path.join(artifact_path, artifact_subdir) | ||||||
| self._upload_file(local_file, artifact_location) | ||||||
|
|
||||||
| def list_artifacts(self, path=None): | ||||||
| run_id = self._extract_run_id(self.artifact_uri) | ||||||
| return self._call_endpoint(MlflowService, ListArtifacts, self._create_json_body(run_id, path)) | ||||||
|
|
||||||
| def _download_file(self, remote_file_path, local_path): | ||||||
| run_id = self._extract_run_id(self.artifact_uri) | ||||||
| read_credentials = self._get_azure_read_credentials(run_id, remote_file_path) | ||||||
| signed_read_uri = read_credentials.credentials.signed_uri | ||||||
| service = BlobClient.from_blob_url(blob_url=signed_read_uri, credential=None) | ||||||
| try: | ||||||
| with open(local_path, "wb") as output_file: | ||||||
| blob = service.download_blob() | ||||||
| output_file.write(blob.readall()) | ||||||
| except Exception as err: | ||||||
| raise MlflowException(err) | ||||||
|
|
||||||
| def delete_artifacts(self, artifact_path=None): | ||||||
| raise MlflowException('Not implemented yet') | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ | |
|
|
||
| _INVALID_DB_URI_MSG = "Please refer to https://mlflow.org/docs/latest/tracking.html#storage for " \ | ||
| "format specifications." | ||
|
|
||
| _ACLED_ARTIFACT_URI = "dbfs:/databricks/mlflow-tracking/" | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Can we move this into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
|
|
||
| def is_local_uri(uri): | ||
| """Returns true if this is a local file path (/foo or file:/foo).""" | ||
|
|
@@ -129,3 +129,7 @@ def _join_posixpaths_and_append_absolute_suffixes(prefix_path, suffix_path): | |
| # joined path | ||
| suffix_path = suffix_path.lstrip(posixpath.sep) | ||
| return posixpath.join(prefix_path, suffix_path) | ||
|
|
||
|
|
||
| def is_artifact_acled_uri(artifact_uri): | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the same vein as the comment above about semantically equivalent URIs (e.g.,
It would also be great to unit test this method against various semantically equivalent URIs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, however, I do not think step 2 is necessary since https://livegrep.dev.databricks.com/view/mlflow/mlflow/mlflow/store/artifact/artifact_repository_registry.py#L63 will only call
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @arjundc-db I think we're relying on an assumption about exactly where this function is going to be called. Future callers of this function are not guaranteed to perform
I'd vote for either option 1 or option 3! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, that makes sense. Using option 3.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: can we rename this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| return artifact_uri.startswith(_ACLED_ARTIFACT_URI.lstrip('/')) | ||
Uh oh!
There was an error while loading. Please reload this page.