Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions amber/src/main/python/pytexera/storage/dataset_file_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,18 @@ def __init__(self, file_path: str):

:param file_path:
Expected format -
"/datasets/ownerEmail/datasetName/versionName/fileRelativePath"
"/dataset/ownerEmail/datasetName/versionName/fileRelativePath"
Example:
"/datasets/bob@texera.com/twitterDataset/v1/california/tw1.csv"
"/dataset/bob@texera.com/twitterDataset/v1/california/tw1.csv"
"""
parts = file_path.strip("/").split("/")

invalid_format = ValueError(
"Invalid file path format. Expected: "
"/datasets/ownerEmail/datasetName/versionName/fileRelativePath"
"/dataset/ownerEmail/datasetName/versionName/fileRelativePath"
)

# TODO(datasets-prefix): require the prefix once all stored paths are migrated (36.sql) and ml model support work is completed.
# TODO(dataset-prefix): require the prefix once all stored paths are migrated (36.sql) and ml model support work is completed.
if parts and parts[0] in {t.value for t in ResourceType}:
if len(parts) < 5:
raise invalid_format
Expand All @@ -80,7 +80,7 @@ def __init__(self, file_path: str):
self.version_name = parts[3]
self.file_relative_path = "/".join(parts[4:])
elif len(parts) >= 4:
self.resource_type = ResourceType.DATASETS
self.resource_type = ResourceType.DATASET
self.owner_email = parts[0]
self.dataset_name = parts[1]
self.version_name = parts[2]
Expand Down
2 changes: 1 addition & 1 deletion amber/src/main/python/pytexera/storage/resource_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
class ResourceType(str, Enum):
"""The leading segment of a logical file path, identifying the resource kind"""

DATASETS = "datasets"
DATASET = "dataset"
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

package org.apache.texera.web.resource.dashboard

import org.apache.texera.amber.core.storage.ResourceType
import org.apache.texera.dao.SqlServer
import org.apache.texera.web.resource.dashboard.DashboardResource.{
DashboardClickableFileEntry,
Expand All @@ -36,7 +37,7 @@ object SearchQueryBuilder {
val FILE_RESOURCE_TYPE = "file"
val WORKFLOW_RESOURCE_TYPE = "workflow"
val PROJECT_RESOURCE_TYPE = "project"
val DATASET_RESOURCE_TYPE = "dataset"
val DATASET_RESOURCE_TYPE = ResourceType.Dataset.toString
val ALL_RESOURCE_TYPE = ""
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package org.apache.texera.web.resource.dashboard.hub

import com.fasterxml.jackson.annotation.{JsonCreator, JsonValue}
import org.apache.texera.amber.core.storage.ResourceType

/**
* Defines all supported entity types for Hub resources.
Expand All @@ -34,7 +35,7 @@ sealed trait EntityType {

object EntityType {
case object Workflow extends EntityType { val value = "workflow" }
case object Dataset extends EntityType { val value = "dataset" }
case object Dataset extends EntityType { val value: String = ResourceType.Dataset.toString }

private val values = Seq(Workflow, Dataset)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,18 @@ def make_response(status_code: int, body=None, content: bytes = b""):

class TestDatasetFileDocumentInit:
def test_parses_prefixed_path(self, auth_env):
doc = DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
doc = DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")
assert doc.owner_email == "bob@x.com"
assert doc.dataset_name == "ds"
assert doc.version_name == "v1"
assert doc.file_relative_path == "file.csv"

def test_joins_nested_relative_path_back_with_slashes(self, auth_env):
doc = DatasetFileDocument("/datasets/bob@x.com/ds/v1/a/b/c/file.csv")
doc = DatasetFileDocument("/dataset/bob@x.com/ds/v1/a/b/c/file.csv")
assert doc.file_relative_path == "a/b/c/file.csv"

def test_strips_leading_and_trailing_slashes_before_parsing(self, auth_env):
doc = DatasetFileDocument("///datasets/bob@x.com/ds/v1/file.csv///")
doc = DatasetFileDocument("///dataset/bob@x.com/ds/v1/file.csv///")
assert doc.owner_email == "bob@x.com"
assert doc.file_relative_path == "file.csv"

Expand All @@ -80,7 +80,7 @@ def test_unknown_leading_segment_is_read_as_a_legacy_owner(self, auth_env):

def test_rejects_prefixed_path_with_too_few_segments(self, auth_env):
with pytest.raises(ValueError, match="Invalid file path format"):
DatasetFileDocument("/datasets/bob@x.com/ds/v1")
DatasetFileDocument("/dataset/bob@x.com/ds/v1")

def test_rejects_legacy_path_with_too_few_segments(self, auth_env):
with pytest.raises(ValueError, match="Invalid file path format"):
Expand All @@ -92,29 +92,29 @@ def test_requires_jwt_token_in_environment(self, monkeypatch):
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
)
with pytest.raises(ValueError, match="JWT token is required"):
DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")

def test_treats_empty_jwt_as_missing(self, monkeypatch):
# An empty string is falsy and should be rejected just like an unset var.
monkeypatch.setenv("USER_JWT_TOKEN", "")
with pytest.raises(ValueError, match="JWT token is required"):
DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")

def test_falls_back_to_default_endpoint_when_env_missing(self, monkeypatch):
monkeypatch.setenv("USER_JWT_TOKEN", "tok")
monkeypatch.delenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", raising=False
)
doc = DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
doc = DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")
assert doc.presign_endpoint == DEFAULT_ENDPOINT

def test_uses_explicit_endpoint_from_environment(self, auth_env):
doc = DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
doc = DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")
assert doc.presign_endpoint == CUSTOM_ENDPOINT


class TestGetPresignedUrl:
def _make_doc(self, monkeypatch, path="/datasets/bob@x.com/ds/v1/file.csv"):
def _make_doc(self, monkeypatch, path="/dataset/bob@x.com/ds/v1/file.csv"):
monkeypatch.setenv("USER_JWT_TOKEN", "test-jwt-token")
monkeypatch.setenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
Expand Down Expand Up @@ -144,9 +144,7 @@ def test_sends_bearer_authorization_header_with_jwt(self, monkeypatch):
def test_url_encodes_filepath_query_parameter(self, monkeypatch):
# urllib.parse.quote keeps "/" as safe by default, but encodes "@"
# and " " — pin both pieces so the contract is explicit.
doc = self._make_doc(
monkeypatch, path="/datasets/bob@x.com/ds/v1/data file.csv"
)
doc = self._make_doc(monkeypatch, path="/dataset/bob@x.com/ds/v1/data file.csv")
with patch(
"pytexera.storage.dataset_file_document.requests.Session.get"
) as mock_get:
Expand All @@ -158,16 +156,16 @@ def test_url_encodes_filepath_query_parameter(self, monkeypatch):
assert "bob%40x.com" in file_path
assert file_path.startswith("/")

def test_sends_datasets_prefixed_filepath(self, monkeypatch):
# The reconstructed filePath sent to the file-service carries the "datasets" prefix.
doc = self._make_doc(monkeypatch, path="/datasets/bob@x.com/ds/v1/file.csv")
def test_sends_dataset_prefixed_filepath(self, monkeypatch):
# The reconstructed filePath sent to the file-service carries the "dataset" prefix.
doc = self._make_doc(monkeypatch, path="/dataset/bob@x.com/ds/v1/file.csv")
with patch(
"pytexera.storage.dataset_file_document.requests.Session.get"
) as mock_get:
mock_get.return_value = make_response(200, body={"presignedUrl": "u"})
doc.get_presigned_url()
_, kwargs = mock_get.call_args
assert kwargs["params"]["filePath"].startswith("/datasets/")
assert kwargs["params"]["filePath"].startswith("/dataset/")

def test_calls_configured_endpoint(self, monkeypatch):
doc = self._make_doc(monkeypatch)
Expand Down Expand Up @@ -235,7 +233,7 @@ def _make_doc(self, monkeypatch):
monkeypatch.setenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
)
return DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
return DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")

def test_returns_bytesio_with_downloaded_content(self, monkeypatch):
doc = self._make_doc(monkeypatch)
Expand Down Expand Up @@ -291,7 +289,7 @@ def _make_doc(self, monkeypatch):
monkeypatch.setenv(
"FILE_SERVICE_GET_DATASET_PRESIGNED_URL_ENDPOINT", CUSTOM_ENDPOINT
)
return DatasetFileDocument("/datasets/bob@x.com/ds/v1/file.csv")
return DatasetFileDocument("/dataset/bob@x.com/ds/v1/file.csv")

def test_presigned_url_request_passes_request_timeout(self, monkeypatch):
doc = self._make_doc(monkeypatch)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -910,7 +910,7 @@ class WorkflowExecutionsResourceSpec
val content =
"""{
| "operators": [
| {"operatorID": "scanA", "operatorProperties": {"fileName": "/datasets/owner@example.com/LockedDS/v1/data.csv"}},
| {"operatorID": "scanA", "operatorProperties": {"fileName": "/dataset/owner@example.com/LockedDS/v1/data.csv"}},
| {"operatorID": "downstreamB", "operatorProperties": {}}
| ],
| "links": [
Expand Down Expand Up @@ -969,7 +969,7 @@ class WorkflowExecutionsResourceSpec
val content =
"""{
| "operators": [
| {"operatorID": "scan", "operatorProperties": {"fileName": "/datasets/test@example.com/MyDS/v1/data.csv"}}
| {"operatorID": "scan", "operatorProperties": {"fileName": "/dataset/test@example.com/MyDS/v1/data.csv"}}
| ],
| "links": []
|}""".stripMargin
Expand Down Expand Up @@ -1408,7 +1408,7 @@ class WorkflowExecutionsResourceSpec

private def scanOperator(operatorId: String, datasetName: String): String =
s"""{"operatorID": "$operatorId", "operatorProperties": """ +
s"""{"fileName": "/datasets/$foreignOwnerEmail/$datasetName/v1/data.csv"}}"""
s"""{"fileName": "/dataset/$foreignOwnerEmail/$datasetName/v1/data.csv"}}"""

// Seeds three datasets owned by somebody other than testUser and wires the workflow as
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"fileEncoding": "UTF_8",
"customDelimiter": ",",
"hasHeader": true,
"fileName": "/datasets/texera/popular-movies-of-imdb/v1/TMDb_updated.csv",
"fileName": "/dataset/texera/popular-movies-of-imdb/v1/TMDb_updated.csv",
"offset": 0,
"limit": 1000
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"fileEncoding": "UTF_8",
"customDelimiter": ",",
"hasHeader": true,
"fileName": "/datasets/texera/iris-species/v1/Iris.csv"
"fileName": "/dataset/texera/iris-species/v1/Iris.csv"
},
"inputPorts": [],
"outputPorts": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ object FileResolver {
* caller can dispatch to the right backing table.
* - Legacy unprefixed (datasets only, backward compat): /ownerEmail/datasetName/versionName/
* fileRelativePath (>= 4 segments), resolved as a dataset. Models are new and always require
* the /models/ prefix.
* the /model/ prefix.
*
* @param fileName the file path to parse
* @return Some((resourceType, ownerEmail, resourceName, versionName, fileRelativePath)) if valid,
Expand All @@ -115,11 +115,11 @@ object FileResolver {
)
case None =>
// Legacy unprefixed dataset path (backward compat): /ownerEmail/datasetName/versionName/<file>.
// TODO(datasets-prefix): require the prefix once all stored paths are migrated (36.sql).
// TODO(dataset-prefix): require the prefix once all stored paths are migrated (36.sql).
if (pathSegments.length >= 4)
Some(
(
ResourceType.Datasets,
ResourceType.Dataset,
pathSegments(0),
pathSegments(1),
pathSegments(2),
Expand All @@ -139,8 +139,8 @@ object FileResolver {
*
* Input: /<prefix>/ownerEmail/resourceName/versionName/fileRelativePath (or legacy unprefixed)
* Output: {scheme}:///{repositoryName}/{versionHash}/fileRelativePath
* e.g. /datasets/bob@x.com/twitter/v1/dir/f.csv -> dataset:///dataset-15/adeq233td/dir/f.csv
* /models/bob@x.com/resnet/v1/weights/m.pt -> model:///model-15/adeq233td/weights/m.pt
* e.g. /dataset/bob@x.com/twitter/v1/dir/f.csv -> dataset:///dataset-15/adeq233td/dir/f.csv
* /model/bob@x.com/resnet/v1/weights/m.pt -> model:///model-15/adeq233td/weights/m.pt
*
* @throws java.io.FileNotFoundException if the path is not a valid versioned-resource path, the
* resource/version does not exist, or the URI is malformed
Expand All @@ -152,10 +152,10 @@ object FileResolver {
)

val (scheme, repositoryName, versionHash) = resourceType match {
case ResourceType.Datasets =>
case ResourceType.Dataset =>
val (repo, hash) = lookupDataset(ownerEmail, resourceName, versionName, fileName)
(DATASET_FILE_URI_SCHEME, repo, hash)
case ResourceType.Models =>
case ResourceType.Model =>
val (repo, hash) = lookupModel(ownerEmail, resourceName, versionName, fileName)
(MODEL_FILE_URI_SCHEME, repo, hash)
case other =>
Expand Down Expand Up @@ -306,7 +306,7 @@ object FileResolver {
return None
}
parsePrefixedPath(path).collect {
case (ResourceType.Datasets, ownerEmail, datasetName, _, _) => (ownerEmail, datasetName)
case (ResourceType.Dataset, ownerEmail, datasetName, _, _) => (ownerEmail, datasetName)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ package org.apache.texera.amber.core.storage
* Path shape: /<prefix>/ownerEmail/resourceName/versionName/fileRelativePath
*/
object ResourceType extends Enumeration {
val Datasets: Value = Value("datasets")
val Models: Value = Value("models")
val Dataset: Value = Value("dataset")
val Model: Value = Value("model")

/**
* Returns the resource type named by the given path segment, or None if it is not a known
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,13 @@ class FileResolverSpec

private val localCsvFilePath = "common/workflow-core/src/test/resources/country_sales_small.csv"

private val datasetACsvFilePath = "/datasets/test_user@test.com/test_dataset/v2/directory/a.csv"
private val datasetACsvFilePath = "/dataset/test_user@test.com/test_dataset/v2/directory/a.csv"

private val dataset1TxtFilePath = "/datasets/test_user@test.com/test_dataset/v1/1.txt"
private val dataset1TxtFilePath = "/dataset/test_user@test.com/test_dataset/v1/1.txt"

private val modelWeightsFilePath = "/models/test_user@test.com/test_model/v2/weights/model.pt"
private val modelWeightsFilePath = "/model/test_user@test.com/test_model/v2/weights/model.pt"

private val modelReadmeFilePath = "/models/test_user@test.com/test_model/v1/README.md"
private val modelReadmeFilePath = "/model/test_user@test.com/test_model/v1/README.md"

// Legacy unprefixed form — still resolvable as a dataset (backward compat).
private val unprefixedDataset1TxtFilePath = "/test_user@test.com/test_dataset/v1/1.txt"
Expand All @@ -137,11 +137,11 @@ class FileResolverSpec
private val unknownResourceTypeFilePath =
"/notAResourceType/test_user@test.com/test_dataset/v1/1.txt"

// A model name presented under the datasets prefix must not resolve as a dataset.
private val modelNameUnderDatasetPrefix = "/datasets/test_user@test.com/test_model/v1/README.md"
// A model name presented under the dataset prefix must not resolve as a dataset.
private val modelNameUnderDatasetPrefix = "/dataset/test_user@test.com/test_model/v1/README.md"

// A dataset name presented under the models prefix must not resolve as a model.
private val datasetNameUnderModelPrefix = "/models/test_user@test.com/test_dataset/v1/1.txt"
// A dataset name presented under the model prefix must not resolve as a model.
private val datasetNameUnderModelPrefix = "/model/test_user@test.com/test_dataset/v1/1.txt"

override protected def beforeAll(): Unit = {
initializeDBAndReplaceDSLContext()
Expand Down Expand Up @@ -225,7 +225,7 @@ class FileResolverSpec

"FileResolver" should "throw not found exception when a prefixed path has too few segments" in {
assertThrows[FileNotFoundException] {
FileResolver.resolve("/datasets/test_user@test.com/test_dataset")
FileResolver.resolve("/dataset/test_user@test.com/test_dataset")
}
}

Expand Down Expand Up @@ -254,12 +254,12 @@ class FileResolverSpec

"parseDatasetOwnerAndName" should "extract owner email and dataset name from a valid path" in {
assert(
FileResolver.parseDatasetOwnerAndName("/datasets/test_user@test.com/test_dataset/v1/1.txt")
FileResolver.parseDatasetOwnerAndName("/dataset/test_user@test.com/test_dataset/v1/1.txt")
== Some(("test_user@test.com", "test_dataset"))
)
// extra segments beyond the file-relative path are ignored
assert(
FileResolver.parseDatasetOwnerAndName("/datasets/owner@x.com/ds/v2/directory/nested/a.csv")
FileResolver.parseDatasetOwnerAndName("/dataset/owner@x.com/ds/v2/directory/nested/a.csv")
== Some(("owner@x.com", "ds"))
)
}
Expand All @@ -272,7 +272,7 @@ class FileResolverSpec
}

it should "return None when the prefixed path has too few segments" in {
assert(FileResolver.parseDatasetOwnerAndName("/datasets/owner@x.com/ds").isEmpty)
assert(FileResolver.parseDatasetOwnerAndName("/dataset/owner@x.com/ds").isEmpty)
assert(FileResolver.parseDatasetOwnerAndName("owner/dataset").isEmpty)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import org.apache.texera.dao.jooq.generated.tables.User.USER
object FileListerSourceOpExec {

/**
* Parses a dataset version path (/datasets/ownerEmail/datasetName/versionName) into its
* Parses a dataset version path (/dataset/ownerEmail/datasetName/versionName) into its
* (resourceTypePrefix, ownerEmail, datasetName, versionName) components.
*
* @throws IllegalArgumentException if the path is not a well-formed dataset version path
Expand All @@ -42,14 +42,14 @@ object FileListerSourceOpExec {
): (String, String, String, String) = {
val segments = datasetVersionPath.split("/").filter(_.nonEmpty)
val invalidPath = s"Invalid dataset version path '$datasetVersionPath'; " +
"expected /datasets/ownerEmail/datasetName/versionName"
"expected /dataset/ownerEmail/datasetName/versionName"

if (segments.headOption.exists(ResourceType.isValidPrefix)) {
require(segments.length >= 4, invalidPath)
(segments(0), segments(1), segments(2), segments(3))
} else {
require(segments.length >= 3, invalidPath)
(ResourceType.Datasets.toString, segments(0), segments(1), segments(2))
(ResourceType.Dataset.toString, segments(0), segments(1), segments(2))
}
}

Expand Down
Loading
Loading