Add dataproc handler#197
Conversation
Alexander-Stuckey
left a comment
There was a problem hiding this comment.
Some things to think about, looks pretty good to start with
| def upload_to_gcs( | ||
| local_path: str, | ||
| bucket: str, | ||
| prefix: str = '', | ||
| storage_client: storage.Client | None = None, | ||
| ) -> str: | ||
| """Upload a local file to GCS and return the gs:// URI. | ||
|
|
||
| The uploaded blob path is bucket/prefix/basename. Passing an empty prefix | ||
| uploads to the bucket root. | ||
|
|
||
| Args: | ||
| local_path: Path to the file on the local filesystem. | ||
| bucket: Target bucket, either as gs://bucket-name or bucket-name. | ||
| prefix: Blob path prefix within the bucket. | ||
| storage_client: Optional pre-built storage client, mainly for tests. | ||
|
|
||
| Returns: | ||
| The gs:// URI of the uploaded blob. | ||
| """ | ||
| client = storage_client or storage.Client() | ||
| bucket_name = bucket.removeprefix('gs://').rstrip('/') | ||
| basename = os.path.basename(local_path) | ||
| blob_name = f'{prefix.strip("/")}/{basename}' if prefix else basename | ||
| client.bucket(bucket_name).blob(blob_name).upload_from_filename(local_path) | ||
| gcs_uri = f'gs://{bucket_name}/{blob_name}' | ||
| print(f'Uploaded {local_path} to {gcs_uri}') | ||
| return gcs_uri |
There was a problem hiding this comment.
I kinda didn't like this function, so I got Fable to pitch in. Needs a couple of new imports
from cloudpathlib import GSClient, GSPath
def upload_to_gcs(
local_path: str,
bucket: str,
prefix: str = '',
client: GSClient | None = None,
) -> str:
bucket_name = bucket.removeprefix('gs://').rstrip('/')
dest = GSPath(f'gs://{bucket_name}', client=client) / prefix.strip('/') / os.path.basename(local_path)
dest.upload_from(local_path)
return str(dest)
We can then do some finangling to allow upload from either file or memory (these belong in the class itself)
def _dest(self, prefix: str | None, name: str) -> GSPath:
effective = prefix if prefix is not None else f'scripts/{self._name}'
return GSPath(f'gs://{self._staging_bucket_name}', client=self._gs_client) / effective.strip('/') / name
def upload(self, local_path: str, prefix: str | None = None) -> str:
dest = self._dest(prefix, os.path.basename(local_path))
dest.upload_from(local_path)
return str(dest)
def upload_bytes(self, data: bytes, name: str, prefix: str | None = None) -> str:
dest = self._dest(prefix, name)
dest.write_bytes(data)
return str(dest)
There was a problem hiding this comment.
I'm never sure how to feel about this. The google storage client feels like the blessed way of doing this, but it's pretty long-winded... Equally if we lean in on cloudpathlib (like we do elsewhere in cpg_utils/flow) this can be even simpler - e.g. using AnyPath to obfuscate between local and GCP paths. I think I like the idea of being able to upload from memory, but can't quite grok how that would look in practice from this suggestion, could you sketch it out a bit more?
There was a problem hiding this comment.
So cloudpathlib does exactly the same blob.upload_from_filename under the hood, so it's much of the same, but (subjectively) easier to read over?
Regarding the in-memory upload, thinking more about it, it seems that if something like a PED file is needed, that could just be generated inside the Dataproc cluster?
So perhaps we can just ditch the whole 'in memory' part
There was a problem hiding this comment.
Cool, ok. Pushing on with this as the MVP which we can potentially extend later, I've adopted the GSPath/Client version of the method. Will try and get a test run going, see how it behaves
| }, | ||
| ) | ||
| uri_prefix = job.driver_output_resource_uri or None | ||
| except Exception: # noqa: BLE001 - log streamer must not kill the run |
There was a problem hiding this comment.
Do you really want a bare Exception? Surely these is a tighter class that can be used
| Entries whose key does not start with a lowercase letter after sanitisation | ||
| are dropped with a warning to stderr, matching the GCP label constraints. |
There was a problem hiding this comment.
| Entries whose key does not start with a lowercase letter after sanitisation | |
| are dropped with a warning to stderr, matching the GCP label constraints. | |
| Entries whose sanitised key does not start with a lowercase letter causes an error. |
| Intended for CLI arguments. Malformed entries missing an equals sign are | ||
| skipped with a warning to stderr. |
There was a problem hiding this comment.
| Intended for CLI arguments. Malformed entries missing an equals sign are | |
| skipped with a warning to stderr. | |
| Intended to format CLI-passed arguments. Malformed entries missing an equals sign cause an error. |
| """ | ||
| if policy_ref.startswith('projects/'): | ||
| return policy_ref | ||
| if any(value is None for value in [project, region, policy_ref]): |
There was a problem hiding this comment.
| if any(value is None for value in [project, region, policy_ref]): | |
| if not all(project, region, policy_ref): |
Empty strings here would also break, so run an any() truthiness test
Context: for the seqera trial work the latest workflow has a few files chucked in the workflow bin here. One of these is a metamist registration script which was already ported to cpg-utils as #194. Another is a Dataproc manager. This is for the latter.
This takes the single-use Dataproc handler from the test repo, makes it generic, and exposes it as a context manager class. Instead of being dialled into a single use case, with one set of scripts, inputs, params etc., this tries to make those behaviours configurable. It's an alternative to the current
cpg_utils.dataprocmodule which uses hailctl to mediate all the dataproc interactions... That's fine, but we probably don't want to be using Hail to wean ourselves off Hail:hailneeded to be added to PKGS, Hail's own dataproc startup script didn't seem to be installing it in an accessible way?)This does need to be tested. Currently I've got a branch on the go here - I've built a new docker image containing this feature branch and will see how it behaves when replacing the metamist registration/dataproc interaction scripts.
--
Unrelated - it turns out the doctests in this repository were just cosmetic, pytest didn't look for them. The
test/test_doctests.pyfile was doing nothing so that's gone. Instead the doctests are registered in pyproject.toml so pytest knows to look for them.