refactor: upload data unification, less permissions and less endpoints (#31959)
This commit is contained in:
parent
09c1987de4
commit
1b375b715c
|
|
@ -24,6 +24,7 @@ assists people when migrating to a new version.
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
|
- [31959](https://github.com/apache/superset/pull/31959) Removes the following endpoints from data uploads: /api/v1/database/<id>/<file type>_upload and /api/v1/database/<file type>_metadata, in favour of new one (Details on the PR). And simplifies permissions.
|
||||||
- [31844](https://github.com/apache/superset/pull/31844) The `ALERT_REPORTS_EXECUTE_AS` and `THUMBNAILS_EXECUTE_AS` config parameters have been renamed to `ALERT_REPORTS_EXECUTORS` and `THUMBNAILS_EXECUTORS` respectively. A new config flag `CACHE_WARMUP_EXECUTORS` has also been introduced to be able to control which user is used to execute cache warmup tasks. Finally, the config flag `THUMBNAILS_SELENIUM_USER` has been removed. To use a fixed executor for async tasks, use the new `FixedExecutor` class. See the config and docs for more info on setting up different executor profiles.
|
- [31844](https://github.com/apache/superset/pull/31844) The `ALERT_REPORTS_EXECUTE_AS` and `THUMBNAILS_EXECUTE_AS` config parameters have been renamed to `ALERT_REPORTS_EXECUTORS` and `THUMBNAILS_EXECUTORS` respectively. A new config flag `CACHE_WARMUP_EXECUTORS` has also been introduced to be able to control which user is used to execute cache warmup tasks. Finally, the config flag `THUMBNAILS_SELENIUM_USER` has been removed. To use a fixed executor for async tasks, use the new `FixedExecutor` class. See the config and docs for more info on setting up different executor profiles.
|
||||||
- [31894](https://github.com/apache/superset/pull/31894) Domain sharding is deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration will be removed in the next major version (6.0)
|
- [31894](https://github.com/apache/superset/pull/31894) Domain sharding is deprecated in favor of HTTP2. The `SUPERSET_WEBSERVER_DOMAINS` configuration will be removed in the next major version (6.0)
|
||||||
- [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling.
|
- [31774](https://github.com/apache/superset/pull/31774): Fixes the spelling of the `USE-ANALAGOUS-COLORS` feature flag. Please update any scripts/configuration item to use the new/corrected `USE-ANALOGOUS-COLORS` flag spelling.
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,7 @@ import userEvent from '@testing-library/user-event';
|
||||||
import { waitFor } from '@testing-library/react';
|
import { waitFor } from '@testing-library/react';
|
||||||
import { UploadFile } from 'antd/lib/upload/interface';
|
import { UploadFile } from 'antd/lib/upload/interface';
|
||||||
|
|
||||||
fetchMock.post('glob:*api/v1/database/1/csv_upload/', {});
|
fetchMock.post('glob:*api/v1/database/1/upload/', {});
|
||||||
fetchMock.post('glob:*api/v1/database/1/excel_upload/', {});
|
|
||||||
fetchMock.post('glob:*api/v1/database/1/columnar_upload/', {});
|
|
||||||
|
|
||||||
fetchMock.get(
|
fetchMock.get(
|
||||||
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:eq,value:!t)),page:0,page_size:100)',
|
'glob:*api/v1/database/?q=(filters:!((col:allow_file_upload,opr:eq,value:!t)),page:0,page_size:100)',
|
||||||
|
|
@ -643,18 +641,21 @@ test('CSV, form post', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
userEvent.click(uploadButton);
|
userEvent.click(uploadButton);
|
||||||
await waitFor(() => fetchMock.called('glob:*api/v1/database/1/csv_upload/'));
|
await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'));
|
||||||
|
|
||||||
// Get the matching fetch calls made
|
// Get the matching fetch calls made
|
||||||
const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/csv_upload/');
|
const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/upload/');
|
||||||
expect(matchingCalls).toHaveLength(1);
|
expect(matchingCalls).toHaveLength(1);
|
||||||
const [_, options] = matchingCalls[0];
|
const [_, options] = matchingCalls[0];
|
||||||
const formData = options?.body as FormData;
|
const formData = options?.body as FormData;
|
||||||
|
expect(formData.get('type')).toBe('csv');
|
||||||
expect(formData.get('table_name')).toBe('table1');
|
expect(formData.get('table_name')).toBe('table1');
|
||||||
expect(formData.get('schema')).toBe('public');
|
expect(formData.get('schema')).toBe('public');
|
||||||
expect(formData.get('table_name')).toBe('table1');
|
expect(formData.get('table_name')).toBe('table1');
|
||||||
const fileData = formData.get('file') as File;
|
const fileData = formData.get('file') as File;
|
||||||
expect(fileData.name).toBe('test.csv');
|
expect(fileData.name).toBe('test.csv');
|
||||||
|
// Avoid leaking fetchMock calls
|
||||||
|
fetchMock.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Excel, form post', async () => {
|
test('Excel, form post', async () => {
|
||||||
|
|
@ -700,22 +701,21 @@ test('Excel, form post', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
userEvent.click(uploadButton);
|
userEvent.click(uploadButton);
|
||||||
await waitFor(() =>
|
await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'));
|
||||||
fetchMock.called('glob:*api/v1/database/1/excel_upload/'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the matching fetch calls made
|
// Get the matching fetch calls made
|
||||||
const matchingCalls = fetchMock.calls(
|
const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/upload/');
|
||||||
'glob:*api/v1/database/1/excel_upload/',
|
|
||||||
);
|
|
||||||
expect(matchingCalls).toHaveLength(1);
|
expect(matchingCalls).toHaveLength(1);
|
||||||
const [_, options] = matchingCalls[0];
|
const [_, options] = matchingCalls[0];
|
||||||
const formData = options?.body as FormData;
|
const formData = options?.body as FormData;
|
||||||
|
expect(formData.get('type')).toBe('excel');
|
||||||
expect(formData.get('table_name')).toBe('table1');
|
expect(formData.get('table_name')).toBe('table1');
|
||||||
expect(formData.get('schema')).toBe('public');
|
expect(formData.get('schema')).toBe('public');
|
||||||
expect(formData.get('table_name')).toBe('table1');
|
expect(formData.get('table_name')).toBe('table1');
|
||||||
const fileData = formData.get('file') as File;
|
const fileData = formData.get('file') as File;
|
||||||
expect(fileData.name).toBe('test.xls');
|
expect(fileData.name).toBe('test.xls');
|
||||||
|
// Avoid leaking fetchMock calls
|
||||||
|
fetchMock.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Columnar, form post', async () => {
|
test('Columnar, form post', async () => {
|
||||||
|
|
@ -761,22 +761,21 @@ test('Columnar, form post', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
userEvent.click(uploadButton);
|
userEvent.click(uploadButton);
|
||||||
await waitFor(() =>
|
await waitFor(() => fetchMock.called('glob:*api/v1/database/1/upload/'));
|
||||||
fetchMock.called('glob:*api/v1/database/1/columnar_upload/'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the matching fetch calls made
|
// Get the matching fetch calls made
|
||||||
const matchingCalls = fetchMock.calls(
|
const matchingCalls = fetchMock.calls('glob:*api/v1/database/1/upload/');
|
||||||
'glob:*api/v1/database/1/columnar_upload/',
|
|
||||||
);
|
|
||||||
expect(matchingCalls).toHaveLength(1);
|
expect(matchingCalls).toHaveLength(1);
|
||||||
const [_, options] = matchingCalls[0];
|
const [_, options] = matchingCalls[0];
|
||||||
const formData = options?.body as FormData;
|
const formData = options?.body as FormData;
|
||||||
|
expect(formData.get('type')).toBe('columnar');
|
||||||
expect(formData.get('table_name')).toBe('table1');
|
expect(formData.get('table_name')).toBe('table1');
|
||||||
expect(formData.get('schema')).toBe('public');
|
expect(formData.get('schema')).toBe('public');
|
||||||
expect(formData.get('table_name')).toBe('table1');
|
expect(formData.get('table_name')).toBe('table1');
|
||||||
const fileData = formData.get('file') as File;
|
const fileData = formData.get('file') as File;
|
||||||
expect(fileData.name).toBe('test.parquet');
|
expect(fileData.name).toBe('test.parquet');
|
||||||
|
// Avoid leaking fetchMock calls
|
||||||
|
fetchMock.resetHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('CSV, validate file extension returns false', () => {
|
test('CSV, validate file extension returns false', () => {
|
||||||
|
|
|
||||||
|
|
@ -230,19 +230,8 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({
|
||||||
const [previewUploadedFile, setPreviewUploadedFile] = useState<boolean>(true);
|
const [previewUploadedFile, setPreviewUploadedFile] = useState<boolean>(true);
|
||||||
const [fileLoading, setFileLoading] = useState<boolean>(false);
|
const [fileLoading, setFileLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const createTypeToEndpointMap = (
|
const createTypeToEndpointMap = (databaseId: number) =>
|
||||||
databaseId: number,
|
`/api/v1/database/${databaseId}/upload/`;
|
||||||
): { [key: string]: string } => ({
|
|
||||||
csv: `/api/v1/database/${databaseId}/csv_upload/`,
|
|
||||||
excel: `/api/v1/database/${databaseId}/excel_upload/`,
|
|
||||||
columnar: `/api/v1/database/${databaseId}/columnar_upload/`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeToFileMetadataEndpoint = {
|
|
||||||
csv: '/api/v1/database/csv_metadata/',
|
|
||||||
excel: '/api/v1/database/excel_metadata/',
|
|
||||||
columnar: '/api/v1/database/columnar_metadata/',
|
|
||||||
};
|
|
||||||
|
|
||||||
const nullValuesOptions = [
|
const nullValuesOptions = [
|
||||||
{
|
{
|
||||||
|
|
@ -389,9 +378,10 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({
|
||||||
if (type === 'csv') {
|
if (type === 'csv') {
|
||||||
formData.append('delimiter', mergedValues.delimiter);
|
formData.append('delimiter', mergedValues.delimiter);
|
||||||
}
|
}
|
||||||
|
formData.append('type', type);
|
||||||
setFileLoading(true);
|
setFileLoading(true);
|
||||||
return SupersetClient.post({
|
return SupersetClient.post({
|
||||||
endpoint: typeToFileMetadataEndpoint[type],
|
endpoint: '/api/v1/database/upload_metadata/',
|
||||||
body: formData,
|
body: formData,
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
})
|
})
|
||||||
|
|
@ -472,7 +462,8 @@ const UploadDataModal: FunctionComponent<UploadDataModalProps> = ({
|
||||||
}
|
}
|
||||||
appendFormData(formData, mergedValues);
|
appendFormData(formData, mergedValues);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const endpoint = createTypeToEndpointMap(currentDatabaseId)[type];
|
const endpoint = createTypeToEndpointMap(currentDatabaseId);
|
||||||
|
formData.append('type', type);
|
||||||
return SupersetClient.post({
|
return SupersetClient.post({
|
||||||
endpoint,
|
endpoint,
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
|
||||||
|
|
@ -168,8 +168,7 @@ const resetUseSelectorMock = () => {
|
||||||
permissions: {},
|
permissions: {},
|
||||||
roles: {
|
roles: {
|
||||||
Admin: [
|
Admin: [
|
||||||
['can_csv_upload', 'Database'], // So we can upload CSV
|
['can_upload', 'Database'], // So we can upload data (CSV, Excel, Columnar)
|
||||||
['can_excel_upload', 'Database'], // So we can upload CSV
|
|
||||||
['can_write', 'Database'], // So we can write DBs
|
['can_write', 'Database'], // So we can write DBs
|
||||||
['can_write', 'Dataset'], // So we can write Datasets
|
['can_write', 'Dataset'], // So we can write Datasets
|
||||||
['can_write', 'Chart'], // So we can write Datasets
|
['can_write', 'Chart'], // So we can write Datasets
|
||||||
|
|
|
||||||
|
|
@ -507,14 +507,14 @@ export const uploadUserPerms = (
|
||||||
allowedExt: Array<string>,
|
allowedExt: Array<string>,
|
||||||
) => {
|
) => {
|
||||||
const canUploadCSV =
|
const canUploadCSV =
|
||||||
findPermission('can_csv_upload', 'Database', roles) &&
|
findPermission('can_upload', 'Database', roles) &&
|
||||||
checkUploadExtensions(csvExt, allowedExt);
|
checkUploadExtensions(csvExt, allowedExt);
|
||||||
const canUploadColumnar =
|
const canUploadColumnar =
|
||||||
checkUploadExtensions(colExt, allowedExt) &&
|
checkUploadExtensions(colExt, allowedExt) &&
|
||||||
findPermission('can_columnar_upload', 'Database', roles);
|
findPermission('can_upload', 'Database', roles);
|
||||||
const canUploadExcel =
|
const canUploadExcel =
|
||||||
checkUploadExtensions(excelExt, allowedExt) &&
|
checkUploadExtensions(excelExt, allowedExt) &&
|
||||||
findPermission('can_excel_upload', 'Database', roles);
|
findPermission('can_upload', 'Database', roles);
|
||||||
return {
|
return {
|
||||||
canUploadCSV,
|
canUploadCSV,
|
||||||
canUploadColumnar,
|
canUploadColumnar,
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from superset.connectors.sqla.models import SqlaTable
|
||||||
from superset.daos.database import DatabaseDAO
|
from superset.daos.database import DatabaseDAO
|
||||||
from superset.models.core import Database
|
from superset.models.core import Database
|
||||||
from superset.sql_parse import Table
|
from superset.sql_parse import Table
|
||||||
|
from superset.utils.backports import StrEnum
|
||||||
from superset.utils.core import get_user
|
from superset.utils.core import get_user
|
||||||
from superset.utils.decorators import on_error, transaction
|
from superset.utils.decorators import on_error, transaction
|
||||||
from superset.views.database.validators import schema_allows_file_upload
|
from superset.views.database.validators import schema_allows_file_upload
|
||||||
|
|
@ -45,6 +46,12 @@ logger = logging.getLogger(__name__)
|
||||||
READ_CHUNK_SIZE = 1000
|
READ_CHUNK_SIZE = 1000
|
||||||
|
|
||||||
|
|
||||||
|
class UploadFileType(StrEnum):
|
||||||
|
CSV = "csv"
|
||||||
|
EXCEL = "excel"
|
||||||
|
COLUMNAR = "columnar"
|
||||||
|
|
||||||
|
|
||||||
class ReaderOptions(TypedDict, total=False):
|
class ReaderOptions(TypedDict, total=False):
|
||||||
already_exists: str
|
already_exists: str
|
||||||
index_label: str
|
index_label: str
|
||||||
|
|
|
||||||
|
|
@ -169,9 +169,7 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
|
||||||
"delete_object": "write",
|
"delete_object": "write",
|
||||||
"copy_dash": "write",
|
"copy_dash": "write",
|
||||||
"get_connection": "write",
|
"get_connection": "write",
|
||||||
"excel_metadata": "excel_upload",
|
"upload_metadata": "upload",
|
||||||
"columnar_metadata": "columnar_upload",
|
|
||||||
"csv_metadata": "csv_upload",
|
|
||||||
"slack_channels": "write",
|
"slack_channels": "write",
|
||||||
"put_filters": "write",
|
"put_filters": "write",
|
||||||
"put_colors": "write",
|
"put_colors": "write",
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,11 @@ from superset.commands.database.ssh_tunnel.exceptions import (
|
||||||
from superset.commands.database.tables import TablesDatabaseCommand
|
from superset.commands.database.tables import TablesDatabaseCommand
|
||||||
from superset.commands.database.test_connection import TestConnectionDatabaseCommand
|
from superset.commands.database.test_connection import TestConnectionDatabaseCommand
|
||||||
from superset.commands.database.update import UpdateDatabaseCommand
|
from superset.commands.database.update import UpdateDatabaseCommand
|
||||||
from superset.commands.database.uploaders.base import UploadCommand
|
from superset.commands.database.uploaders.base import (
|
||||||
|
BaseDataReader,
|
||||||
|
UploadCommand,
|
||||||
|
UploadFileType,
|
||||||
|
)
|
||||||
from superset.commands.database.uploaders.columnar_reader import ColumnarReader
|
from superset.commands.database.uploaders.columnar_reader import ColumnarReader
|
||||||
from superset.commands.database.uploaders.csv_reader import CSVReader
|
from superset.commands.database.uploaders.csv_reader import CSVReader
|
||||||
from superset.commands.database.uploaders.excel_reader import ExcelReader
|
from superset.commands.database.uploaders.excel_reader import ExcelReader
|
||||||
|
|
@ -72,10 +76,6 @@ from superset.databases.decorators import check_table_access
|
||||||
from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
|
from superset.databases.filters import DatabaseFilter, DatabaseUploadEnabledFilter
|
||||||
from superset.databases.schemas import (
|
from superset.databases.schemas import (
|
||||||
CatalogsResponseSchema,
|
CatalogsResponseSchema,
|
||||||
ColumnarMetadataUploadFilePostSchema,
|
|
||||||
ColumnarUploadPostSchema,
|
|
||||||
CSVMetadataUploadFilePostSchema,
|
|
||||||
CSVUploadPostSchema,
|
|
||||||
database_catalogs_query_schema,
|
database_catalogs_query_schema,
|
||||||
database_schemas_query_schema,
|
database_schemas_query_schema,
|
||||||
database_tables_query_schema,
|
database_tables_query_schema,
|
||||||
|
|
@ -88,8 +88,6 @@ from superset.databases.schemas import (
|
||||||
DatabaseTablesResponse,
|
DatabaseTablesResponse,
|
||||||
DatabaseTestConnectionSchema,
|
DatabaseTestConnectionSchema,
|
||||||
DatabaseValidateParametersSchema,
|
DatabaseValidateParametersSchema,
|
||||||
ExcelMetadataUploadFilePostSchema,
|
|
||||||
ExcelUploadPostSchema,
|
|
||||||
get_export_ids_schema,
|
get_export_ids_schema,
|
||||||
OAuth2ProviderResponseSchema,
|
OAuth2ProviderResponseSchema,
|
||||||
openapi_spec_methods_override,
|
openapi_spec_methods_override,
|
||||||
|
|
@ -99,6 +97,8 @@ from superset.databases.schemas import (
|
||||||
TableExtraMetadataResponseSchema,
|
TableExtraMetadataResponseSchema,
|
||||||
TableMetadataResponseSchema,
|
TableMetadataResponseSchema,
|
||||||
UploadFileMetadata,
|
UploadFileMetadata,
|
||||||
|
UploadFileMetadataPostSchema,
|
||||||
|
UploadPostSchema,
|
||||||
ValidateSQLRequest,
|
ValidateSQLRequest,
|
||||||
ValidateSQLResponse,
|
ValidateSQLResponse,
|
||||||
)
|
)
|
||||||
|
|
@ -162,12 +162,8 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
"delete_ssh_tunnel",
|
"delete_ssh_tunnel",
|
||||||
"schemas_access_for_file_upload",
|
"schemas_access_for_file_upload",
|
||||||
"get_connection",
|
"get_connection",
|
||||||
"csv_upload",
|
"upload_metadata",
|
||||||
"csv_metadata",
|
"upload",
|
||||||
"excel_upload",
|
|
||||||
"excel_metadata",
|
|
||||||
"columnar_upload",
|
|
||||||
"columnar_metadata",
|
|
||||||
"oauth2",
|
"oauth2",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -282,8 +278,6 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
openapi_spec_tag = "Database"
|
openapi_spec_tag = "Database"
|
||||||
openapi_spec_component_schemas = (
|
openapi_spec_component_schemas = (
|
||||||
CatalogsResponseSchema,
|
CatalogsResponseSchema,
|
||||||
ColumnarUploadPostSchema,
|
|
||||||
CSVUploadPostSchema,
|
|
||||||
DatabaseConnectionSchema,
|
DatabaseConnectionSchema,
|
||||||
DatabaseFunctionNamesResponse,
|
DatabaseFunctionNamesResponse,
|
||||||
DatabaseSchemaAccessForFileUploadResponse,
|
DatabaseSchemaAccessForFileUploadResponse,
|
||||||
|
|
@ -291,15 +285,13 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
DatabaseTablesResponse,
|
DatabaseTablesResponse,
|
||||||
DatabaseTestConnectionSchema,
|
DatabaseTestConnectionSchema,
|
||||||
DatabaseValidateParametersSchema,
|
DatabaseValidateParametersSchema,
|
||||||
ExcelUploadPostSchema,
|
|
||||||
TableExtraMetadataResponseSchema,
|
TableExtraMetadataResponseSchema,
|
||||||
TableMetadataResponseSchema,
|
TableMetadataResponseSchema,
|
||||||
SelectStarResponseSchema,
|
SelectStarResponseSchema,
|
||||||
SchemasResponseSchema,
|
SchemasResponseSchema,
|
||||||
CSVMetadataUploadFilePostSchema,
|
UploadFileMetadataPostSchema,
|
||||||
ExcelMetadataUploadFilePostSchema,
|
|
||||||
ColumnarMetadataUploadFilePostSchema,
|
|
||||||
UploadFileMetadata,
|
UploadFileMetadata,
|
||||||
|
UploadPostSchema,
|
||||||
ValidateSQLRequest,
|
ValidateSQLRequest,
|
||||||
ValidateSQLResponse,
|
ValidateSQLResponse,
|
||||||
)
|
)
|
||||||
|
|
@ -1621,30 +1613,31 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
command.run()
|
command.run()
|
||||||
return self.response(200, message="OK")
|
return self.response(200, message="OK")
|
||||||
|
|
||||||
@expose("/csv_metadata/", methods=("POST",))
|
@expose("/upload_metadata/", methods=("POST",))
|
||||||
@protect()
|
@protect()
|
||||||
@statsd_metrics
|
@statsd_metrics
|
||||||
@event_logger.log_this_with_context(
|
@event_logger.log_this_with_context(
|
||||||
action=(
|
action=(
|
||||||
lambda self, *args, **kwargs: f"{self.__class__.__name__}" ".csv_metadata"
|
lambda self, *args, **kwargs: f"{self.__class__.__name__}"
|
||||||
|
".upload_metadata"
|
||||||
),
|
),
|
||||||
log_to_statsd=False,
|
log_to_statsd=False,
|
||||||
)
|
)
|
||||||
@requires_form_data
|
@requires_form_data
|
||||||
def csv_metadata(self) -> Response:
|
def upload_metadata(self) -> Response:
|
||||||
"""Upload a CSV file and returns file metadata.
|
"""Upload a file and returns file metadata.
|
||||||
---
|
---
|
||||||
post:
|
post:
|
||||||
summary: Upload a CSV file and returns file metadata
|
summary: Upload a file and returns file metadata
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
multipart/form-data:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/CSVMetadataUploadFilePostSchema'
|
$ref: '#/components/schemas/UploadFileMetadataPostSchema'
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Columnar upload response
|
description: Upload response
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
|
|
@ -1664,25 +1657,32 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
try:
|
try:
|
||||||
request_form = request.form.to_dict()
|
request_form = request.form.to_dict()
|
||||||
request_form["file"] = request.files.get("file")
|
request_form["file"] = request.files.get("file")
|
||||||
parameters = CSVMetadataUploadFilePostSchema().load(request_form)
|
parameters = UploadFileMetadataPostSchema().load(request_form)
|
||||||
except ValidationError as error:
|
except ValidationError as error:
|
||||||
return self.response_400(message=error.messages)
|
return self.response_400(message=error.messages)
|
||||||
metadata = CSVReader(parameters).file_metadata(parameters["file"])
|
if parameters["type"] == UploadFileType.CSV.value:
|
||||||
|
metadata = CSVReader(parameters).file_metadata(parameters["file"])
|
||||||
|
elif parameters["type"] == UploadFileType.EXCEL.value:
|
||||||
|
metadata = ExcelReader(parameters).file_metadata(parameters["file"])
|
||||||
|
elif parameters["type"] == UploadFileType.COLUMNAR.value:
|
||||||
|
metadata = ColumnarReader(parameters).file_metadata(parameters["file"])
|
||||||
|
else:
|
||||||
|
self.response_400(message="Unexpected Invalid file type")
|
||||||
return self.response(200, result=UploadFileMetadata().dump(metadata))
|
return self.response(200, result=UploadFileMetadata().dump(metadata))
|
||||||
|
|
||||||
@expose("/<int:pk>/csv_upload/", methods=("POST",))
|
@expose("/<int:pk>/upload/", methods=("POST",))
|
||||||
@protect()
|
@protect()
|
||||||
@statsd_metrics
|
@statsd_metrics
|
||||||
@event_logger.log_this_with_context(
|
@event_logger.log_this_with_context(
|
||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.csv_upload",
|
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.upload",
|
||||||
log_to_statsd=False,
|
log_to_statsd=False,
|
||||||
)
|
)
|
||||||
@requires_form_data
|
@requires_form_data
|
||||||
def csv_upload(self, pk: int) -> Response:
|
def upload(self, pk: int) -> Response:
|
||||||
"""Upload a CSV file into a database.
|
"""Upload a file into a database.
|
||||||
---
|
---
|
||||||
post:
|
post:
|
||||||
summary: Upload a CSV file to a database table
|
summary: Upload a file to a database table
|
||||||
parameters:
|
parameters:
|
||||||
- in: path
|
- in: path
|
||||||
schema:
|
schema:
|
||||||
|
|
@ -1693,7 +1693,7 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
content:
|
content:
|
||||||
multipart/form-data:
|
multipart/form-data:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/CSVUploadPostSchema'
|
$ref: '#/components/schemas/UploadPostSchema'
|
||||||
responses:
|
responses:
|
||||||
201:
|
201:
|
||||||
description: CSV upload response
|
description: CSV upload response
|
||||||
|
|
@ -1718,232 +1718,22 @@ class DatabaseRestApi(BaseSupersetModelRestApi):
|
||||||
try:
|
try:
|
||||||
request_form = request.form.to_dict()
|
request_form = request.form.to_dict()
|
||||||
request_form["file"] = request.files.get("file")
|
request_form["file"] = request.files.get("file")
|
||||||
parameters = CSVUploadPostSchema().load(request_form)
|
parameters = UploadPostSchema().load(request_form)
|
||||||
|
reader: BaseDataReader
|
||||||
|
if parameters["type"] == UploadFileType.CSV.value:
|
||||||
|
reader = CSVReader(parameters)
|
||||||
|
elif parameters["type"] == UploadFileType.EXCEL.value:
|
||||||
|
reader = ExcelReader(parameters)
|
||||||
|
elif parameters["type"] == UploadFileType.COLUMNAR.value:
|
||||||
|
reader = ColumnarReader(parameters)
|
||||||
|
else:
|
||||||
|
return self.response_400(message="Unexpected Invalid file type")
|
||||||
UploadCommand(
|
UploadCommand(
|
||||||
pk,
|
pk,
|
||||||
parameters["table_name"],
|
parameters["table_name"],
|
||||||
parameters["file"],
|
parameters["file"],
|
||||||
parameters.get("schema"),
|
parameters.get("schema"),
|
||||||
CSVReader(parameters),
|
reader,
|
||||||
).run()
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
return self.response(201, message="OK")
|
|
||||||
|
|
||||||
@expose("/excel_metadata/", methods=("POST",))
|
|
||||||
@protect()
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=(
|
|
||||||
lambda self, *args, **kwargs: f"{self.__class__.__name__}" ".excel_metadata"
|
|
||||||
),
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
@requires_form_data
|
|
||||||
def excel_metadata(self) -> Response:
|
|
||||||
"""Upload an Excel file and returns file metadata.
|
|
||||||
---
|
|
||||||
post:
|
|
||||||
summary: Upload an Excel file and returns file metadata
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ExcelMetadataUploadFilePostSchema'
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Columnar upload response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
result:
|
|
||||||
$ref: '#/components/schemas/UploadFileMetadata'
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
request_form = request.form.to_dict()
|
|
||||||
request_form["file"] = request.files.get("file")
|
|
||||||
parameters = ExcelMetadataUploadFilePostSchema().load(request_form)
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
metadata = ExcelReader().file_metadata(parameters["file"])
|
|
||||||
return self.response(200, result=UploadFileMetadata().dump(metadata))
|
|
||||||
|
|
||||||
@expose("/<int:pk>/excel_upload/", methods=("POST",))
|
|
||||||
@protect()
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.excel_upload",
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
@requires_form_data
|
|
||||||
def excel_upload(self, pk: int) -> Response:
|
|
||||||
"""Upload an Excel file into a database.
|
|
||||||
---
|
|
||||||
post:
|
|
||||||
summary: Upload an Excel file to a database table
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: pk
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ExcelUploadPostSchema'
|
|
||||||
responses:
|
|
||||||
201:
|
|
||||||
description: Excel upload response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
422:
|
|
||||||
$ref: '#/components/responses/422'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
request_form = request.form.to_dict()
|
|
||||||
request_form["file"] = request.files.get("file")
|
|
||||||
parameters = ExcelUploadPostSchema().load(request_form)
|
|
||||||
UploadCommand(
|
|
||||||
pk,
|
|
||||||
parameters["table_name"],
|
|
||||||
parameters["file"],
|
|
||||||
parameters.get("schema"),
|
|
||||||
ExcelReader(parameters),
|
|
||||||
).run()
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
return self.response(201, message="OK")
|
|
||||||
|
|
||||||
@expose("/columnar_metadata/", methods=("POST",))
|
|
||||||
@protect()
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}"
|
|
||||||
".columnar_metadata",
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
@requires_form_data
|
|
||||||
def columnar_metadata(self) -> Response:
|
|
||||||
"""Upload a Columnar file and returns file metadata.
|
|
||||||
---
|
|
||||||
post:
|
|
||||||
summary: Upload a Columnar file and returns file metadata
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ColumnarMetadataUploadFilePostSchema'
|
|
||||||
responses:
|
|
||||||
200:
|
|
||||||
description: Columnar upload response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
result:
|
|
||||||
$ref: '#/components/schemas/UploadFileMetadata'
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
request_form = request.form.to_dict()
|
|
||||||
request_form["file"] = request.files.get("file")
|
|
||||||
parameters = ColumnarMetadataUploadFilePostSchema().load(request_form)
|
|
||||||
except ValidationError as error:
|
|
||||||
return self.response_400(message=error.messages)
|
|
||||||
metadata = ColumnarReader().file_metadata(parameters["file"])
|
|
||||||
return self.response(200, result=UploadFileMetadata().dump(metadata))
|
|
||||||
|
|
||||||
@expose("/<int:pk>/columnar_upload/", methods=("POST",))
|
|
||||||
@protect()
|
|
||||||
@statsd_metrics
|
|
||||||
@event_logger.log_this_with_context(
|
|
||||||
action=lambda self,
|
|
||||||
*args,
|
|
||||||
**kwargs: f"{self.__class__.__name__}.columnar_upload",
|
|
||||||
log_to_statsd=False,
|
|
||||||
)
|
|
||||||
@requires_form_data
|
|
||||||
def columnar_upload(self, pk: int) -> Response:
|
|
||||||
"""Upload a Columnar file into a database.
|
|
||||||
---
|
|
||||||
post:
|
|
||||||
summary: Upload a Columnar file to a database table
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
schema:
|
|
||||||
type: integer
|
|
||||||
name: pk
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
multipart/form-data:
|
|
||||||
schema:
|
|
||||||
$ref: '#/components/schemas/ColumnarUploadPostSchema'
|
|
||||||
responses:
|
|
||||||
201:
|
|
||||||
description: Columnar upload response
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
400:
|
|
||||||
$ref: '#/components/responses/400'
|
|
||||||
401:
|
|
||||||
$ref: '#/components/responses/401'
|
|
||||||
404:
|
|
||||||
$ref: '#/components/responses/404'
|
|
||||||
422:
|
|
||||||
$ref: '#/components/responses/422'
|
|
||||||
500:
|
|
||||||
$ref: '#/components/responses/500'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
request_form = request.form.to_dict()
|
|
||||||
request_form["file"] = request.files.get("file")
|
|
||||||
parameters = ColumnarUploadPostSchema().load(request_form)
|
|
||||||
UploadCommand(
|
|
||||||
pk,
|
|
||||||
parameters["table_name"],
|
|
||||||
parameters["file"],
|
|
||||||
parameters.get("schema"),
|
|
||||||
ColumnarReader(parameters),
|
|
||||||
).run()
|
).run()
|
||||||
except ValidationError as error:
|
except ValidationError as error:
|
||||||
return self.response_400(message=error.messages)
|
return self.response_400(message=error.messages)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ from superset.commands.database.ssh_tunnel.exceptions import (
|
||||||
SSHTunnelInvalidCredentials,
|
SSHTunnelInvalidCredentials,
|
||||||
SSHTunnelMissingCredentials,
|
SSHTunnelMissingCredentials,
|
||||||
)
|
)
|
||||||
|
from superset.commands.database.uploaders.base import UploadFileType
|
||||||
from superset.constants import PASSWORD_MASK
|
from superset.constants import PASSWORD_MASK
|
||||||
from superset.databases.types import ( # pylint:disable=unused-import
|
from superset.databases.types import ( # pylint:disable=unused-import
|
||||||
EncryptedDict, # noqa: F401
|
EncryptedDict, # noqa: F401
|
||||||
|
|
@ -1081,20 +1082,22 @@ class DelimitedListField(fields.List):
|
||||||
) from exc
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
class BaseUploadFilePostSchema(Schema):
|
class BaseUploadFilePostSchemaMixin(Schema):
|
||||||
_extension_config_key = ""
|
|
||||||
|
|
||||||
@validates("file")
|
@validates("file")
|
||||||
def validate_file_extension(self, file: FileStorage) -> None:
|
def validate_file_extension(self, file: FileStorage) -> None:
|
||||||
allowed_extensions = current_app.config["ALLOWED_EXTENSIONS"].intersection(
|
allowed_extensions = current_app.config["ALLOWED_EXTENSIONS"]
|
||||||
current_app.config[self._extension_config_key]
|
|
||||||
)
|
|
||||||
file_suffix = Path(file.filename).suffix
|
file_suffix = Path(file.filename).suffix
|
||||||
if not file_suffix or file_suffix[1:] not in allowed_extensions:
|
if not file_suffix or file_suffix[1:] not in allowed_extensions:
|
||||||
raise ValidationError([_("File extension is not allowed.")])
|
raise ValidationError([_("File extension is not allowed.")])
|
||||||
|
|
||||||
|
|
||||||
class BaseUploadPostSchema(BaseUploadFilePostSchema):
|
class UploadPostSchema(BaseUploadFilePostSchemaMixin):
|
||||||
|
type = fields.Enum(
|
||||||
|
UploadFileType,
|
||||||
|
required=True,
|
||||||
|
by_value=True,
|
||||||
|
metadata={"description": "File type to upload"},
|
||||||
|
)
|
||||||
already_exists = fields.String(
|
already_exists = fields.String(
|
||||||
load_default="fail",
|
load_default="fail",
|
||||||
validate=OneOf(choices=("fail", "replace", "append")),
|
validate=OneOf(choices=("fail", "replace", "append")),
|
||||||
|
|
@ -1123,43 +1126,26 @@ class BaseUploadPostSchema(BaseUploadFilePostSchema):
|
||||||
metadata={"description": "The name of the table to be created/appended"},
|
metadata={"description": "The name of the table to be created/appended"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ------------
|
||||||
class ColumnarUploadPostSchema(BaseUploadPostSchema):
|
# CSV Schema
|
||||||
"""
|
# ------------
|
||||||
Schema for Columnar Upload
|
|
||||||
"""
|
|
||||||
|
|
||||||
_extension_config_key = "COLUMNAR_EXTENSIONS"
|
|
||||||
|
|
||||||
file = fields.Raw(
|
file = fields.Raw(
|
||||||
required=True,
|
required=True,
|
||||||
metadata={
|
metadata={
|
||||||
"description": "The Columnar file to upload",
|
"description": "The file to upload",
|
||||||
"type": "string",
|
|
||||||
"format": "binary",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CSVUploadPostSchema(BaseUploadPostSchema):
|
|
||||||
"""
|
|
||||||
Schema for CSV Upload
|
|
||||||
"""
|
|
||||||
|
|
||||||
_extension_config_key = "CSV_EXTENSIONS"
|
|
||||||
|
|
||||||
file = fields.Raw(
|
|
||||||
required=True,
|
|
||||||
metadata={
|
|
||||||
"description": "The CSV file to upload",
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "text/csv",
|
"format": "text/csv",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
delimiter = fields.String(metadata={"description": "The delimiter of the CSV file"})
|
delimiter = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "[CSV only] The character used to separate values in the CSV"
|
||||||
|
" file (e.g., a comma, semicolon, or tab)."
|
||||||
|
}
|
||||||
|
)
|
||||||
column_data_types = fields.String(
|
column_data_types = fields.String(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "A dictionary with column names and "
|
"description": "[CSV only] A dictionary with column names and "
|
||||||
"their data types if you need to change "
|
"their data types if you need to change "
|
||||||
"the defaults. Example: {'user_id':'int'}. "
|
"the defaults. Example: {'user_id':'int'}. "
|
||||||
"Check Python Pandas library for supported data types"
|
"Check Python Pandas library for supported data types"
|
||||||
|
|
@ -1167,57 +1153,69 @@ class CSVUploadPostSchema(BaseUploadPostSchema):
|
||||||
)
|
)
|
||||||
day_first = fields.Boolean(
|
day_first = fields.Boolean(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "DD/MM format dates, international and European format"
|
"description": "[CSV only] DD/MM format dates, international and European"
|
||||||
|
" format"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
skip_blank_lines = fields.Boolean(
|
skip_blank_lines = fields.Boolean(
|
||||||
metadata={"description": "Skip blank lines in the CSV file."}
|
metadata={"description": "[CSV only] Skip blank lines in the CSV file."}
|
||||||
)
|
)
|
||||||
skip_initial_space = fields.Boolean(
|
skip_initial_space = fields.Boolean(
|
||||||
metadata={"description": "Skip spaces after delimiter."}
|
metadata={"description": "[CSV only] Skip spaces after delimiter."}
|
||||||
)
|
)
|
||||||
column_dates = DelimitedListField(
|
column_dates = DelimitedListField(
|
||||||
fields.String(),
|
fields.String(),
|
||||||
metadata={
|
metadata={
|
||||||
"description": "A list of column names that should be "
|
"description": "[CSV and Excel only] A list of column names that should be "
|
||||||
"parsed as dates. Example: date,timestamp"
|
"parsed as dates. Example: date,timestamp"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
decimal_character = fields.String(
|
decimal_character = fields.String(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "Character to recognize as decimal point. Default is '.'"
|
"description": "[CSV and Excel only] Character to recognize as decimal"
|
||||||
|
" point. Default is '.'"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
header_row = fields.Integer(
|
header_row = fields.Integer(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "Row containing the headers to use as column names"
|
"description": "[CSV and Excel only] Row containing the headers to use as"
|
||||||
"(0 is first line of data). Leave empty if there is no header row."
|
" column names (0 is first line of data). Leave empty if"
|
||||||
|
" there is no header row."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
index_column = fields.String(
|
index_column = fields.String(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "Column to use as the row labels of the dataframe. "
|
"description": "[CSV and Excel only] Column to use as the row labels of the"
|
||||||
"Leave empty if no index column"
|
" dataframe. Leave empty if no index column"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
null_values = DelimitedListField(
|
null_values = DelimitedListField(
|
||||||
fields.String(),
|
fields.String(),
|
||||||
metadata={
|
metadata={
|
||||||
"description": "A list of strings that should be treated as null. "
|
"description": "[CSV and Excel only] A list of strings that should be "
|
||||||
"Examples: '' for empty strings, 'None', 'N/A',"
|
"treated as null. Examples: '' for empty strings, 'None',"
|
||||||
"Warning: Hive database supports only a single value"
|
" 'N/A', Warning: Hive database supports only a single value"
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
rows_to_read = fields.Integer(
|
rows_to_read = fields.Integer(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "Number of rows to read from the file. "
|
"description": "[CSV and Excel only] Number of rows to read from the file. "
|
||||||
"If None, reads all rows."
|
"If None, reads all rows."
|
||||||
},
|
},
|
||||||
allow_none=True,
|
allow_none=True,
|
||||||
validate=Range(min=1),
|
validate=Range(min=1),
|
||||||
)
|
)
|
||||||
skip_rows = fields.Integer(
|
skip_rows = fields.Integer(
|
||||||
metadata={"description": "Number of rows to skip at start of file."}
|
metadata={
|
||||||
|
"description": "[CSV and Excel only] Number of rows to skip at start"
|
||||||
|
" of file."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
sheet_name = fields.String(
|
||||||
|
metadata={
|
||||||
|
"description": "[Excel only]] Strings used for sheet names "
|
||||||
|
"(default is the first sheet)."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@post_load
|
@post_load
|
||||||
|
|
@ -1234,79 +1232,17 @@ class CSVUploadPostSchema(BaseUploadPostSchema):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ExcelUploadPostSchema(BaseUploadPostSchema):
|
class UploadFileMetadataPostSchema(BaseUploadFilePostSchemaMixin):
|
||||||
"""
|
"""
|
||||||
Schema for Excel Upload
|
Schema for Upload file metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_extension_config_key = "EXCEL_EXTENSIONS"
|
type = fields.Enum(
|
||||||
|
UploadFileType,
|
||||||
file = fields.Raw(
|
|
||||||
required=True,
|
required=True,
|
||||||
metadata={
|
by_value=True,
|
||||||
"description": "The Excel file to upload",
|
metadata={"description": "File type to upload"},
|
||||||
"type": "string",
|
|
||||||
"format": "binary",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
sheet_name = fields.String(
|
|
||||||
metadata={
|
|
||||||
"description": "Strings used for sheet names "
|
|
||||||
"(default is the first sheet)."
|
|
||||||
}
|
|
||||||
)
|
|
||||||
column_dates = DelimitedListField(
|
|
||||||
fields.String(),
|
|
||||||
metadata={
|
|
||||||
"description": "A list of column names that should be "
|
|
||||||
"parsed as dates. Example: date,timestamp"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
decimal_character = fields.String(
|
|
||||||
metadata={
|
|
||||||
"description": "Character to recognize as decimal point. Default is '.'"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
header_row = fields.Integer(
|
|
||||||
metadata={
|
|
||||||
"description": "Row containing the headers to use as column names"
|
|
||||||
"(0 is first line of data). Leave empty if there is no header row."
|
|
||||||
}
|
|
||||||
)
|
|
||||||
index_column = fields.String(
|
|
||||||
metadata={
|
|
||||||
"description": "Column to use as the row labels of the dataframe. "
|
|
||||||
"Leave empty if no index column"
|
|
||||||
}
|
|
||||||
)
|
|
||||||
null_values = DelimitedListField(
|
|
||||||
fields.String(),
|
|
||||||
metadata={
|
|
||||||
"description": "A list of strings that should be treated as null. "
|
|
||||||
"Examples: '' for empty strings, 'None', 'N/A',"
|
|
||||||
"Warning: Hive database supports only a single value"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
rows_to_read = fields.Integer(
|
|
||||||
metadata={
|
|
||||||
"description": "Number of rows to read from the file. "
|
|
||||||
"If None, reads all rows."
|
|
||||||
},
|
|
||||||
allow_none=True,
|
|
||||||
validate=Range(min=1),
|
|
||||||
)
|
|
||||||
skip_rows = fields.Integer(
|
|
||||||
metadata={"description": "Number of rows to skip at start of file."}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CSVMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
|
|
||||||
"""
|
|
||||||
Schema for CSV metadata.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_extension_config_key = "CSV_EXTENSIONS"
|
|
||||||
|
|
||||||
file = fields.Raw(
|
file = fields.Raw(
|
||||||
required=True,
|
required=True,
|
||||||
metadata={
|
metadata={
|
||||||
|
|
@ -1315,30 +1251,12 @@ class CSVMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
|
||||||
"format": "binary",
|
"format": "binary",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
delimiter = fields.String(metadata={"description": "The delimiter of the CSV file"})
|
delimiter = fields.String(
|
||||||
header_row = fields.Integer(
|
|
||||||
metadata={
|
metadata={
|
||||||
"description": "Row containing the headers to use as column names"
|
"description": "The character used to separate values in the CSV file"
|
||||||
"(0 is first line of data). Leave empty if there is no header row."
|
" (e.g., a comma, semicolon, or tab)."
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExcelMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
|
|
||||||
"""
|
|
||||||
Schema for CSV metadata.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_extension_config_key = "EXCEL_EXTENSIONS"
|
|
||||||
|
|
||||||
file = fields.Raw(
|
|
||||||
required=True,
|
|
||||||
metadata={
|
|
||||||
"description": "The file to upload",
|
|
||||||
"type": "string",
|
|
||||||
"format": "binary",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
header_row = fields.Integer(
|
header_row = fields.Integer(
|
||||||
metadata={
|
metadata={
|
||||||
"description": "Row containing the headers to use as column names"
|
"description": "Row containing the headers to use as column names"
|
||||||
|
|
@ -1347,23 +1265,6 @@ class ExcelMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ColumnarMetadataUploadFilePostSchema(BaseUploadFilePostSchema):
|
|
||||||
"""
|
|
||||||
Schema for CSV metadata.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_extension_config_key = "COLUMNAR_EXTENSIONS"
|
|
||||||
|
|
||||||
file = fields.Raw(
|
|
||||||
required=True,
|
|
||||||
metadata={
|
|
||||||
"description": "The file to upload",
|
|
||||||
"type": "string",
|
|
||||||
"format": "binary",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UploadFileMetadataItemSchema(Schema):
|
class UploadFileMetadataItemSchema(Schema):
|
||||||
sheet_name = fields.String(metadata={"description": "The name of the sheet"})
|
sheet_name = fields.String(metadata={"description": "The name of the sheet"})
|
||||||
column_names = fields.List(
|
column_names = fields.List(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
"""converge_upload_permissions
|
||||||
|
|
||||||
|
Revision ID: 74ad1125881c
|
||||||
|
Revises: d482d51c15ca
|
||||||
|
Create Date: 2025-01-22 14:34:25.610084
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = "74ad1125881c"
|
||||||
|
down_revision = "d482d51c15ca"
|
||||||
|
|
||||||
|
from alembic import op # noqa: E402
|
||||||
|
from sqlalchemy.exc import SQLAlchemyError # noqa: E402
|
||||||
|
from sqlalchemy.orm import Session # noqa: E402
|
||||||
|
|
||||||
|
from superset.migrations.shared.security_converge import ( # noqa: E402
|
||||||
|
add_pvms,
|
||||||
|
get_reversed_new_pvms,
|
||||||
|
get_reversed_pvm_map,
|
||||||
|
migrate_roles,
|
||||||
|
Pvm,
|
||||||
|
)
|
||||||
|
|
||||||
|
NEW_PVMS = {"Database": ("can_upload",)}
|
||||||
|
|
||||||
|
PVM_MAP = {
|
||||||
|
Pvm("Database", "can_csv_upload"): (Pvm("Database", "can_upload"),),
|
||||||
|
Pvm("Database", "can_excel_upload"): (Pvm("Database", "can_upload"),),
|
||||||
|
Pvm("Database", "can_columnar_upload"): (Pvm("Database", "can_upload"),),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def do_upgrade(session: Session) -> None:
|
||||||
|
add_pvms(session, NEW_PVMS)
|
||||||
|
migrate_roles(session, PVM_MAP)
|
||||||
|
|
||||||
|
|
||||||
|
def do_downgrade(session: Session) -> None:
|
||||||
|
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
|
||||||
|
migrate_roles(session, get_reversed_pvm_map(PVM_MAP))
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
session = Session(bind=bind)
|
||||||
|
|
||||||
|
do_upgrade(session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
except SQLAlchemyError as ex:
|
||||||
|
session.rollback()
|
||||||
|
raise Exception(f"An error occurred while upgrading permissions: {ex}") from ex
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
bind = op.get_bind()
|
||||||
|
session = Session(bind=bind)
|
||||||
|
|
||||||
|
do_downgrade(session)
|
||||||
|
|
||||||
|
try:
|
||||||
|
session.commit()
|
||||||
|
except SQLAlchemyError as ex:
|
||||||
|
print(f"An error occurred while downgrading permissions: {ex}")
|
||||||
|
session.rollback()
|
||||||
|
pass
|
||||||
|
|
@ -270,8 +270,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
|
||||||
}
|
}
|
||||||
|
|
||||||
ALPHA_ONLY_PMVS = {
|
ALPHA_ONLY_PMVS = {
|
||||||
("can_csv_upload", "Database"),
|
("can_upload", "Database"),
|
||||||
("can_excel_upload", "Database"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ADMIN_ONLY_PERMISSIONS = {
|
ADMIN_ONLY_PERMISSIONS = {
|
||||||
|
|
|
||||||
|
|
@ -1433,11 +1433,9 @@ class TestDatabaseApi(SupersetTestCase):
|
||||||
assert rv.status_code == 200
|
assert rv.status_code == 200
|
||||||
assert set(data["permissions"]) == {
|
assert set(data["permissions"]) == {
|
||||||
"can_read",
|
"can_read",
|
||||||
"can_columnar_upload",
|
|
||||||
"can_csv_upload",
|
|
||||||
"can_excel_upload",
|
|
||||||
"can_write",
|
"can_write",
|
||||||
"can_export",
|
"can_export",
|
||||||
|
"can_upload",
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_get_invalid_database_table_metadata(self):
|
def test_get_invalid_database_table_metadata(self):
|
||||||
|
|
|
||||||
|
|
@ -1336,7 +1336,7 @@ class TestRolePermission(SupersetTestCase):
|
||||||
self.assert_cannot_menu("Upload a CSV", perm_set)
|
self.assert_cannot_menu("Upload a CSV", perm_set)
|
||||||
self.assert_cannot_menu("ReportSchedule", perm_set)
|
self.assert_cannot_menu("ReportSchedule", perm_set)
|
||||||
self.assert_cannot_menu("Alerts & Report", perm_set)
|
self.assert_cannot_menu("Alerts & Report", perm_set)
|
||||||
assert ("can_csv_upload", "Database") not in perm_set
|
assert ("can_upload", "Database") not in perm_set
|
||||||
|
|
||||||
def assert_can_gamma(self, perm_set):
|
def assert_can_gamma(self, perm_set):
|
||||||
self.assert_can_read("Dataset", perm_set)
|
self.assert_can_read("Dataset", perm_set)
|
||||||
|
|
@ -1365,7 +1365,7 @@ class TestRolePermission(SupersetTestCase):
|
||||||
self.assert_can_all("CssTemplate", perm_set)
|
self.assert_can_all("CssTemplate", perm_set)
|
||||||
self.assert_can_all("Dataset", perm_set)
|
self.assert_can_all("Dataset", perm_set)
|
||||||
self.assert_can_read("Database", perm_set)
|
self.assert_can_read("Database", perm_set)
|
||||||
assert ("can_csv_upload", "Database") in perm_set
|
assert ("can_upload", "Database") in perm_set
|
||||||
self.assert_can_menu("Manage", perm_set)
|
self.assert_can_menu("Manage", perm_set)
|
||||||
self.assert_can_menu("Annotation Layers", perm_set)
|
self.assert_can_menu("Annotation Layers", perm_set)
|
||||||
self.assert_can_menu("CSS Templates", perm_set)
|
self.assert_can_menu("CSS Templates", perm_set)
|
||||||
|
|
|
||||||
|
|
@ -842,6 +842,7 @@ def test_oauth2_error(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -855,6 +856,7 @@ def test_oauth2_error(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
"file": ANY,
|
"file": ANY,
|
||||||
|
|
@ -864,6 +866,7 @@ def test_oauth2_error(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table2",
|
"table_name": "table2",
|
||||||
"delimiter": ";",
|
"delimiter": ";",
|
||||||
|
|
@ -879,6 +882,7 @@ def test_oauth2_error(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"already_exists": "replace",
|
"already_exists": "replace",
|
||||||
"column_dates": ["col1", "col2"],
|
"column_dates": ["col1", "col2"],
|
||||||
"delimiter": ";",
|
"delimiter": ";",
|
||||||
|
|
@ -889,6 +893,7 @@ def test_oauth2_error(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table2",
|
"table_name": "table2",
|
||||||
"delimiter": ";",
|
"delimiter": ";",
|
||||||
|
|
@ -911,6 +916,7 @@ def test_oauth2_error(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"already_exists": "replace",
|
"already_exists": "replace",
|
||||||
"columns_read": ["col1", "col2"],
|
"columns_read": ["col1", "col2"],
|
||||||
"null_values": ["None", "N/A", "''"],
|
"null_values": ["None", "N/A", "''"],
|
||||||
|
|
@ -945,7 +951,7 @@ def test_csv_upload(
|
||||||
reader_mock = mocker.patch.object(CSVReader, "__init__")
|
reader_mock = mocker.patch.object(CSVReader, "__init__")
|
||||||
reader_mock.return_value = None
|
reader_mock.return_value = None
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/csv_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data=payload,
|
data=payload,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
|
|
@ -960,6 +966,7 @@ def test_csv_upload(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -968,6 +975,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "",
|
"table_name": "",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -976,11 +984,17 @@ def test_csv_upload(
|
||||||
{"message": {"table_name": ["Length must be between 1 and 10000."]}},
|
{"message": {"table_name": ["Length must be between 1 and 10000."]}},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"table_name": "table1", "delimiter": ",", "already_exists": "fail"},
|
{
|
||||||
|
"type": "csv",
|
||||||
|
"table_name": "table1",
|
||||||
|
"delimiter": ",",
|
||||||
|
"already_exists": "fail",
|
||||||
|
},
|
||||||
{"message": {"file": ["Field may not be null."]}},
|
{"message": {"file": ["Field may not be null."]}},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": "xpto",
|
"file": "xpto",
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -990,6 +1004,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -999,6 +1014,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1009,6 +1025,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1019,6 +1036,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1029,6 +1047,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1039,6 +1058,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1049,6 +1069,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1059,6 +1080,7 @@ def test_csv_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "csv",
|
||||||
"file": (create_csv_file(), "out.csv"),
|
"file": (create_csv_file(), "out.csv"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1082,7 +1104,7 @@ def test_csv_upload_validation(
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/csv_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data=payload,
|
data=payload,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
|
|
@ -1116,8 +1138,9 @@ def test_csv_upload_file_extension_invalid(
|
||||||
"""
|
"""
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/csv_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data={
|
data={
|
||||||
|
"type": "csv",
|
||||||
"file": create_csv_file(filename=filename),
|
"file": create_csv_file(filename=filename),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1152,8 +1175,9 @@ def test_csv_upload_file_extension_valid(
|
||||||
"""
|
"""
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/csv_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data={
|
data={
|
||||||
|
"type": "csv",
|
||||||
"file": create_csv_file(filename=filename),
|
"file": create_csv_file(filename=filename),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"delimiter": ",",
|
"delimiter": ",",
|
||||||
|
|
@ -1168,6 +1192,7 @@ def test_csv_upload_file_extension_valid(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
},
|
},
|
||||||
|
|
@ -1180,6 +1205,7 @@ def test_csv_upload_file_extension_valid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
"file": ANY,
|
"file": ANY,
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
|
|
@ -1188,6 +1214,7 @@ def test_csv_upload_file_extension_valid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table2",
|
"table_name": "table2",
|
||||||
"sheet_name": "Sheet1",
|
"sheet_name": "Sheet1",
|
||||||
|
|
@ -1203,6 +1230,7 @@ def test_csv_upload_file_extension_valid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"already_exists": "replace",
|
"already_exists": "replace",
|
||||||
"column_dates": ["col1", "col2"],
|
"column_dates": ["col1", "col2"],
|
||||||
"sheet_name": "Sheet1",
|
"sheet_name": "Sheet1",
|
||||||
|
|
@ -1213,6 +1241,7 @@ def test_csv_upload_file_extension_valid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table2",
|
"table_name": "table2",
|
||||||
"sheet_name": "Sheet1",
|
"sheet_name": "Sheet1",
|
||||||
|
|
@ -1231,6 +1260,7 @@ def test_csv_upload_file_extension_valid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"already_exists": "replace",
|
"already_exists": "replace",
|
||||||
"columns_read": ["col1", "col2"],
|
"columns_read": ["col1", "col2"],
|
||||||
"null_values": ["None", "N/A", "''"],
|
"null_values": ["None", "N/A", "''"],
|
||||||
|
|
@ -1261,7 +1291,7 @@ def test_excel_upload(
|
||||||
reader_mock = mocker.patch.object(ExcelReader, "__init__")
|
reader_mock = mocker.patch.object(ExcelReader, "__init__")
|
||||||
reader_mock.return_value = None
|
reader_mock.return_value = None
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/excel_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data=payload,
|
data=payload,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
|
|
@ -1276,6 +1306,7 @@ def test_excel_upload(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"sheet_name": "Sheet1",
|
"sheet_name": "Sheet1",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1284,6 +1315,7 @@ def test_excel_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "",
|
"table_name": "",
|
||||||
"sheet_name": "Sheet1",
|
"sheet_name": "Sheet1",
|
||||||
|
|
@ -1292,11 +1324,12 @@ def test_excel_upload(
|
||||||
{"message": {"table_name": ["Length must be between 1 and 10000."]}},
|
{"message": {"table_name": ["Length must be between 1 and 10000."]}},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"table_name": "table1", "already_exists": "fail"},
|
{"type": "excel", "table_name": "table1", "already_exists": "fail"},
|
||||||
{"message": {"file": ["Field may not be null."]}},
|
{"message": {"file": ["Field may not be null."]}},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": "xpto",
|
"file": "xpto",
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1305,6 +1338,7 @@ def test_excel_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "xpto",
|
"already_exists": "xpto",
|
||||||
|
|
@ -1313,6 +1347,7 @@ def test_excel_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1322,6 +1357,7 @@ def test_excel_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1331,6 +1367,7 @@ def test_excel_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "excel",
|
||||||
"file": (create_excel_file(), "out.xls"),
|
"file": (create_excel_file(), "out.xls"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1353,7 +1390,7 @@ def test_excel_upload_validation(
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/excel_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data=payload,
|
data=payload,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
|
|
@ -1387,8 +1424,9 @@ def test_excel_upload_file_extension_invalid(
|
||||||
"""
|
"""
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/excel_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data={
|
data={
|
||||||
|
"type": "excel",
|
||||||
"file": create_excel_file(filename=filename),
|
"file": create_excel_file(filename=filename),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
},
|
},
|
||||||
|
|
@ -1403,6 +1441,7 @@ def test_excel_upload_file_extension_invalid(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"file": (create_columnar_file(), "out.parquet"),
|
"file": (create_columnar_file(), "out.parquet"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
},
|
},
|
||||||
|
|
@ -1415,6 +1454,7 @@ def test_excel_upload_file_extension_invalid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
"file": ANY,
|
"file": ANY,
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
|
|
@ -1423,6 +1463,7 @@ def test_excel_upload_file_extension_invalid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"file": (create_columnar_file(), "out.parquet"),
|
"file": (create_columnar_file(), "out.parquet"),
|
||||||
"table_name": "table2",
|
"table_name": "table2",
|
||||||
"already_exists": "replace",
|
"already_exists": "replace",
|
||||||
|
|
@ -1439,6 +1480,7 @@ def test_excel_upload_file_extension_invalid(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"already_exists": "replace",
|
"already_exists": "replace",
|
||||||
"columns_read": ["col1", "col2"],
|
"columns_read": ["col1", "col2"],
|
||||||
"file": ANY,
|
"file": ANY,
|
||||||
|
|
@ -1467,7 +1509,7 @@ def test_columnar_upload(
|
||||||
reader_mock = mocker.patch.object(ColumnarReader, "__init__")
|
reader_mock = mocker.patch.object(ColumnarReader, "__init__")
|
||||||
reader_mock.return_value = None
|
reader_mock.return_value = None
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/columnar_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data=payload,
|
data=payload,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
|
|
@ -1482,6 +1524,7 @@ def test_columnar_upload(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"file": (create_columnar_file(), "out.parquet"),
|
"file": (create_columnar_file(), "out.parquet"),
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
},
|
},
|
||||||
|
|
@ -1489,6 +1532,7 @@ def test_columnar_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"file": (create_columnar_file(), "out.parquet"),
|
"file": (create_columnar_file(), "out.parquet"),
|
||||||
"table_name": "",
|
"table_name": "",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1496,11 +1540,12 @@ def test_columnar_upload(
|
||||||
{"message": {"table_name": ["Length must be between 1 and 10000."]}},
|
{"message": {"table_name": ["Length must be between 1 and 10000."]}},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{"table_name": "table1", "already_exists": "fail"},
|
{"type": "columnar", "table_name": "table1", "already_exists": "fail"},
|
||||||
{"message": {"file": ["Field may not be null."]}},
|
{"message": {"file": ["Field may not be null."]}},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"file": "xpto",
|
"file": "xpto",
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "fail",
|
"already_exists": "fail",
|
||||||
|
|
@ -1509,6 +1554,7 @@ def test_columnar_upload(
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
"type": "columnar",
|
||||||
"file": (create_columnar_file(), "out.parquet"),
|
"file": (create_columnar_file(), "out.parquet"),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
"already_exists": "xpto",
|
"already_exists": "xpto",
|
||||||
|
|
@ -1530,7 +1576,7 @@ def test_columnar_upload_validation(
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/columnar_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data=payload,
|
data=payload,
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
|
|
@ -1559,8 +1605,9 @@ def test_columnar_upload_file_extension_valid(
|
||||||
"""
|
"""
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/columnar_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data={
|
data={
|
||||||
|
"type": "columnar",
|
||||||
"file": (create_columnar_file(), filename),
|
"file": (create_columnar_file(), filename),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
},
|
},
|
||||||
|
|
@ -1595,8 +1642,9 @@ def test_columnar_upload_file_extension_invalid(
|
||||||
"""
|
"""
|
||||||
_ = mocker.patch.object(UploadCommand, "run")
|
_ = mocker.patch.object(UploadCommand, "run")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/1/columnar_upload/",
|
"/api/v1/database/1/upload/",
|
||||||
data={
|
data={
|
||||||
|
"type": "columnar",
|
||||||
"file": create_columnar_file(filename=filename),
|
"file": create_columnar_file(filename=filename),
|
||||||
"table_name": "table1",
|
"table_name": "table1",
|
||||||
},
|
},
|
||||||
|
|
@ -1611,8 +1659,8 @@ def test_csv_metadata(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(CSVReader, "file_metadata")
|
_ = mocker.patch.object(CSVReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/csv_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={"file": create_csv_file()},
|
data={"type": "csv", "file": create_csv_file()},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
@ -1623,8 +1671,8 @@ def test_csv_metadata_bad_extension(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(CSVReader, "file_metadata")
|
_ = mocker.patch.object(CSVReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/csv_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={"file": create_csv_file(filename="test.out")},
|
data={"type": "csv", "file": create_csv_file(filename="test.out")},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
@ -1636,21 +1684,29 @@ def test_csv_metadata_validation(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(CSVReader, "file_metadata")
|
_ = mocker.patch.object(CSVReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/csv_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={},
|
data={"type": "csv"},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert response.json == {"message": {"file": ["Field may not be null."]}}
|
assert response.json == {"message": {"file": ["Field may not be null."]}}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/database/upload_metadata/",
|
||||||
|
data={"file": create_csv_file(filename="test.csv")},
|
||||||
|
content_type="multipart/form-data",
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
assert response.json == {"message": {"type": ["Missing data for required field."]}}
|
||||||
|
|
||||||
|
|
||||||
def test_excel_metadata(
|
def test_excel_metadata(
|
||||||
mocker: MockerFixture, client: Any, full_api_access: None
|
mocker: MockerFixture, client: Any, full_api_access: None
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(ExcelReader, "file_metadata")
|
_ = mocker.patch.object(ExcelReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/excel_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={"file": create_excel_file()},
|
data={"type": "excel", "file": create_excel_file()},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
@ -1661,8 +1717,8 @@ def test_excel_metadata_bad_extension(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(ExcelReader, "file_metadata")
|
_ = mocker.patch.object(ExcelReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/excel_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={"file": create_excel_file(filename="test.out")},
|
data={"type": "excel", "file": create_excel_file(filename="test.out")},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
@ -1674,8 +1730,8 @@ def test_excel_metadata_validation(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(ExcelReader, "file_metadata")
|
_ = mocker.patch.object(ExcelReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/excel_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={},
|
data={"type": "excel"},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
@ -1687,8 +1743,8 @@ def test_columnar_metadata(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(ColumnarReader, "file_metadata")
|
_ = mocker.patch.object(ColumnarReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/columnar_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={"file": create_columnar_file()},
|
data={"type": "columnar", "file": create_columnar_file()},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
@ -1699,8 +1755,8 @@ def test_columnar_metadata_bad_extension(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(ColumnarReader, "file_metadata")
|
_ = mocker.patch.object(ColumnarReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/columnar_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={"file": create_columnar_file(filename="test.out")},
|
data={"type": "columnar", "file": create_columnar_file(filename="test.out")},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
@ -1712,8 +1768,8 @@ def test_columnar_metadata_validation(
|
||||||
) -> None:
|
) -> None:
|
||||||
_ = mocker.patch.object(ColumnarReader, "file_metadata")
|
_ = mocker.patch.object(ColumnarReader, "file_metadata")
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/database/columnar_metadata/",
|
"/api/v1/database/upload_metadata/",
|
||||||
data={},
|
data={"type": "columnar"},
|
||||||
content_type="multipart/form-data",
|
content_type="multipart/form-data",
|
||||||
)
|
)
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue