diff --git a/.codespellrc b/.codespellrc index 261197eb..07d4908c 100644 --- a/.codespellrc +++ b/.codespellrc @@ -3,7 +3,7 @@ ignore-words-list = Skyflow,skyflow,skyflowapi,skyflowapis,deidentify,reidentify,detokenize,upsert,upserting,binlookup,byot,creds,fpe,devsecops,formdata,vaultid,dotenv,usecwd,runid,dateutil,Homogenous # Skip these files and folders -skip = .git,.venv,venv,env,__pycache__,*.pyc,*.egg-info,dist,build,.idea,.vscode,*.log,requirements.txt,./skyflow/generated,setup.py +skip = .git,.venv,venv,env,__pycache__,*.pyc,*.egg-info,dist,build,.idea,.vscode,*.log,requirements.txt,generated,setup.py # If you want to verify it is working, you can uncomment this line to see what files it checks # count = diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 7ad03858..61f9fa17 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -4,15 +4,28 @@ on: push: tags: '*.*.*b*' paths-ignore: - - "setup.py" + - "*/setup.py" - "*.yml" - "*.md" - - "skyflow/utils/_version.py" + - "*/skyflow/utils/_version.py" jobs: build-and-deploy: + strategy: + matrix: + include: + - variant: v2 + package-name: skyflow + tag-prefix: '' + - variant: flowvault + package-name: skyflow_flowvault + tag-prefix: 'flowvault-' + if: (matrix.variant == 'flowvault' && startsWith(github.ref_name, 'flowvault-')) || (matrix.variant == 'v2' && !startsWith(github.ref_name, 'flowvault-')) uses: ./.github/workflows/shared-build-and-deploy.yml with: ref: ${{ github.ref_name }} tag: 'beta' + variant: ${{ matrix.variant }} + package-name: ${{ matrix.package-name }} + tag-prefix: ${{ matrix.tag-prefix }} secrets: inherit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 952ccb38..e82cabd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,39 @@ jobs: error: 'One of your your commit messages is not matching the format with JIRA ID Ex: ( SDK-123 commit message )' test: + strategy: + fail-fast: false + matrix: + include: + - variant: v2 + package-name: skyflow + coverage-omit: "skyflow/generated/*,skyflow/utils/validations/*,skyflow/vault/data/*,skyflow/vault/detect/*,skyflow/vault/tokens/*,skyflow/vault/connection/*,skyflow/error/*,skyflow/utils/enums/*,skyflow/vault/controller/_audit.py,skyflow/vault/controller/_bin_look_up.py" + - variant: flowvault + package-name: skyflow_flowvault + coverage-omit: "skyflow_flowvault/generated/*" uses: ./.github/workflows/shared-tests.yml with: python-version: '3.9' + variant: ${{ matrix.variant }} + package-name: ${{ matrix.package-name }} + coverage-omit: ${{ matrix.coverage-omit }} secrets: inherit + + test-common: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - run: pip install -e ./common + - run: pip install coverage + - run: python -m coverage run --source=common --omit="common/generated/*,common/tests/*" -m unittest discover -s common/tests -t . + - run: coverage xml -o test-coverage.xml + - name: Codecov + uses: codecov/codecov-action@v2.1.0 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: test-coverage.xml + name: codecov-skyflow-python-common + verbose: true diff --git a/.github/workflows/internal-release.yml b/.github/workflows/internal-release.yml index 2e273096..b91e5b41 100644 --- a/.github/workflows/internal-release.yml +++ b/.github/workflows/internal-release.yml @@ -5,19 +5,33 @@ on: tags-ignore: - '*.*' paths-ignore: - - "setup.py" + - "*/setup.py" - "*.yml" - "*.md" - - "skyflow/utils/_version.py" + - "*/skyflow/utils/_version.py" - "samples/**" + - "flowvault/samples/**" branches: - release/* + - flowvault-release/* jobs: build-and-deploy: + strategy: + matrix: + include: + - variant: v2 + package-name: skyflow + tag-prefix: '' + - variant: flowvault + package-name: skyflow_flowvault + tag-prefix: 'flowvault-' + if: (matrix.variant == 'flowvault' && startsWith(github.ref_name, 'flowvault-')) || (matrix.variant == 'v2' && !startsWith(github.ref_name, 'flowvault-')) uses: ./.github/workflows/shared-build-and-deploy.yml with: ref: ${{ github.ref_name }} tag: 'internal' + variant: ${{ matrix.variant }} + package-name: ${{ matrix.package-name }} + tag-prefix: ${{ matrix.tag-prefix }} secrets: inherit - \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 01b8c040..de816ac0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,39 @@ on: jobs: test: + strategy: + fail-fast: false + matrix: + include: + - variant: v2 + package-name: skyflow + coverage-omit: "skyflow/generated/*,skyflow/utils/validations/*,skyflow/vault/data/*,skyflow/vault/detect/*,skyflow/vault/tokens/*,skyflow/vault/connection/*,skyflow/error/*,skyflow/utils/enums/*,skyflow/vault/controller/_audit.py,skyflow/vault/controller/_bin_look_up.py" + - variant: flowvault + package-name: skyflow_flowvault + coverage-omit: "skyflow_flowvault/generated/*" uses: ./.github/workflows/shared-tests.yml with: python-version: '3.9' + variant: ${{ matrix.variant }} + package-name: ${{ matrix.package-name }} + coverage-omit: ${{ matrix.coverage-omit }} secrets: inherit + + test-common: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: '3.9' + - run: pip install -e ./common + - run: pip install coverage + - run: python -m coverage run --source=common --omit="common/generated/*,common/tests/*" -m unittest discover -s common/tests -t . + - run: coverage xml -o test-coverage.xml + - name: Codecov + uses: codecov/codecov-action@v2.1.0 + with: + token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} + files: test-coverage.xml + name: codecov-skyflow-python-common + verbose: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d062daf4..01152204 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,15 +4,28 @@ on: push: tags: "*.*.*" paths-ignore: - - "setup.py" + - "*/setup.py" - "*.yml" - "*.md" - - "skyflow/utils/_version.py" + - "*/skyflow/utils/_version.py" jobs: build-and-deploy: + strategy: + matrix: + include: + - variant: v2 + package-name: skyflow + tag-prefix: '' + - variant: flowvault + package-name: skyflow_flowvault + tag-prefix: 'flowvault-' + if: (matrix.variant == 'flowvault' && startsWith(github.ref_name, 'flowvault-')) || (matrix.variant == 'v2' && !startsWith(github.ref_name, 'flowvault-')) uses: ./.github/workflows/shared-build-and-deploy.yml with: ref: main tag: 'public' + variant: ${{ matrix.variant }} + package-name: ${{ matrix.package-name }} + tag-prefix: ${{ matrix.tag-prefix }} secrets: inherit diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index bce5fc8e..c286b921 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -20,7 +20,7 @@ jobs: - name: Run Semgrep run: | - semgrep --config .semgreprules/customRule.yml --config auto --severity ERROR --sarif . > results.sarif + semgrep --config .semgreprules/customRule.yml --config auto --severity ERROR --exclude "**/generated/**" --sarif . > results.sarif - name: Upload SARIF file uses: github/codeql-action/upload-sarif@v3 diff --git a/.github/workflows/shared-build-and-deploy.yml b/.github/workflows/shared-build-and-deploy.yml index 135b87bc..adffc3fe 100644 --- a/.github/workflows/shared-build-and-deploy.yml +++ b/.github/workflows/shared-build-and-deploy.yml @@ -7,12 +7,29 @@ on: description: 'Git reference to use (e.g., main or branch name)' required: true type: string - + tag: description: 'Release Tag' required: true type: string + variant: + description: 'Build variant directory to release (v2 or flowvault)' + required: true + type: string + + package-name: + description: 'Importable package name for this variant (e.g. skyflow or skyflow_flowvault)' + required: false + type: string + default: 'skyflow' + + tag-prefix: + description: 'Prefix distinguishing this variant''s git tags from other variants'' (e.g. "flowvault-" for flowvault, empty for v2)' + required: false + type: string + default: '' + jobs: build-and-deploy: runs-on: ubuntu-latest @@ -29,7 +46,7 @@ jobs: - name: Resolve Branch for the Tagged Commit id: resolve-branch - if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} + if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} run: | TAG_COMMIT=$(git rev-list -n 1 ${{ github.ref_name }}) @@ -47,18 +64,31 @@ jobs: id: previoustag uses: WyriHaximus/github-action-get-previous-tag@v1 with: - fallback: 1.0.0 + fallback: ${{ inputs.tag-prefix }}1.0.0 + pattern: ${{ inputs.tag-prefix }}[0-9]*.[0-9]*.[0-9]* + + - name: Resolve version number + id: version + env: + PREVIOUS_TAG: ${{ steps.previoustag.outputs.tag }} + TAG_PREFIX: ${{ inputs.tag-prefix }} + run: | + TAG="$PREVIOUS_TAG" + VERSION="${TAG#$TAG_PREFIX}" + echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Bump Version + working-directory: ${{ inputs.variant }} run: | - chmod +x ./ci-scripts/bump_version.sh + chmod +x ../ci-scripts/bump_version.sh if ${{ inputs.tag == 'internal' }}; then - ./ci-scripts/bump_version.sh "${{ steps.previoustag.outputs.tag }}" "$(git rev-parse --short "$GITHUB_SHA")" + ../ci-scripts/bump_version.sh "${{ steps.version.outputs.version }}" "$(git rev-parse --short "$GITHUB_SHA")" "${{ inputs.package-name }}" else - ./ci-scripts/bump_version.sh "${{ steps.previoustag.outputs.tag }}" + ../ci-scripts/bump_version.sh "${{ steps.version.outputs.version }}" "" "${{ inputs.package-name }}" fi - name: Commit changes + working-directory: ${{ inputs.variant }} run: | git config user.name "${{ github.actor }}" git config user.email "${{ github.actor }}@users.noreply.github.com" @@ -68,27 +98,29 @@ jobs: fi git add setup.py - git add skyflow/utils/_version.py + git add ${{ inputs.package-name }}/utils/_version.py if [[ "${{ inputs.tag }}" == "internal" ]]; then - VERSION="${{ steps.previoustag.outputs.tag }}.dev0+$(git rev-parse --short $GITHUB_SHA)" - COMMIT_MESSAGE="[AUTOMATED] Private Release $VERSION" + VERSION="${{ steps.version.outputs.version }}.dev0+$(git rev-parse --short $GITHUB_SHA)" + COMMIT_MESSAGE="[AUTOMATED] Private Release (${{ inputs.variant }}) $VERSION" git commit -m "$COMMIT_MESSAGE" git push origin ${{ github.ref_name }} -f fi if [[ "${{ inputs.tag }}" == "beta" || "${{ inputs.tag }}" == "public" ]]; then - COMMIT_MESSAGE="[AUTOMATED] Public Release - ${{ steps.previoustag.outputs.tag }}" + COMMIT_MESSAGE="[AUTOMATED] Public Release (${{ inputs.variant }}) - ${{ steps.previoustag.outputs.tag }}" git commit -m "$COMMIT_MESSAGE" git push origin ${{ env.branch_name }} fi - - name: Build and install skyflow package + - name: Build and install package + working-directory: ${{ inputs.variant }} run: | python setup.py sdist bdist_wheel - pip install dist/skyflow-*.whl + pip install dist/*.whl - name: Build and Publish Package - if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} + if: ${{ inputs.tag == 'beta' || inputs.tag == 'public' }} + working-directory: ${{ inputs.variant }} env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PUBLISH_TOKEN }} @@ -98,9 +130,10 @@ jobs: - name: Build and Publish to JFrog Artifactory if: ${{ inputs.tag == 'internal' }} + working-directory: ${{ inputs.variant }} env: TWINE_USERNAME: ${{ secrets.JFROG_USERNAME }} TWINE_PASSWORD: ${{ secrets.JFROG_PASSWORD }} run: | python setup.py sdist bdist_wheel - twine upload --repository-url https://prekarilabs.jfrog.io/artifactory/api/pypi/skyflow-python/ dist/* \ No newline at end of file + twine upload --repository-url https://prekarilabs.jfrog.io/artifactory/api/pypi/skyflow-python/ dist/* diff --git a/.github/workflows/shared-tests.yml b/.github/workflows/shared-tests.yml index 24fa6a0e..796eb89f 100644 --- a/.github/workflows/shared-tests.yml +++ b/.github/workflows/shared-tests.yml @@ -7,6 +7,20 @@ on: description: 'Python version to use' required: true type: string + variant: + description: 'Build variant directory to test (v2 or flowvault)' + required: true + type: string + package-name: + description: 'Importable package name for this variant (e.g. skyflow or skyflow_flowvault)' + required: false + type: string + default: 'skyflow' + coverage-omit: + description: 'Comma-separated coverage --omit patterns, relative to the variant directory' + required: false + type: string + default: 'skyflow/generated/*' jobs: run-tests: @@ -23,12 +37,14 @@ jobs: with: name: "credentials.json" json: ${{ secrets.VALID_SKYFLOW_CREDS_TEST }} + dir: ${{ inputs.variant }} - - name: Build and install skyflow package + - name: Build and install package + working-directory: ${{ inputs.variant }} run: | pip install --upgrade pip setuptools wheel python setup.py sdist bdist_wheel - pip install dist/skyflow-*.whl + pip install dist/*.whl pip install ".[dev]" - name: Run Spell Check @@ -36,19 +52,21 @@ jobs: - name: Run Linter Ruff run: ruff check . --output-format=github - + - name: 'Run Tests' + working-directory: ${{ inputs.variant }} run: | pip install -r requirements.txt - python -m coverage run --source=skyflow --omit=skyflow/generated/*,skyflow/utils/validations/*,skyflow/vault/data/*,skyflow/vault/detect/*,skyflow/vault/tokens/*,skyflow/vault/connection/*,skyflow/error/*,skyflow/utils/enums/*,skyflow/vault/controller/_audit.py,skyflow/vault/controller/_bin_look_up.py -m unittest discover + python -m coverage run --source=${{ inputs.package-name }} --omit=${{ inputs.coverage-omit }} -m unittest discover - name: coverage + working-directory: ${{ inputs.variant }} run: coverage xml -o test-coverage.xml - name: Codecov uses: codecov/codecov-action@v2.1.0 with: token: ${{ secrets.CODECOV_REPO_UPLOAD_TOKEN }} - files: test-coverage.xml - name: codecov-skyflow-python + files: ${{ inputs.variant }}/test-coverage.xml + name: codecov-skyflow-python-${{ inputs.variant }} verbose: true diff --git a/.gitignore b/.gitignore index 0414a3a5..cceb4f18 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,7 @@ celerybeat.pid # Environments .env .venv +.venv-* env/ venv/ ENV/ diff --git a/.semgreprules/customRule.yml b/.semgreprules/customRule.yml index b275e280..f80b5d4b 100644 --- a/.semgreprules/customRule.yml +++ b/.semgreprules/customRule.yml @@ -12,7 +12,7 @@ rules: - golang - docker patterns: - - pattern-regex: (?i)\b(api[_-]key|api[_-]token|api[_-]secret[_-]key|api[_-]password|token|secret[_-]key|password|auth[_-]key|auth[_-]token|AUTH_PASSWORD)\s*[:=]\s*(['"]?)((?!YOUR_EXCLUSION_PATTERN_HERE)[A-Z]+.*?)\2 + - pattern-regex: (?i)\b(api[_-]key|api[_-]token|api[_-]secret[_-]key|api[_-]password|token|secret[_-]key|password|auth[_-]key|auth[_-]token|AUTH_PASSWORD)\s*[:=]\s*(['"])(?!\1\2)((?!YOUR_EXCLUSION_PATTERN_HERE)[A-Z]+.*?)\2 - id: check-logger-appconfig message: >- diff --git a/ci-scripts/bump_version.sh b/ci-scripts/bump_version.sh index ab79e8aa..0fc6e782 100755 --- a/ci-scripts/bump_version.sh +++ b/ci-scripts/bump_version.sh @@ -1,13 +1,14 @@ Version=$1 SEMVER=$Version +PackageName=${3:-skyflow} if [ -z "$2" ] then echo "Bumping package version to $1" sed -E "s/current_version = .+/current_version = '$SEMVER'/g" setup.py > tempfile && cat tempfile > setup.py && rm -f tempfile - sed -E "s/SDK_VERSION = .+/SDK_VERSION = '$SEMVER'/g" skyflow/utils/_version.py > tempfile && cat tempfile > skyflow/utils/_version.py && rm -f tempfile - sed -E "s/__version__ = .+/__version__ = '$SEMVER'/g" skyflow/generated/rest/version.py > tempfile && cat tempfile > skyflow/generated/rest/version.py && rm -f tempfile + sed -E "s/SDK_VERSION = .+/SDK_VERSION = '$SEMVER'/g" $PackageName/utils/_version.py > tempfile && cat tempfile > $PackageName/utils/_version.py && rm -f tempfile + sed -E "s/__version__ = .+/__version__ = '$SEMVER'/g" $PackageName/generated/rest/version.py > tempfile && cat tempfile > $PackageName/generated/rest/version.py && rm -f tempfile echo -------------------------- echo "Done, Package now at $1" @@ -18,8 +19,8 @@ else echo "Bumping package version to $DEV_VERSION" sed -E "s/current_version = .+/current_version = '$DEV_VERSION'/g" setup.py > tempfile && cat tempfile > setup.py && rm -f tempfile - sed -E "s/SDK_VERSION = .+/SDK_VERSION = '$DEV_VERSION'/g" skyflow/utils/_version.py > tempfile && cat tempfile > skyflow/utils/_version.py && rm -f tempfile - sed -E "s/__version__ = .+/__version__ = '$DEV_VERSION'/g" skyflow/generated/rest/version.py > tempfile && cat tempfile > skyflow/generated/rest/version.py && rm -f tempfile + sed -E "s/SDK_VERSION = .+/SDK_VERSION = '$DEV_VERSION'/g" $PackageName/utils/_version.py > tempfile && cat tempfile > $PackageName/utils/_version.py && rm -f tempfile + sed -E "s/__version__ = .+/__version__ = '$DEV_VERSION'/g" $PackageName/generated/rest/version.py > tempfile && cat tempfile > $PackageName/generated/rest/version.py && rm -f tempfile echo -------------------------- echo "Done, Package now at $DEV_VERSION" diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 00000000..984fd32b --- /dev/null +++ b/common/__init__.py @@ -0,0 +1,3 @@ +# common.utils and common.errors mutually depend on each other -- forcing utils to load first +# here avoids the circular import (mirrors skyflow/__init__.py's own first line). +from . import utils # noqa: F401 diff --git a/skyflow/generated/__init__.py b/common/client/__init__.py similarity index 100% rename from skyflow/generated/__init__.py rename to common/client/__init__.py diff --git a/common/client/base_skyflow.py b/common/client/base_skyflow.py new file mode 100644 index 00000000..29cd1a7c --- /dev/null +++ b/common/client/base_skyflow.py @@ -0,0 +1,341 @@ +from abc import ABC, abstractmethod +from collections import OrderedDict +from common.errors import SkyflowError +from common.utils import SkyflowMessages +from common.utils.logger import log_info, log_warn +from common.utils.constants import OptionField + +_BUILDER_TEMPLATE_ERROR = ( + "BaseSkyflowImpl.Builder is an interface template -- build a concrete Skyflow " + "class via make_skyflow_class() instead of using it directly. Missing: {missing}" +) +_CONNECTIONS_NOT_SUPPORTED_ERROR = "Connections are not supported by this Skyflow SDK variant" +_DETECT_NOT_SUPPORTED_ERROR = "Detect is not supported by this Skyflow SDK variant" + + +class BaseSkyflow(ABC): + @classmethod + @abstractmethod + def builder(cls): + raise NotImplementedError + + @abstractmethod + def add_vault_config(self, config): + raise NotImplementedError + + @abstractmethod + def remove_vault_config(self, vault_id): + raise NotImplementedError + + @abstractmethod + def update_vault_config(self, config): + raise NotImplementedError + + @abstractmethod + def get_vault_config(self, vault_id): + raise NotImplementedError + + @abstractmethod + def add_skyflow_credentials(self, credentials): + raise NotImplementedError + + @abstractmethod + def update_skyflow_credentials(self, credentials): + raise NotImplementedError + + @abstractmethod + def set_log_level(self, log_level): + raise NotImplementedError + + @abstractmethod + def update_log_level(self, log_level): + raise NotImplementedError + + @abstractmethod + def get_log_level(self): + raise NotImplementedError + + @abstractmethod + def vault(self, vault_id=None): + raise NotImplementedError + + +class BaseSkyflowImpl(BaseSkyflow): + + def __init__(self, builder): + if type(self) is BaseSkyflowImpl: + raise SkyflowError( + SkyflowMessages.Error.BASE_SKYFLOW_INSTANTIATION_NOT_ALLOWED.value, + SkyflowMessages.ErrorCodes.INVALID_INPUT.value, + ) + self.__builder = builder + log_info(self.__builder._skyflow_messages.Info.CLIENT_INITIALIZED.value, self.__builder.get_logger()) + + @classmethod + def builder(cls): + return cls.Builder() + + def add_vault_config(self, config): + self.__builder._add_vault_config(config) + return self + + def remove_vault_config(self, vault_id): + self.__builder.remove_vault_config(vault_id) + + def update_vault_config(self, config): + self.__builder.update_vault_config(config) + + def get_vault_config(self, vault_id): + return self.__builder.get_vault_config(vault_id).get(OptionField.VAULT_CLIENT).get_config() + + def add_skyflow_credentials(self, credentials): + self.__builder._add_skyflow_credentials(credentials) + return self + + def update_skyflow_credentials(self, credentials): + self.__builder._add_skyflow_credentials(credentials) + + def set_log_level(self, log_level): + self.__builder._set_log_level(log_level) + return self + + def update_log_level(self, log_level): + """.. deprecated:: Use set_log_level() instead. Will be removed in a future release.""" + log_warn(self.__builder._skyflow_messages.Warning.UPDATE_LOG_LEVEL_DEPRECATED.value) + return self.set_log_level(log_level) + + def get_log_level(self): + return self.__builder.get_log_level() + + def vault(self, vault_id=None): + vault_config = self.__builder.get_vault_config(vault_id) + return vault_config.get(OptionField.VAULT_CONTROLLER) + + def _get_builder(self): + return self.__builder + + class Builder(ABC): + _vault_client_cls = None + _vault_controller_cls = None + _connection_cls = None + _detect_cls = None + _logger_cls = None + _default_log_level = None + _skyflow_messages = None + _skyflow_cls = None + _validate_vault_config = None + _validate_update_vault_config = None + _validate_connection_config = None + _validate_update_connection_config = None + _validate_log_level = None + _validate_credentials = None + _set_active_log_level = None + + _REQUIRED_HOOKS = ( + '_vault_client_cls', '_vault_controller_cls', '_logger_cls', '_default_log_level', + '_skyflow_messages', '_skyflow_cls', '_validate_vault_config', + '_validate_update_vault_config', '_validate_log_level', '_validate_credentials', + ) + + def __init__(self): + missing = [hook for hook in self._REQUIRED_HOOKS if getattr(self, hook) is None] + if missing: + raise NotImplementedError(_BUILDER_TEMPLATE_ERROR.format(missing=', '.join(missing))) + self.__vault_configs = OrderedDict() + self.__vault_list = list() + self.__connection_configs = OrderedDict() + self.__connection_list = list() + self.__skyflow_credentials = None + self.__log_level = self._default_log_level + self.__logger = self._logger_cls(self._default_log_level) + + def _require_connections(self): + if self._connection_cls is None: + raise NotImplementedError(_CONNECTIONS_NOT_SUPPORTED_ERROR) + + def _require_detect(self): + if self._detect_cls is None: + raise NotImplementedError(_DETECT_NOT_SUPPORTED_ERROR) + + def add_vault_config(self, config): + vault_id = config.get(OptionField.VAULT_ID) + if not isinstance(vault_id, str) or not vault_id: + raise SkyflowError( + self._skyflow_messages.Error.INVALID_VAULT_ID.value, + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value + ) + if vault_id in [vault.get(OptionField.VAULT_ID) for vault in self.__vault_list]: + log_info(self._skyflow_messages.Info.VAULT_CONFIG_EXISTS.value.format(vault_id), self.__logger) + raise SkyflowError( + self._skyflow_messages.Error.VAULT_ID_ALREADY_EXISTS.value.format(vault_id), + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value + ) + self.__vault_list.append(config) + return self + + def remove_vault_config(self, vault_id): + if vault_id in self.__vault_configs.keys(): + self.__vault_configs.pop(vault_id) + else: + raise SkyflowError(self._skyflow_messages.Error.INVALID_VAULT_ID.value, + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + + def update_vault_config(self, config): + self._validate_update_vault_config(self.__logger, config) + vault_id = config.get(OptionField.VAULT_ID) + if vault_id not in self.__vault_configs: + raise SkyflowError(self._skyflow_messages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format(vault_id), self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + vault_config = self.__vault_configs[vault_id] + vault_config.get(OptionField.VAULT_CLIENT).update_config(config) + + def get_vault_config(self, vault_id): + if vault_id is None: + if self.__vault_configs: + return next(iter(self.__vault_configs.values())) + raise SkyflowError(self._skyflow_messages.Error.EMPTY_VAULT_CONFIGS.value, self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + + if vault_id in self.__vault_configs: + return self.__vault_configs.get(vault_id) + log_info(self._skyflow_messages.Info.VAULT_CONFIG_DOES_NOT_EXIST.value.format(vault_id), self.__logger) + raise SkyflowError(self._skyflow_messages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format(vault_id), self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + + def add_connection_config(self, config): + self._require_connections() + connection_id = config.get(OptionField.CONNECTION_ID) + if not isinstance(connection_id, str) or not connection_id: + raise SkyflowError( + self._skyflow_messages.Error.INVALID_CONNECTION_ID.value, + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value + ) + if connection_id in [connection.get(OptionField.CONNECTION_ID) for connection in self.__connection_list]: + log_info(self._skyflow_messages.Info.CONNECTION_CONFIG_EXISTS.value.format(connection_id), self.__logger) + raise SkyflowError( + self._skyflow_messages.Error.CONNECTION_ID_ALREADY_EXISTS.value.format(connection_id), + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value + ) + self.__connection_list.append(config) + return self + + def remove_connection_config(self, connection_id): + self._require_connections() + if connection_id in self.__connection_configs.keys(): + self.__connection_configs.pop(connection_id) + else: + raise SkyflowError(self._skyflow_messages.Error.INVALID_CONNECTION_ID.value, + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + + def update_connection_config(self, config): + self._require_connections() + self._validate_update_connection_config(self.__logger, config) + connection_id = config.get(OptionField.CONNECTION_ID) + if connection_id not in self.__connection_configs: + raise SkyflowError(self._skyflow_messages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format(connection_id), self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + connection_config = self.__connection_configs[connection_id] + connection_config.get(OptionField.VAULT_CLIENT).update_config(config) + + def get_connection_config(self, connection_id): + self._require_connections() + if connection_id is None: + if self.__connection_configs: + return next(iter(self.__connection_configs.values())) + + raise SkyflowError(self._skyflow_messages.Error.EMPTY_CONNECTION_CONFIGS.value, self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + + if connection_id in self.__connection_configs: + return self.__connection_configs.get(connection_id) + log_info(self._skyflow_messages.Info.CONNECTION_CONFIG_DOES_NOT_EXIST.value.format(connection_id), self.__logger) + raise SkyflowError(self._skyflow_messages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format(connection_id), self._skyflow_messages.ErrorCodes.INVALID_INPUT.value) + + def add_skyflow_credentials(self, credentials): + self.__skyflow_credentials = credentials + return self + + def set_log_level(self, log_level): + self.__log_level = log_level + return self + + def get_logger(self): + return self.__logger + + def get_log_level(self): + return self.__log_level + + def _add_vault_config(self, config): + self._validate_vault_config(self.__logger, config) + vault_id = config.get(OptionField.VAULT_ID) + if vault_id in self.__vault_configs: + raise SkyflowError( + self._skyflow_messages.Error.VAULT_ID_ALREADY_EXISTS.value.format(vault_id), + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value + ) + vault_client = self._vault_client_cls(config) + vault_config = { + OptionField.VAULT_CLIENT: vault_client, + OptionField.VAULT_CONTROLLER: self._vault_controller_cls(vault_client), + } + if self._detect_cls is not None: + vault_config[OptionField.DETECT_CONTROLLER] = self._detect_cls(vault_client) + self.__vault_configs[vault_id] = vault_config + log_info(self._skyflow_messages.Info.VAULT_CONTROLLER_INITIALIZED.value.format(vault_id), self.__logger) + if self._detect_cls is not None: + log_info(self._skyflow_messages.Info.DETECT_CONTROLLER_INITIALIZED.value.format(vault_id), self.__logger) + + def _add_connection_config(self, config): + self._validate_connection_config(self.__logger, config) + connection_id = config.get(OptionField.CONNECTION_ID) + if connection_id in self.__connection_configs: + raise SkyflowError( + self._skyflow_messages.Error.CONNECTION_ID_ALREADY_EXISTS.value.format(connection_id), + self._skyflow_messages.ErrorCodes.INVALID_INPUT.value + ) + vault_client = self._vault_client_cls(config) + self.__connection_configs[connection_id] = { + OptionField.VAULT_CLIENT: vault_client, + OptionField.CONTROLLER: self._connection_cls(vault_client) + } + log_info(self._skyflow_messages.Info.CONNECTION_CONTROLLER_INITIALIZED.value.format(connection_id), self.__logger) + + def _update_vault_client_logger(self, log_level, logger): + for vault_id, vault_config in self.__vault_configs.items(): + vault_config.get(OptionField.VAULT_CLIENT).set_logger(log_level, logger) + + for connection_id, connection_config in self.__connection_configs.items(): + connection_config.get(OptionField.VAULT_CLIENT).set_logger(log_level, logger) + + def _set_log_level(self, log_level): + self._validate_log_level(self.__logger, log_level) + self.__log_level = log_level + self.__logger.set_log_level(log_level) + if self._set_active_log_level is not None: + self._set_active_log_level(log_level) + self._update_vault_client_logger(log_level, self.__logger) + log_info(self._skyflow_messages.Info.LOGGER_SETUP_DONE.value, self.__logger) + log_info(self._skyflow_messages.Info.CURRENT_LOG_LEVEL.value.format(self.__log_level), self.__logger) + + def _add_skyflow_credentials(self, credentials): + if credentials is not None: + self.__skyflow_credentials = credentials + self._validate_credentials(self.__logger, credentials) + for vault_id, vault_config in self.__vault_configs.items(): + vault_config.get(OptionField.VAULT_CLIENT).set_common_skyflow_credentials(credentials) + + for connection_id, connection_config in self.__connection_configs.items(): + connection_config.get(OptionField.VAULT_CLIENT).set_common_skyflow_credentials(self.__skyflow_credentials) + + def build(self): + self._validate_log_level(self.__logger, self.__log_level) + self.__logger.set_log_level(self.__log_level) + if self._set_active_log_level is not None: + self._set_active_log_level(self.__log_level) + + for config in self.__vault_list: + self._add_vault_config(config) + + for config in self.__connection_list: + self._add_connection_config(config) + + self._update_vault_client_logger(self.__log_level, self.__logger) + + self._add_skyflow_credentials(self.__skyflow_credentials) + + return self._skyflow_cls(self) diff --git a/common/client/utils/__init__.py b/common/client/utils/__init__.py new file mode 100644 index 00000000..5c114c7f --- /dev/null +++ b/common/client/utils/__init__.py @@ -0,0 +1 @@ +from common.client.utils._utils import ConnectionCapable, DetectCapable, ConnectionMixin, DetectMixin, make_skyflow_class diff --git a/common/client/utils/_utils.py b/common/client/utils/_utils.py new file mode 100644 index 00000000..5815dd70 --- /dev/null +++ b/common/client/utils/_utils.py @@ -0,0 +1,127 @@ +from abc import ABC, abstractmethod +from functools import partial + +from common.utils.constants import OptionField +from common.utils.enums import LogLevel as _CommonLogLevel +from common.utils.logger import Logger as _CommonLogger +from common.utils.validations import ( + validate_vault_config as _common_validate_vault_config, + validate_update_vault_config as _common_validate_update_vault_config, + validate_log_level as _common_validate_log_level, + validate_credentials as _common_validate_credentials, +) +from common.client.base_skyflow import BaseSkyflowImpl + + +class ConnectionCapable(ABC): + @abstractmethod + def add_connection_config(self, config): + raise NotImplementedError + + @abstractmethod + def remove_connection_config(self, connection_id): + raise NotImplementedError + + @abstractmethod + def update_connection_config(self, config): + raise NotImplementedError + + @abstractmethod + def get_connection_config(self, connection_id): + raise NotImplementedError + + @abstractmethod + def connection(self, connection_id=None): + raise NotImplementedError + + +class DetectCapable(ABC): + @abstractmethod + def detect(self, vault_id=None): + raise NotImplementedError + + +class ConnectionMixin(ConnectionCapable): + + def add_connection_config(self, config): + builder = self._get_builder() + builder._require_connections() + builder._add_connection_config(config) + return self + + def remove_connection_config(self, connection_id): + builder = self._get_builder() + builder._require_connections() + builder.remove_connection_config(connection_id) + return self + + def update_connection_config(self, config): + builder = self._get_builder() + builder._require_connections() + builder.update_connection_config(config) + return self + + def get_connection_config(self, connection_id): + builder = self._get_builder() + builder._require_connections() + return builder.get_connection_config(connection_id).get(OptionField.VAULT_CLIENT).get_config() + + def connection(self, connection_id=None): + builder = self._get_builder() + builder._require_connections() + connection_config = builder.get_connection_config(connection_id) + return connection_config.get(OptionField.CONTROLLER) + + +class DetectMixin(DetectCapable): + + def detect(self, vault_id=None): + builder = self._get_builder() + builder._require_detect() + vault_config = builder.get_vault_config(vault_id) + return vault_config.get(OptionField.DETECT_CONTROLLER) + + +def make_skyflow_class(*, vault_client_cls, vault_controller_cls, skyflow_messages, + validate_vault_config=None, validate_update_vault_config=None, + validate_log_level=None, validate_credentials=None, + logger_cls=_CommonLogger, default_log_level=_CommonLogLevel.ERROR, + connection_cls=None, detect_cls=None, + validate_connection_config=None, validate_update_connection_config=None, + set_active_log_level=None): + + if connection_cls is not None and (validate_connection_config is None or validate_update_connection_config is None): + raise ValueError("connection_cls requires validate_connection_config and validate_update_connection_config") + + validate_vault_config = validate_vault_config or partial(_common_validate_vault_config, messages=skyflow_messages) + validate_update_vault_config = validate_update_vault_config or partial(_common_validate_update_vault_config, messages=skyflow_messages) + validate_log_level = validate_log_level or partial(_common_validate_log_level, messages=skyflow_messages) + validate_credentials = validate_credentials or partial(_common_validate_credentials, messages=skyflow_messages) + + builder_attrs = { + '_vault_client_cls': vault_client_cls, + '_vault_controller_cls': vault_controller_cls, + '_connection_cls': connection_cls, + '_detect_cls': detect_cls, + '_logger_cls': logger_cls, + '_default_log_level': default_log_level, + '_skyflow_messages': skyflow_messages, + '_validate_vault_config': staticmethod(validate_vault_config), + '_validate_update_vault_config': staticmethod(validate_update_vault_config), + '_validate_connection_config': staticmethod(validate_connection_config) if validate_connection_config else None, + '_validate_update_connection_config': staticmethod(validate_update_connection_config) if validate_update_connection_config else None, + '_validate_log_level': staticmethod(validate_log_level), + '_validate_credentials': staticmethod(validate_credentials), + '_set_active_log_level': staticmethod(set_active_log_level) if set_active_log_level else None, + } + variant_builder = type('Builder', (BaseSkyflowImpl.Builder,), builder_attrs) + + bases = [BaseSkyflowImpl] + if connection_cls is not None: + bases.append(ConnectionMixin) + if detect_cls is not None: + bases.append(DetectMixin) + + variant_skyflow = type('Skyflow', tuple(bases), {'Builder': variant_builder}) + variant_builder._skyflow_cls = variant_skyflow + return variant_skyflow diff --git a/common/errors/__init__.py b/common/errors/__init__.py new file mode 100644 index 00000000..17a2fe39 --- /dev/null +++ b/common/errors/__init__.py @@ -0,0 +1 @@ +from ._skyflow_error import SkyflowError diff --git a/skyflow/error/_skyflow_error.py b/common/errors/_skyflow_error.py similarity index 93% rename from skyflow/error/_skyflow_error.py rename to common/errors/_skyflow_error.py index cda064e2..8ee07694 100644 --- a/skyflow/error/_skyflow_error.py +++ b/common/errors/_skyflow_error.py @@ -1,4 +1,4 @@ -from skyflow.utils import SkyflowMessages +from common.utils import SkyflowMessages class SkyflowError(Exception): def __init__(self, diff --git a/skyflow/service_account/client/__init__.py b/common/generated/__init__.py similarity index 100% rename from skyflow/service_account/client/__init__.py rename to common/generated/__init__.py diff --git a/common/generated/requirements.txt b/common/generated/requirements.txt new file mode 100644 index 00000000..e80f640a --- /dev/null +++ b/common/generated/requirements.txt @@ -0,0 +1,4 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2 +typing_extensions>= 4.0.0 diff --git a/common/generated/rest/__init__.py b/common/generated/rest/__init__.py new file mode 100644 index 00000000..2b7e1ccf --- /dev/null +++ b/common/generated/rest/__init__.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .types import GooglerpcStatus, ProtobufAny, V1GetAuthTokenResponse +from .errors import BadRequestError, NotFoundError, UnauthorizedError +from . import authentication +from .client import AsyncSkyflowAuth, SkyflowAuth +from .environment import SkyflowAuthEnvironment +from .version import __version__ + +__all__ = [ + "AsyncSkyflowAuth", + "BadRequestError", + "GooglerpcStatus", + "NotFoundError", + "ProtobufAny", + "SkyflowAuth", + "SkyflowAuthEnvironment", + "UnauthorizedError", + "V1GetAuthTokenResponse", + "__version__", + "authentication", +] diff --git a/skyflow/generated/rest/authentication/__init__.py b/common/generated/rest/authentication/__init__.py similarity index 100% rename from skyflow/generated/rest/authentication/__init__.py rename to common/generated/rest/authentication/__init__.py diff --git a/common/generated/rest/authentication/client.py b/common/generated/rest/authentication/client.py new file mode 100644 index 00000000..9653b6d3 --- /dev/null +++ b/common/generated/rest/authentication/client.py @@ -0,0 +1,181 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.v_1_get_auth_token_response import V1GetAuthTokenResponse +from .raw_client import AsyncRawAuthenticationClient, RawAuthenticationClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class AuthenticationClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawAuthenticationClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawAuthenticationClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawAuthenticationClient + """ + return self._raw_client + + def authentication_service_get_auth_token( + self, + *, + grant_type: str, + assertion: str, + subject_token: typing.Optional[str] = OMIT, + subject_token_type: typing.Optional[str] = OMIT, + requested_token_use: typing.Optional[str] = OMIT, + scope: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1GetAuthTokenResponse: + """ +

Generates a Bearer Token to authenticate with Skyflow. This method doesn't require the Authorization header.

Note: For recommended ways to authenticate, see API authentication.

+ + Parameters + ---------- + grant_type : str + Grant type of the request. Set this to `urn:ietf:params:oauth:grant-type:jwt-bearer`. + + assertion : str + User-signed JWT token that contains the following fields:
+ + subject_token : typing.Optional[str] + Subject token. + + subject_token_type : typing.Optional[str] + Subject token type. + + requested_token_use : typing.Optional[str] + Token use type. Either `delegation` or `impersonation`. + + scope : typing.Optional[str] + Subset of available roles to associate with the requested token. Uses the format "role:\ role:\". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1GetAuthTokenResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + token="YOUR_TOKEN", + ) + client.authentication.authentication_service_get_auth_token( + grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion="eyLhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoiY29tcGFueSIsImV4cCI6MTYxNTE5MzgwNywiaWF0IjoxNjE1MTY1MDQwLCJhdWQiOiKzb21lYXVkaWVuY2UifQ.4pcPyMDQ9o1PSyXnrXCjTwXyr4BSezdI1AVTmud2fU3", + ) + """ + _response = self._raw_client.authentication_service_get_auth_token( + grant_type=grant_type, + assertion=assertion, + subject_token=subject_token, + subject_token_type=subject_token_type, + requested_token_use=requested_token_use, + scope=scope, + request_options=request_options, + ) + return _response.data + + +class AsyncAuthenticationClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawAuthenticationClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawAuthenticationClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawAuthenticationClient + """ + return self._raw_client + + async def authentication_service_get_auth_token( + self, + *, + grant_type: str, + assertion: str, + subject_token: typing.Optional[str] = OMIT, + subject_token_type: typing.Optional[str] = OMIT, + requested_token_use: typing.Optional[str] = OMIT, + scope: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1GetAuthTokenResponse: + """ +

Generates a Bearer Token to authenticate with Skyflow. This method doesn't require the Authorization header.

Note: For recommended ways to authenticate, see API authentication.

+ + Parameters + ---------- + grant_type : str + Grant type of the request. Set this to `urn:ietf:params:oauth:grant-type:jwt-bearer`. + + assertion : str + User-signed JWT token that contains the following fields:
+ + subject_token : typing.Optional[str] + Subject token. + + subject_token_type : typing.Optional[str] + Subject token type. + + requested_token_use : typing.Optional[str] + Token use type. Either `delegation` or `impersonation`. + + scope : typing.Optional[str] + Subset of available roles to associate with the requested token. Uses the format "role:\ role:\". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1GetAuthTokenResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.authentication.authentication_service_get_auth_token( + grant_type="urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion="eyLhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaXNzIjoiY29tcGFueSIsImV4cCI6MTYxNTE5MzgwNywiaWF0IjoxNjE1MTY1MDQwLCJhdWQiOiKzb21lYXVkaWVuY2UifQ.4pcPyMDQ9o1PSyXnrXCjTwXyr4BSezdI1AVTmud2fU3", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.authentication_service_get_auth_token( + grant_type=grant_type, + assertion=assertion, + subject_token=subject_token, + subject_token_type=subject_token_type, + requested_token_use=requested_token_use, + scope=scope, + request_options=request_options, + ) + return _response.data diff --git a/common/generated/rest/authentication/raw_client.py b/common/generated/rest/authentication/raw_client.py new file mode 100644 index 00000000..bb1c2ed7 --- /dev/null +++ b/common/generated/rest/authentication/raw_client.py @@ -0,0 +1,241 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..errors.bad_request_error import BadRequestError +from ..errors.not_found_error import NotFoundError +from ..errors.unauthorized_error import UnauthorizedError +from ..types.v_1_get_auth_token_response import V1GetAuthTokenResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawAuthenticationClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def authentication_service_get_auth_token( + self, + *, + grant_type: str, + assertion: str, + subject_token: typing.Optional[str] = OMIT, + subject_token_type: typing.Optional[str] = OMIT, + requested_token_use: typing.Optional[str] = OMIT, + scope: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1GetAuthTokenResponse]: + """ +

Generates a Bearer Token to authenticate with Skyflow. This method doesn't require the Authorization header.

Note: For recommended ways to authenticate, see API authentication.

+ + Parameters + ---------- + grant_type : str + Grant type of the request. Set this to `urn:ietf:params:oauth:grant-type:jwt-bearer`. + + assertion : str + User-signed JWT token that contains the following fields:
  • iss: Issuer of the JWT.
  • key: Unique identifier for the key.
  • aud: Recipient the JWT is intended for.
  • exp: Time the JWT expires.
  • sub: Subject of the JWT.
  • ctx: (Optional) Value for Context-aware authorization.
+ + subject_token : typing.Optional[str] + Subject token. + + subject_token_type : typing.Optional[str] + Subject token type. + + requested_token_use : typing.Optional[str] + Token use type. Either `delegation` or `impersonation`. + + scope : typing.Optional[str] + Subset of available roles to associate with the requested token. Uses the format "role:\ role:\". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1GetAuthTokenResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v1/auth/sa/oauth/token", + method="POST", + json={ + "grant_type": grant_type, + "assertion": assertion, + "subject_token": subject_token, + "subject_token_type": subject_token_type, + "requested_token_use": requested_token_use, + "scope": scope, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1GetAuthTokenResponse, + parse_obj_as( + type_=V1GetAuthTokenResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Dict[str, typing.Optional[typing.Any]], + parse_obj_as( + type_=typing.Dict[str, typing.Optional[typing.Any]], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Dict[str, typing.Optional[typing.Any]], + parse_obj_as( + type_=typing.Dict[str, typing.Optional[typing.Any]], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Dict[str, typing.Optional[typing.Any]], + parse_obj_as( + type_=typing.Dict[str, typing.Optional[typing.Any]], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawAuthenticationClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def authentication_service_get_auth_token( + self, + *, + grant_type: str, + assertion: str, + subject_token: typing.Optional[str] = OMIT, + subject_token_type: typing.Optional[str] = OMIT, + requested_token_use: typing.Optional[str] = OMIT, + scope: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1GetAuthTokenResponse]: + """ +

Generates a Bearer Token to authenticate with Skyflow. This method doesn't require the Authorization header.

Note: For recommended ways to authenticate, see API authentication.

+ + Parameters + ---------- + grant_type : str + Grant type of the request. Set this to `urn:ietf:params:oauth:grant-type:jwt-bearer`. + + assertion : str + User-signed JWT token that contains the following fields:
  • iss: Issuer of the JWT.
  • key: Unique identifier for the key.
  • aud: Recipient the JWT is intended for.
  • exp: Time the JWT expires.
  • sub: Subject of the JWT.
  • ctx: (Optional) Value for Context-aware authorization.
+ + subject_token : typing.Optional[str] + Subject token. + + subject_token_type : typing.Optional[str] + Subject token type. + + requested_token_use : typing.Optional[str] + Token use type. Either `delegation` or `impersonation`. + + scope : typing.Optional[str] + Subset of available roles to associate with the requested token. Uses the format "role:\ role:\". + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1GetAuthTokenResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v1/auth/sa/oauth/token", + method="POST", + json={ + "grant_type": grant_type, + "assertion": assertion, + "subject_token": subject_token, + "subject_token_type": subject_token_type, + "requested_token_use": requested_token_use, + "scope": scope, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1GetAuthTokenResponse, + parse_obj_as( + type_=V1GetAuthTokenResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + typing.Dict[str, typing.Optional[typing.Any]], + parse_obj_as( + type_=typing.Dict[str, typing.Optional[typing.Any]], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Dict[str, typing.Optional[typing.Any]], + parse_obj_as( + type_=typing.Dict[str, typing.Optional[typing.Any]], # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + typing.Dict[str, typing.Optional[typing.Any]], + parse_obj_as( + type_=typing.Dict[str, typing.Optional[typing.Any]], # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/common/generated/rest/client.py b/common/generated/rest/client.py new file mode 100644 index 00000000..b210e4da --- /dev/null +++ b/common/generated/rest/client.py @@ -0,0 +1,153 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .authentication.client import AsyncAuthenticationClient, AuthenticationClient +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .environment import SkyflowAuthEnvironment + + +class SkyflowAuth: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : SkyflowAuthEnvironment + The environment to use for requests from the client. from .environment import SkyflowAuthEnvironment + + + + Defaults to SkyflowAuthEnvironment.PRODUCTION + + + + token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + token="YOUR_TOKEN", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: SkyflowAuthEnvironment = SkyflowAuthEnvironment.PRODUCTION, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + token=token, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.authentication = AuthenticationClient(client_wrapper=self._client_wrapper) + + +class AsyncSkyflowAuth: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : typing.Optional[str] + The base url to use for requests from the client. + + environment : SkyflowAuthEnvironment + The environment to use for requests from the client. from .environment import SkyflowAuthEnvironment + + + + Defaults to SkyflowAuthEnvironment.PRODUCTION + + + + token : typing.Optional[typing.Union[str, typing.Callable[[], str]]] + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + token="YOUR_TOKEN", + ) + """ + + def __init__( + self, + *, + base_url: typing.Optional[str] = None, + environment: SkyflowAuthEnvironment = SkyflowAuthEnvironment.PRODUCTION, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=_get_base_url(base_url=base_url, environment=environment), + token=token, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.authentication = AsyncAuthenticationClient(client_wrapper=self._client_wrapper) + + +def _get_base_url(*, base_url: typing.Optional[str] = None, environment: SkyflowAuthEnvironment) -> str: + if base_url is not None: + return base_url + elif environment is not None: + return environment.value + else: + raise Exception("Please pass in either base_url or environment to construct the client") diff --git a/skyflow/generated/rest/core/__init__.py b/common/generated/rest/core/__init__.py similarity index 100% rename from skyflow/generated/rest/core/__init__.py rename to common/generated/rest/core/__init__.py diff --git a/skyflow/generated/rest/core/api_error.py b/common/generated/rest/core/api_error.py similarity index 100% rename from skyflow/generated/rest/core/api_error.py rename to common/generated/rest/core/api_error.py diff --git a/common/generated/rest/core/client_wrapper.py b/common/generated/rest/core/client_wrapper.py new file mode 100644 index 00000000..9588d762 --- /dev/null +++ b/common/generated/rest/core/client_wrapper.py @@ -0,0 +1,86 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient + + +class BaseClientWrapper: + def __init__( + self, + *, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + ): + self._token = token + self._headers = headers + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "skyflow.generated.rest", + "X-Fern-SDK-Version": "0.0.9", + **(self.get_custom_headers() or {}), + } + token = self._get_token() + if token is not None: + headers["Authorization"] = f"Bearer {token}" + return headers + + def _get_token(self) -> typing.Optional[str]: + if isinstance(self._token, str) or self._token is None: + return self._token + else: + return self._token() + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.Client, + ): + super().__init__(token=token, headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__(token=token, headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) diff --git a/skyflow/generated/rest/core/datetime_utils.py b/common/generated/rest/core/datetime_utils.py similarity index 100% rename from skyflow/generated/rest/core/datetime_utils.py rename to common/generated/rest/core/datetime_utils.py diff --git a/skyflow/generated/rest/core/file.py b/common/generated/rest/core/file.py similarity index 100% rename from skyflow/generated/rest/core/file.py rename to common/generated/rest/core/file.py diff --git a/skyflow/generated/rest/core/force_multipart.py b/common/generated/rest/core/force_multipart.py similarity index 100% rename from skyflow/generated/rest/core/force_multipart.py rename to common/generated/rest/core/force_multipart.py diff --git a/skyflow/generated/rest/core/http_client.py b/common/generated/rest/core/http_client.py similarity index 100% rename from skyflow/generated/rest/core/http_client.py rename to common/generated/rest/core/http_client.py diff --git a/skyflow/generated/rest/core/http_response.py b/common/generated/rest/core/http_response.py similarity index 100% rename from skyflow/generated/rest/core/http_response.py rename to common/generated/rest/core/http_response.py diff --git a/skyflow/generated/rest/core/jsonable_encoder.py b/common/generated/rest/core/jsonable_encoder.py similarity index 100% rename from skyflow/generated/rest/core/jsonable_encoder.py rename to common/generated/rest/core/jsonable_encoder.py diff --git a/skyflow/generated/rest/core/pydantic_utilities.py b/common/generated/rest/core/pydantic_utilities.py similarity index 100% rename from skyflow/generated/rest/core/pydantic_utilities.py rename to common/generated/rest/core/pydantic_utilities.py diff --git a/skyflow/generated/rest/core/query_encoder.py b/common/generated/rest/core/query_encoder.py similarity index 100% rename from skyflow/generated/rest/core/query_encoder.py rename to common/generated/rest/core/query_encoder.py diff --git a/skyflow/generated/rest/core/remove_none_from_dict.py b/common/generated/rest/core/remove_none_from_dict.py similarity index 100% rename from skyflow/generated/rest/core/remove_none_from_dict.py rename to common/generated/rest/core/remove_none_from_dict.py diff --git a/skyflow/generated/rest/core/request_options.py b/common/generated/rest/core/request_options.py similarity index 100% rename from skyflow/generated/rest/core/request_options.py rename to common/generated/rest/core/request_options.py diff --git a/skyflow/generated/rest/core/serialization.py b/common/generated/rest/core/serialization.py similarity index 100% rename from skyflow/generated/rest/core/serialization.py rename to common/generated/rest/core/serialization.py diff --git a/common/generated/rest/environment.py b/common/generated/rest/environment.py new file mode 100644 index 00000000..b1c13812 --- /dev/null +++ b/common/generated/rest/environment.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import enum + + +class SkyflowAuthEnvironment(enum.Enum): + PRODUCTION = "https://manage.skyflowapis.com" + SANDBOX = "https://manage.skyflowapis-preview.com" diff --git a/common/generated/rest/errors/__init__.py b/common/generated/rest/errors/__init__.py new file mode 100644 index 00000000..fdf6196c --- /dev/null +++ b/common/generated/rest/errors/__init__.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .bad_request_error import BadRequestError +from .not_found_error import NotFoundError +from .unauthorized_error import UnauthorizedError + +__all__ = ["BadRequestError", "NotFoundError", "UnauthorizedError"] diff --git a/common/generated/rest/errors/bad_request_error.py b/common/generated/rest/errors/bad_request_error.py new file mode 100644 index 00000000..c5d0db48 --- /dev/null +++ b/common/generated/rest/errors/bad_request_error.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class BadRequestError(ApiError): + def __init__( + self, + body: typing.Dict[str, typing.Optional[typing.Any]], + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/common/generated/rest/errors/not_found_error.py b/common/generated/rest/errors/not_found_error.py new file mode 100644 index 00000000..66307415 --- /dev/null +++ b/common/generated/rest/errors/not_found_error.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class NotFoundError(ApiError): + def __init__( + self, + body: typing.Dict[str, typing.Optional[typing.Any]], + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/common/generated/rest/errors/unauthorized_error.py b/common/generated/rest/errors/unauthorized_error.py new file mode 100644 index 00000000..3d58c2e6 --- /dev/null +++ b/common/generated/rest/errors/unauthorized_error.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.api_error import ApiError + + +class UnauthorizedError(ApiError): + def __init__( + self, + body: typing.Dict[str, typing.Optional[typing.Any]], + headers: typing.Optional[typing.Dict[str, str]] = None, + ): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/skyflow/generated/rest/py.typed b/common/generated/rest/py.typed similarity index 100% rename from skyflow/generated/rest/py.typed rename to common/generated/rest/py.typed diff --git a/common/generated/rest/types/__init__.py b/common/generated/rest/types/__init__.py new file mode 100644 index 00000000..8a9140f5 --- /dev/null +++ b/common/generated/rest/types/__init__.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .googlerpc_status import GooglerpcStatus +from .protobuf_any import ProtobufAny +from .v_1_get_auth_token_response import V1GetAuthTokenResponse + +__all__ = ["GooglerpcStatus", "ProtobufAny", "V1GetAuthTokenResponse"] diff --git a/skyflow/generated/rest/types/googlerpc_status.py b/common/generated/rest/types/googlerpc_status.py similarity index 100% rename from skyflow/generated/rest/types/googlerpc_status.py rename to common/generated/rest/types/googlerpc_status.py diff --git a/skyflow/generated/rest/types/protobuf_any.py b/common/generated/rest/types/protobuf_any.py similarity index 100% rename from skyflow/generated/rest/types/protobuf_any.py rename to common/generated/rest/types/protobuf_any.py diff --git a/skyflow/generated/rest/types/v_1_get_auth_token_response.py b/common/generated/rest/types/v_1_get_auth_token_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_get_auth_token_response.py rename to common/generated/rest/types/v_1_get_auth_token_response.py diff --git a/common/generated/rest/version.py b/common/generated/rest/version.py new file mode 100644 index 00000000..4f3bb47a --- /dev/null +++ b/common/generated/rest/version.py @@ -0,0 +1,6 @@ +# NOTE: hand-patched, not Fern-generated content. Fern originally emitted a runtime +# metadata.version("skyflow.generated.rest") lookup here, but this code is bundled into the +# skyflow (v2) and v3 wheels via a build_py hook rather than published under that distribution +# name, so the lookup always raised PackageNotFoundError on import. Hardcoded until the Fern +# generator config (skyflow-fern-config) is updated to stop emitting a runtime lookup here. +__version__ = "0.0.9" diff --git a/common/service_account/__init__.py b/common/service_account/__init__.py new file mode 100644 index 00000000..58a05b45 --- /dev/null +++ b/common/service_account/__init__.py @@ -0,0 +1 @@ +from ._utils import generate_bearer_token, generate_bearer_token_from_creds, is_expired, generate_signed_data_tokens, generate_signed_data_tokens_from_creds diff --git a/common/service_account/_utils.py b/common/service_account/_utils.py new file mode 100644 index 00000000..1c9bf945 --- /dev/null +++ b/common/service_account/_utils.py @@ -0,0 +1,247 @@ +import json +import datetime +import re +import time +import jwt +from urllib.parse import urlparse +from common.errors import SkyflowError +from common.service_account.client.auth_client import AuthClient +from common.utils.logger import log_info, log_error_log +from common.utils import get_base_url, format_scope, SkyflowMessages +from common.utils.constants import JWT, CredentialField, JwtField, OptionField, ResponseField +from common.generated.rest.errors.unauthorized_error import UnauthorizedError +from common.utils import is_valid_url +from common.utils.constants import CTX_KEY_REGEX + + +invalid_input_error_code = SkyflowMessages.ErrorCodes.INVALID_INPUT.value + +_CTX_KEY_PATTERN = re.compile(CTX_KEY_REGEX) + +_SNAKE_TO_CAMEL_CRED_MAP = { + 'private_key': CredentialField.PRIVATE_KEY, + 'client_id': CredentialField.CLIENT_ID, + 'key_id': CredentialField.KEY_ID, + 'token_uri': CredentialField.TOKEN_URI, + 'client_name': CredentialField.CLIENT_NAME, +} + + +def _normalize_credentials(credentials): + return {_SNAKE_TO_CAMEL_CRED_MAP.get(k, k): v for k, v in credentials.items()} + + +def _validate_and_resolve_ctx(ctx): + """Validate ctx value and return resolved value for JWT claims. + Returns None if ctx should be omitted, the value if valid, or raises SkyflowError if invalid. + """ + if ctx is None: + return None + if isinstance(ctx, str): + if ctx.strip() == '': + return None + return ctx + if isinstance(ctx, dict): + if len(ctx) == 0: + return None + for key in ctx: + if not isinstance(key, str) or not _CTX_KEY_PATTERN.match(key): + raise SkyflowError( + SkyflowMessages.Error.INVALID_CTX_MAP_KEY.value.format(key), + invalid_input_error_code + ) + return ctx + if isinstance(ctx, (bool, int, float)): + return ctx + raise SkyflowError( + SkyflowMessages.Error.INVALID_CTX_TYPE.value, + invalid_input_error_code + ) + +def is_expired(token, logger = None): + if token is None: + return True + if len(token) == 0: + log_error_log(SkyflowMessages.ErrorLogs.INVALID_BEARER_TOKEN.value) + return True + + try: + decoded = jwt.decode( + token, options={OptionField.VERIFY_SIGNATURE: False, OptionField.VERIFY_AUD: False}) + if time.time() >= decoded[JwtField.EXP]: + log_info(SkyflowMessages.Info.BEARER_TOKEN_EXPIRED.value, logger) + log_error_log(SkyflowMessages.ErrorLogs.INVALID_BEARER_TOKEN.value) + return True + return False + except jwt.ExpiredSignatureError: + return True + except Exception: + log_error_log(SkyflowMessages.Error.JWT_DECODE_ERROR.value, logger) + return True + +def generate_bearer_token(credentials_file_path, options = None, logger = None): + log_info(SkyflowMessages.Info.GET_BEARER_TOKEN_TRIGGERED.value, logger) + try: + with open(credentials_file_path, 'r') as credentials_file: + try: + credentials = json.load(credentials_file) + except Exception: + log_error_log(SkyflowMessages.ErrorLogs.INVALID_CREDENTIALS_FILE.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.FILE_INVALID_JSON.value.format(credentials_file_path), invalid_input_error_code) + except SkyflowError: + raise + except Exception: + raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIAL_FILE_PATH.value, invalid_input_error_code) + result = get_service_account_token(credentials, options, logger) + return result + +def generate_bearer_token_from_creds(credentials, options = None, logger = None): + log_info(SkyflowMessages.Info.GET_BEARER_TOKEN_TRIGGERED.value, logger) + credentials = credentials.strip() + try: + json_credentials = json.loads(credentials.replace('\n', '\\n')) + except Exception: + raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIALS_STRING.value, invalid_input_error_code) + result = get_service_account_token(json_credentials, options, logger) + return result + +def get_service_account_token(credentials, options, logger): + credentials = _normalize_credentials(credentials) + try: + private_key = credentials[CredentialField.PRIVATE_KEY] + except KeyError: + log_error_log(SkyflowMessages.ErrorLogs.PRIVATE_KEY_IS_REQUIRED.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.MISSING_PRIVATE_KEY.value, invalid_input_error_code) + try: + client_id = credentials[CredentialField.CLIENT_ID] + except KeyError: + log_error_log(SkyflowMessages.ErrorLogs.CLIENT_ID_IS_REQUIRED.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.MISSING_CLIENT_ID.value, invalid_input_error_code) + try: + key_id = credentials[CredentialField.KEY_ID] + except KeyError: + log_error_log(SkyflowMessages.ErrorLogs.KEY_ID_IS_REQUIRED.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.MISSING_KEY_ID.value, invalid_input_error_code) + try: + token_uri = credentials[CredentialField.TOKEN_URI] + except KeyError: + log_error_log(SkyflowMessages.ErrorLogs.TOKEN_URI_IS_REQUIRED.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.MISSING_TOKEN_URI.value, invalid_input_error_code) + + if not isinstance(token_uri, str) or not is_valid_url(token_uri): + log_error_log(SkyflowMessages.ErrorLogs.INVALID_TOKEN_URI.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_URI.value, invalid_input_error_code) + + if options and CredentialField.TOKEN_URI_OPTION in options: + token_uri = options[CredentialField.TOKEN_URI_OPTION] + if not isinstance(token_uri, str) or not is_valid_url(token_uri): + log_error_log(SkyflowMessages.ErrorLogs.INVALID_TOKEN_URI.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_URI.value, invalid_input_error_code) + + signed_token = get_signed_jwt(options, client_id, key_id, token_uri, private_key, logger) + base_url = get_base_url(token_uri) + auth_client = AuthClient(base_url) + auth_api = auth_client.get_auth_api() + + formatted_scope = None + if options and OptionField.ROLE_IDS in options: + formatted_scope = format_scope(options.get(OptionField.ROLE_IDS)) + + try: + response = auth_api.authentication_service_get_auth_token(assertion = signed_token, + grant_type=JWT.GRANT_TYPE_JWT_BEARER, + scope=formatted_scope) + log_info(SkyflowMessages.Info.GET_BEARER_TOKEN_SUCCESS.value, logger) + except UnauthorizedError: + log_error_log(SkyflowMessages.ErrorLogs.UNAUTHORIZED_ERROR_IN_GETTING_BEARER_TOKEN.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.UNAUTHORIZED_ERROR_IN_GETTING_BEARER_TOKEN.value, invalid_input_error_code) + except Exception: + log_error_log(SkyflowMessages.ErrorLogs.FAILED_TO_GET_BEARER_TOKEN.value, logger=logger) + raise SkyflowError(SkyflowMessages.Error.FAILED_TO_GET_BEARER_TOKEN.value, invalid_input_error_code) + return response.access_token, response.token_type + +def get_signed_jwt(options, client_id, key_id, token_uri, private_key, logger): + payload = { + JwtField.ISS: client_id, + JwtField.KEY: key_id, + JwtField.AUD: token_uri, + JwtField.SUB: client_id, + JwtField.EXP: datetime.datetime.utcnow() + datetime.timedelta(minutes=60) + } + if options and OptionField.CTX in options: + resolved_ctx = _validate_and_resolve_ctx(options.get(OptionField.CTX)) + if resolved_ctx is not None: + payload[JwtField.CTX] = resolved_ctx + try: + return jwt.encode(payload=payload, key=private_key, algorithm=JWT.ALGORITHM_RS256) + except Exception: + raise SkyflowError(SkyflowMessages.Error.JWT_INVALID_FORMAT.value, invalid_input_error_code) + + + +def get_signed_tokens(credentials_obj, options): + options = options if options is not None else {} + credentials_obj = _normalize_credentials(credentials_obj) + expiry_time = int(time.time()) + options.get(OptionField.TIME_TO_LIVE, 60) + prefix = JWT.SIGNED_TOKEN_PREFIX + + token_uri = credentials_obj.get(CredentialField.TOKEN_URI) + if not isinstance(token_uri, str) or not is_valid_url(token_uri): + log_error_log(SkyflowMessages.ErrorLogs.INVALID_TOKEN_URI.value) + raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_URI.value, invalid_input_error_code) + + resolved_ctx = None + if OptionField.CTX in options: + resolved_ctx = _validate_and_resolve_ctx(options[OptionField.CTX]) + + results = [] + if options and options.get(OptionField.DATA_TOKENS): + for token in options[OptionField.DATA_TOKENS]: + claims = { + JwtField.ISS: JWT.ISSUER_SDK, + JwtField.KEY: credentials_obj.get(CredentialField.KEY_ID), + JwtField.EXP: expiry_time, + JwtField.SUB: credentials_obj.get(CredentialField.CLIENT_ID), + JwtField.TOK: token, + JwtField.IAT: int(time.time()), + } + if resolved_ctx is not None: + claims[JwtField.CTX] = resolved_ctx + private_key = credentials_obj.get(CredentialField.PRIVATE_KEY) + try: + signed_jwt = jwt.encode(claims, private_key, algorithm=JWT.ALGORITHM_RS256) + except Exception: + raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIALS.value, invalid_input_error_code) + results.append(get_signed_data_token_response_object(prefix + signed_jwt, token)) + log_info(SkyflowMessages.Info.GET_SIGNED_DATA_TOKEN_SUCCESS.value) + return results + + +def generate_signed_data_tokens(credentials_file_path, options): + log_info(SkyflowMessages.Info.GET_SIGNED_DATA_TOKENS_TRIGGERED.value) + try: + with open(credentials_file_path, 'r') as credentials_file: + try: + credentials = json.load(credentials_file) + except Exception: + raise SkyflowError(SkyflowMessages.Error.FILE_INVALID_JSON.value.format(credentials_file_path), + invalid_input_error_code) + except SkyflowError: + raise + except Exception: + raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIAL_FILE_PATH.value, invalid_input_error_code) + return get_signed_tokens(credentials, options) + +def generate_signed_data_tokens_from_creds(credentials, options): + log_info(SkyflowMessages.Info.GET_SIGNED_DATA_TOKENS_TRIGGERED.value) + credentials = credentials.strip() + try: + json_credentials = json.loads(credentials.replace('\n', '\\n')) + except Exception: + log_error_log(SkyflowMessages.ErrorLogs.INVALID_CREDENTIALS_FILE.value) + raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIALS_STRING.value, invalid_input_error_code) + return get_signed_tokens(json_credentials, options) + + +def get_signed_data_token_response_object(signed_token, actual_token): + return actual_token, signed_token diff --git a/skyflow/vault/__init__.py b/common/service_account/client/__init__.py similarity index 100% rename from skyflow/vault/__init__.py rename to common/service_account/client/__init__.py diff --git a/common/service_account/client/auth_client.py b/common/service_account/client/auth_client.py new file mode 100644 index 00000000..fe7aabe8 --- /dev/null +++ b/common/service_account/client/auth_client.py @@ -0,0 +1,13 @@ +from common.generated.rest.client import SkyflowAuth +from common.utils.constants import OPTIONAL_TOKEN + +class AuthClient: + def __init__(self, url): + self.__url = url + self.__api_client = self.initialize_api_client() + + def initialize_api_client(self): + return SkyflowAuth(base_url=self.__url, token=OPTIONAL_TOKEN) + + def get_auth_api(self): + return self.__api_client.authentication diff --git a/common/setup.py b/common/setup.py new file mode 100644 index 00000000..fe37d04d --- /dev/null +++ b/common/setup.py @@ -0,0 +1,27 @@ +''' + Copyright (c) 2022 Skyflow, Inc. +''' +from setuptools import setup, find_packages + +setup( + # Never published to PyPI -- bundled into the v2/v3 wheels at build time (see their + # setup.py CustomBuildPy). Exists for local dev (`pip install -e ./common`) and tests/contract/. + name='skyflow-common', + version='0.0.0', + author='Skyflow', + author_email='service-ops@skyflow.com', + description='Internal shared code for the Skyflow Python SDK v2/v3 build variants. Not published independently.', + packages=find_packages(where='.', exclude=['tests*']), + package_data={ + 'common.generated.rest': ['py.typed'], + }, + install_requires=[ + 'pydantic >= 2.0.0', + 'typing-extensions >= 4.0.0', + 'PyJWT >= 2.12, < 3', + 'cryptography >= 44.0.2', + 'httpx >= 0.21.2', + 'python-dotenv >= 1.1.0, < 2', + ], + python_requires=">=3.9", +) diff --git a/skyflow/vault/client/__init__.py b/common/tests/__init__.py similarity index 100% rename from skyflow/vault/client/__init__.py rename to common/tests/__init__.py diff --git a/tests/client/__init__.py b/common/tests/client/__init__.py similarity index 100% rename from tests/client/__init__.py rename to common/tests/client/__init__.py diff --git a/common/tests/client/test_base_skyflow.py b/common/tests/client/test_base_skyflow.py new file mode 100644 index 00000000..f28e9416 --- /dev/null +++ b/common/tests/client/test_base_skyflow.py @@ -0,0 +1,215 @@ +import unittest + +from common.errors import SkyflowError +from common.utils import LogLevel, SkyflowMessages +from common.utils.logger import Logger +from common.client.base_skyflow import BaseSkyflow, BaseSkyflowImpl +from common.client.utils import make_skyflow_class + + +class FakeVaultClient: + def __init__(self, config): + self._config = dict(config) + self.credentials = None + self.logger = None + + def get_config(self): + return self._config + + def update_config(self, config): + self._config.update(config) + + def set_logger(self, log_level, logger): + self.logger = logger + + def set_common_skyflow_credentials(self, credentials): + self.credentials = credentials + + +class FakeVaultController: + def __init__(self, vault_client): + self.vault_client = vault_client + + +class FakeConnection: + def __init__(self, vault_client): + self.vault_client = vault_client + + +class FakeDetect: + def __init__(self, vault_client): + self.vault_client = vault_client + + +def _noop_validate(logger, config): + return True + + +def make_fake_skyflow(with_connections=False, with_detect=False): + kwargs = dict( + vault_client_cls=FakeVaultClient, + vault_controller_cls=FakeVaultController, + logger_cls=Logger, + default_log_level=LogLevel.ERROR, + skyflow_messages=SkyflowMessages, + validate_vault_config=_noop_validate, + validate_update_vault_config=_noop_validate, + validate_log_level=_noop_validate, + validate_credentials=_noop_validate, + ) + if with_connections: + kwargs.update( + connection_cls=FakeConnection, + validate_connection_config=_noop_validate, + validate_update_connection_config=_noop_validate, + ) + if with_detect: + kwargs['detect_cls'] = FakeDetect + return make_skyflow_class(**kwargs) + + +VAULT_CONFIG = {"vault_id": "v1", "cluster_id": "c1", "credentials": {"token": "t"}} + + +class TestMakeSkyflowClassBasics(unittest.TestCase): + def test_build_returns_instance_of_the_produced_class_not_the_template(self): + """Regression pin: build()/builder() must resolve to the specific class produced by + make_skyflow_class(), not the shared template -- two variants must never collide.""" + SkyflowA = make_fake_skyflow() + SkyflowB = make_fake_skyflow() + client_a = SkyflowA.builder().add_vault_config(VAULT_CONFIG).build() + self.assertIsInstance(client_a, SkyflowA) + self.assertNotIsInstance(client_a, SkyflowB) + + def test_two_produced_classes_do_not_share_hooks(self): + SkyflowWithDetect = make_fake_skyflow(with_detect=True) + SkyflowWithoutDetect = make_fake_skyflow(with_detect=False) + self.assertIsNotNone(SkyflowWithDetect.Builder._detect_cls) + self.assertIsNone(SkyflowWithoutDetect.Builder._detect_cls) + + def test_vault_config_crud(self): + Skyflow = make_fake_skyflow() + builder = Skyflow.builder() + builder.add_vault_config(VAULT_CONFIG) + client = builder.build() + + vault_config = client.get_vault_config("v1") + self.assertEqual(vault_config.get("vault_id"), "v1") + + updated = dict(VAULT_CONFIG) + updated["cluster_id"] = "c2" + client.update_vault_config(updated) + self.assertEqual(client.get_vault_config("v1").get("cluster_id"), "c2") + + client.remove_vault_config("v1") + with self.assertRaises(SkyflowError): + client.get_vault_config("v1") + + def test_vault_returns_the_controller(self): + Skyflow = make_fake_skyflow() + client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build() + self.assertIsInstance(client.vault("v1"), FakeVaultController) + + def test_add_skyflow_credentials_and_update(self): + Skyflow = make_fake_skyflow() + client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build() + client.add_skyflow_credentials({"token": "a"}) + client.update_skyflow_credentials({"token": "b"}) + # no assertion error means both delegate correctly to the same underlying builder path + + def test_set_get_and_deprecated_update_log_level(self): + Skyflow = make_fake_skyflow() + client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build() + client.set_log_level(LogLevel.INFO) + self.assertEqual(client.get_log_level(), LogLevel.INFO) + + client.update_log_level(LogLevel.WARN) + self.assertEqual(client.get_log_level(), LogLevel.WARN) + + +class TestConnectionAndDetectGating(unittest.TestCase): + def test_connection_methods_do_not_exist_when_connection_cls_not_supplied(self): + """connection()/add_connection_config()/etc. come from ConnectionMixin, which + make_skyflow_class() only adds to the produced class's bases when connection_cls is + given -- when it's not, the methods are genuinely absent, not just guarded.""" + Skyflow = make_fake_skyflow() + client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build() + self.assertFalse(hasattr(client, "connection")) + with self.assertRaises(AttributeError): + client.connection() + with self.assertRaises(AttributeError): + client.add_connection_config({}) + with self.assertRaises(AttributeError): + client.remove_connection_config("x") + with self.assertRaises(AttributeError): + client.update_connection_config({}) + with self.assertRaises(AttributeError): + client.get_connection_config("x") + + def test_detect_does_not_exist_when_detect_cls_not_supplied(self): + Skyflow = make_fake_skyflow() + client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build() + self.assertFalse(hasattr(client, "detect")) + with self.assertRaises(AttributeError): + client.detect() + + def test_connection_config_crud_when_supplied(self): + Skyflow = make_fake_skyflow(with_connections=True) + connection_config = {"connection_id": "conn1", "connection_url": "https://x", "credentials": {"token": "t"}} + client = Skyflow.builder().add_connection_config(connection_config).build() + + self.assertIsInstance(client.connection("conn1"), FakeConnection) + + updated = dict(connection_config) + updated["connection_url"] = "https://y" + client.update_connection_config(updated) + self.assertEqual(client.get_connection_config("conn1").get("connection_url"), "https://y") + + client.remove_connection_config("conn1") + with self.assertRaises(SkyflowError): + client.get_connection_config("conn1") + + def test_detect_returns_detect_controller_when_supplied(self): + Skyflow = make_fake_skyflow(with_detect=True) + client = Skyflow.builder().add_vault_config(VAULT_CONFIG).build() + self.assertIsInstance(client.detect("v1"), FakeDetect) + + def test_make_skyflow_class_requires_connection_validators_when_connection_cls_given(self): + with self.assertRaises(ValueError): + make_skyflow_class( + vault_client_cls=FakeVaultClient, + vault_controller_cls=FakeVaultController, + logger_cls=Logger, + default_log_level=LogLevel.ERROR, + skyflow_messages=SkyflowMessages, + validate_vault_config=_noop_validate, + validate_update_vault_config=_noop_validate, + validate_log_level=_noop_validate, + validate_credentials=_noop_validate, + connection_cls=FakeConnection, + # validate_connection_config/validate_update_connection_config omitted on purpose + ) + + +class TestBaseSkyflowInterface(unittest.TestCase): + def test_using_the_template_builder_directly_raises_with_a_clear_message(self): + with self.assertRaises(NotImplementedError) as ctx: + BaseSkyflowImpl.Builder() + self.assertIn("make_skyflow_class()", str(ctx.exception)) + self.assertIn("_vault_client_cls", str(ctx.exception)) + + def test_make_skyflow_class_produced_builder_constructs_fine(self): + Skyflow = make_fake_skyflow() + self.assertIsInstance(Skyflow.builder(), BaseSkyflowImpl.Builder) + + def test_instantiating_base_skyflow_impl_directly_raises(self): + with self.assertRaises(SkyflowError): + BaseSkyflowImpl(None) + + def test_instantiating_the_pure_interface_raises_type_error(self): + with self.assertRaises(TypeError): + BaseSkyflow() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/service_account/__init__.py b/common/tests/utils/__init__.py similarity index 100% rename from tests/service_account/__init__.py rename to common/tests/utils/__init__.py diff --git a/tests/utils/__init__.py b/common/tests/utils/validations/__init__.py similarity index 100% rename from tests/utils/__init__.py rename to common/tests/utils/validations/__init__.py diff --git a/common/tests/utils/validations/test__validations.py b/common/tests/utils/validations/test__validations.py new file mode 100644 index 00000000..b10fc008 --- /dev/null +++ b/common/tests/utils/validations/test__validations.py @@ -0,0 +1,169 @@ +import unittest + +from common.errors import SkyflowError +from common.utils import SkyflowMessages, LogLevel, Env +from common.utils.validations import ( + validate_vault_config, + validate_update_vault_config, + validate_credentials, + validate_log_level, +) + +VALID_VAULT_CONFIG = { + "vault_id": "vault123", + "cluster_id": "cluster1", + "env": Env.PROD, + "credentials": {"api_key": "sky-abcde-" + "f" * 32}, +} + + +class FakeMessages: + """Stand-in message catalog to confirm validate_vault_config/etc. actually use the + `messages` param passed in, rather than silently falling back to common's own.""" + + class Error: + class _M: + def __init__(self, text): + self._text = text + + @property + def value(self): + return self._text + + def format(self, *args, **kwargs): + return self._text + + EMPTY_VAULT_ID = _M("FAKE: empty vault id") + INVALID_VAULT_ID = _M("FAKE: invalid vault id") + EMPTY_CLUSTER_ID = _M("FAKE: empty cluster id") + INVALID_CLUSTER_ID = _M("FAKE: invalid cluster id") + EMPTY_CREDENTIALS = _M("FAKE: empty credentials") + INVALID_ENV = _M("FAKE: invalid env") + INVALID_KEY = _M("FAKE: invalid key") + INVALID_LOG_LEVEL = _M("FAKE: invalid log level") + INVALID_CREDENTIALS = _M("FAKE: invalid credentials") + INVALID_CREDENTIALS_IN_CONFIG = _M("FAKE: invalid credentials in config") + + class ErrorLogs: + class _M: + def __init__(self, text): + self._text = text + + @property + def value(self): + return self._text + + VAULTID_IS_REQUIRED = _M("fake log") + CLUSTER_ID_IS_REQUIRED = _M("fake log") + CONNECTION_ID_IS_REQUIRED = _M("fake log") + INVALID_CONNECTION_URL = _M("fake log") + EMPTY_VAULTID = _M("fake log") + EMPTY_CLUSTER_ID = _M("fake log") + EMPTY_CONNECTION_ID = _M("fake log") + EMPTY_CONNECTION_URL = _M("fake log") + EMPTY_CREDENTIALS_PATH = _M("fake log") + EMPTY_CREDENTIALS_STRING = _M("fake log") + EMPTY_TOKEN_VALUE = _M("fake log") + EMPTY_API_KEY_VALUE = _M("fake log") + INVALID_KEY = _M("fake log") + INVALID_LOG_LEVEL = _M("fake log") + ENV_IS_REQUIRED = _M("fake log") + + class Info: + class _M: + def __init__(self, text): + self._text = text + + @property + def value(self): + return self._text + + VALIDATING_VAULT_CONFIG = _M("fake info") + + +class TestValidateVaultConfig(unittest.TestCase): + def test_valid_config_passes(self): + self.assertTrue(validate_vault_config(None, dict(VALID_VAULT_CONFIG))) + + def test_missing_vault_id_raises(self): + config = dict(VALID_VAULT_CONFIG) + del config["vault_id"] + with self.assertRaises(SkyflowError): + validate_vault_config(None, config) + + def test_unknown_key_raises(self): + config = dict(VALID_VAULT_CONFIG) + config["unexpected"] = True + with self.assertRaises(SkyflowError): + validate_vault_config(None, config) + + def test_empty_credentials_raises(self): + config = dict(VALID_VAULT_CONFIG) + config["credentials"] = {} + with self.assertRaises(SkyflowError): + validate_vault_config(None, config) + + def test_credentials_are_validated(self): + config = dict(VALID_VAULT_CONFIG) + config["credentials"] = {"api_key": "not-a-valid-key"} + with self.assertRaises(SkyflowError): + validate_vault_config(None, config) + + def test_uses_injected_messages_for_raised_error(self): + config = dict(VALID_VAULT_CONFIG) + del config["vault_id"] + with self.assertRaises(SkyflowError) as ctx: + validate_vault_config(None, config, messages=FakeMessages) + self.assertIn("FAKE", ctx.exception.message) + + def test_defaults_to_common_messages_when_not_injected(self): + config = dict(VALID_VAULT_CONFIG) + del config["vault_id"] + with self.assertRaises(SkyflowError) as ctx: + validate_vault_config(None, config) + self.assertEqual(ctx.exception.message, SkyflowMessages.Error.INVALID_VAULT_ID.value) + + +class TestValidateUpdateVaultConfig(unittest.TestCase): + def test_valid_update_passes(self): + self.assertTrue(validate_update_vault_config(None, dict(VALID_VAULT_CONFIG))) + + def test_credentials_required_on_update(self): + """Unlike validate_vault_config, credentials are mandatory here.""" + config = dict(VALID_VAULT_CONFIG) + del config["credentials"] + with self.assertRaises(SkyflowError): + validate_update_vault_config(None, config) + + def test_uses_injected_messages(self): + config = dict(VALID_VAULT_CONFIG) + del config["credentials"] + with self.assertRaises(SkyflowError) as ctx: + validate_update_vault_config(None, config, messages=FakeMessages) + self.assertIn("FAKE", ctx.exception.message) + + +class TestValidateCredentialsMessageInjection(unittest.TestCase): + def test_uses_injected_messages(self): + with self.assertRaises(SkyflowError) as ctx: + validate_credentials(None, {}, messages=FakeMessages) + self.assertIn("FAKE", ctx.exception.message) + + def test_defaults_to_common_messages(self): + with self.assertRaises(SkyflowError) as ctx: + validate_credentials(None, {}) + self.assertEqual(ctx.exception.message, SkyflowMessages.Error.INVALID_CREDENTIALS.value) + + +class TestValidateLogLevelMessageInjection(unittest.TestCase): + def test_valid_log_level_passes(self): + validate_log_level(None, LogLevel.INFO) # should not raise + + def test_uses_injected_messages(self): + with self.assertRaises(SkyflowError) as ctx: + validate_log_level(None, "not-a-log-level", messages=FakeMessages) + self.assertIn("FAKE", ctx.exception.message) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/utils/logger/__init__.py b/common/tests/vault/__init__.py similarity index 100% rename from tests/utils/logger/__init__.py rename to common/tests/vault/__init__.py diff --git a/tests/utils/validations/__init__.py b/common/tests/vault/data/__init__.py similarity index 100% rename from tests/utils/validations/__init__.py rename to common/tests/vault/data/__init__.py diff --git a/common/tests/vault/data/test_base_insert.py b/common/tests/vault/data/test_base_insert.py new file mode 100644 index 00000000..f49da1a8 --- /dev/null +++ b/common/tests/vault/data/test_base_insert.py @@ -0,0 +1,42 @@ +import unittest + +from common.vault.data import BaseInsertRequest, BaseInsertResponse + + +class TestBaseInsertRequest(unittest.TestCase): + def test_table_and_values_are_required(self): + with self.assertRaises(TypeError): + BaseInsertRequest() + + def test_upsert_defaults_to_none(self): + request = BaseInsertRequest(table="t1", values=[{"values": {"a": 1}}]) + self.assertIsNone(request.upsert) + + def test_construction(self): + request = BaseInsertRequest(table="t1", values=[{"values": {"a": 1}}], upsert="upsert_val") + self.assertEqual(request.table, "t1") + self.assertEqual(request.values, [{"values": {"a": 1}}]) + self.assertEqual(request.upsert, "upsert_val") + + +class TestBaseInsertResponse(unittest.TestCase): + def test_defaults(self): + response = BaseInsertResponse() + self.assertIsNone(response.inserted_fields) + self.assertIsNone(response.errors) + + def test_repr_uses_subclass_name(self): + class InsertResponse(BaseInsertResponse): + pass + + response = InsertResponse(inserted_fields=[{"skyflow_id": "id1"}], errors=None) + self.assertIn("InsertResponse", repr(response)) + self.assertNotIn("BaseInsertResponse", repr(response)) + + def test_str_matches_repr(self): + response = BaseInsertResponse(inserted_fields=[], errors=[]) + self.assertEqual(str(response), repr(response)) + + +if __name__ == "__main__": + unittest.main() diff --git a/common/tests/vault/test_base_vault_client.py b/common/tests/vault/test_base_vault_client.py new file mode 100644 index 00000000..b02acbf2 --- /dev/null +++ b/common/tests/vault/test_base_vault_client.py @@ -0,0 +1,289 @@ +import unittest +from unittest.mock import patch, MagicMock + +from common.vault.base_vault_client import BaseVaultClient + +CONFIG = { + "credentials": "some_credentials", + "cluster_id": "test_cluster_id", + "env": "test_env", + "vault_id": "test_vault_id", + "roles": ["role_id_1", "role_id_2"], + "ctx": "context" +} + +CREDENTIALS_WITH_API_KEY = {"api_key": "dummy_api_key"} +CREDENTIALS_WITH_TOKEN = {"token": "dummy_static_token"} +CREDENTIALS_WITH_PATH = {"path": "/some/path/credentials.json"} +CREDENTIALS_WITH_STRING = {"credentials_string": '{"clientID": "x"}'} + + +class DummyVaultClient(BaseVaultClient): + """Minimal concrete subclass -- exercises BaseVaultClient's shared logic without any + variant-specific generated-API wiring. Mirrors what test__client.py used to test directly + against v2's VaultClient, before initialize_client_configuration/get_bearer_token moved here. + + resolve_vault_url is a real (if trivial) implementation here, not just a stub, because + BaseVaultClient.initialize_client_configuration() now delegates URL construction to it -- + each variant's derivation differs (see BaseVaultClient.resolve_vault_url's docstring), so + there's no generic default to fall back on.""" + + def resolve_vault_url(self, cluster_id, env, vault_id, logger=None): + return "https://test-vault-url.com" + + def initialize_api_client(self, vault_url, bearer_token): + self._api_client = MagicMock() + + +class TestBaseVaultClient(unittest.TestCase): + def setUp(self): + self.vault_client = DummyVaultClient(dict(CONFIG)) + + # ------------------------------------------------------------------ # + # Basic setters / getters + # ------------------------------------------------------------------ # + + def test_set_common_skyflow_credentials(self): + credentials = {"api_key": "dummy_api_key"} + self.vault_client.set_common_skyflow_credentials(credentials) + self.assertEqual(self.vault_client.get_common_skyflow_credentials(), credentials) + + def test_set_logger(self): + mock_logger = MagicMock() + self.vault_client.set_logger("INFO", mock_logger) + self.assertEqual(self.vault_client.get_log_level(), "INFO") + self.assertEqual(self.vault_client.get_logger(), mock_logger) + + def test_get_vault_id(self): + self.assertEqual(self.vault_client.get_vault_id(), CONFIG["vault_id"]) + + def test_get_config(self): + self.assertEqual(self.vault_client.get_config(), CONFIG) + + # ------------------------------------------------------------------ # + # initialize_client_configuration — first call (slow path) + # ------------------------------------------------------------------ # + + @patch("common.vault.base_vault_client.get_credentials") + @patch.object(DummyVaultClient, "resolve_vault_url") + @patch.object(DummyVaultClient, "initialize_api_client") + def test_initialize_client_configuration_first_call( + self, mock_init_api_client, mock_resolve_vault_url, mock_get_credentials + ): + mock_get_credentials.return_value = CREDENTIALS_WITH_API_KEY + mock_resolve_vault_url.return_value = "https://test-vault-url.com" + + self.vault_client.initialize_client_configuration() + + mock_get_credentials.assert_called_once_with( + CONFIG["credentials"], None, logger=None + ) + mock_resolve_vault_url.assert_called_once_with( + CONFIG["cluster_id"], CONFIG["env"], CONFIG["vault_id"], logger=None + ) + mock_init_api_client.assert_called_once() + + # ------------------------------------------------------------------ # + # initialize_client_configuration — fast path (static token) + # ------------------------------------------------------------------ # + + @patch("common.vault.base_vault_client.get_credentials") + @patch.object(DummyVaultClient, "resolve_vault_url") + def test_initialize_client_configuration_fast_path_api_key( + self, mock_resolve_vault_url, mock_get_credentials + ): + """Once initialized with api_key, subsequent calls skip all work.""" + mock_get_credentials.return_value = CREDENTIALS_WITH_API_KEY + mock_resolve_vault_url.return_value = "https://test-vault-url.com" + + self.vault_client.initialize_client_configuration() # first call — slow path + mock_get_credentials.reset_mock() + mock_resolve_vault_url.reset_mock() + + self.vault_client.initialize_client_configuration() # second call — fast path + + mock_get_credentials.assert_not_called() + mock_resolve_vault_url.assert_not_called() + + @patch("common.vault.base_vault_client.get_credentials") + @patch.object(DummyVaultClient, "resolve_vault_url") + def test_initialize_client_configuration_fast_path_static_token( + self, mock_resolve_vault_url, mock_get_credentials + ): + """Once initialized with a static token, subsequent calls skip all work.""" + mock_get_credentials.return_value = CREDENTIALS_WITH_TOKEN + mock_resolve_vault_url.return_value = "https://test-vault-url.com" + + self.vault_client.initialize_client_configuration() + mock_get_credentials.reset_mock() + mock_resolve_vault_url.reset_mock() + + self.vault_client.initialize_client_configuration() + + mock_get_credentials.assert_not_called() + mock_resolve_vault_url.assert_not_called() + + # ------------------------------------------------------------------ # + # initialize_client_configuration — fast path (service account) + # ------------------------------------------------------------------ # + + @patch("common.vault.base_vault_client.is_expired", return_value=False) + @patch("common.vault.base_vault_client.get_credentials") + @patch.object(DummyVaultClient, "resolve_vault_url") + @patch.object(DummyVaultClient, "initialize_api_client") + def test_initialize_client_configuration_fast_path_valid_sa_token( + self, mock_init_api_client, mock_resolve_vault_url, mock_get_credentials, mock_is_expired + ): + """Service account with a still-valid token skips get_bearer_token entirely.""" + mock_get_credentials.return_value = CREDENTIALS_WITH_PATH + mock_resolve_vault_url.return_value = "https://test-vault-url.com" + + # Seed the cached bearer token as if first call already ran + self.vault_client._api_client = MagicMock() + self.vault_client._is_static_token = False + self.vault_client._bearer_token = "cached_sa_token" + self.vault_client._credentials = CREDENTIALS_WITH_PATH + + self.vault_client.initialize_client_configuration() + + mock_get_credentials.assert_not_called() + mock_resolve_vault_url.assert_not_called() + mock_init_api_client.assert_not_called() + + # ------------------------------------------------------------------ # + # initialize_client_configuration — token expiry (no client reinit) + # ------------------------------------------------------------------ # + + @patch("common.vault.base_vault_client.generate_bearer_token", return_value=("new_sa_token", None)) + @patch("common.vault.base_vault_client.is_expired", return_value=True) + @patch("common.vault.base_vault_client.get_credentials") + @patch.object(DummyVaultClient, "resolve_vault_url") + @patch.object(DummyVaultClient, "initialize_api_client") + def test_initialize_client_configuration_expired_token_no_reinit( + self, mock_init_api_client, mock_resolve_vault_url, mock_get_credentials, + mock_is_expired, mock_generate_bearer_token + ): + """Expired service account token is regenerated in-place; the api client is NOT recreated.""" + mock_get_credentials.return_value = CREDENTIALS_WITH_PATH + mock_resolve_vault_url.return_value = "https://test-vault-url.com" + + # Client already initialized — simulate warm state with an expired token + self.vault_client._api_client = MagicMock() + self.vault_client._is_static_token = False + self.vault_client._bearer_token = "expired_sa_token" + self.vault_client._credentials = CREDENTIALS_WITH_PATH + + self.vault_client.initialize_client_configuration() + + # Token was regenerated + mock_generate_bearer_token.assert_called_once() + self.assertEqual(self.vault_client._bearer_token, "new_sa_token") + # api client was NOT recreated + mock_init_api_client.assert_not_called() + + # ------------------------------------------------------------------ # + # initialize_client_configuration — config update forces reinit + # ------------------------------------------------------------------ # + + @patch("common.vault.base_vault_client.get_credentials") + @patch.object(DummyVaultClient, "resolve_vault_url") + @patch.object(DummyVaultClient, "initialize_api_client") + def test_initialize_client_configuration_reinit_after_update_config( + self, mock_init_api_client, mock_resolve_vault_url, mock_get_credentials + ): + """update_config() marks the client stale; next call must recreate it.""" + mock_get_credentials.return_value = CREDENTIALS_WITH_API_KEY + mock_resolve_vault_url.return_value = "https://test-vault-url.com" + + # Simulate already-initialized client + self.vault_client._api_client = MagicMock() + self.vault_client._is_static_token = True + + self.vault_client.update_config({"cluster_id": "new_cluster"}) + self.vault_client.initialize_client_configuration() + + mock_get_credentials.assert_called_once() + mock_resolve_vault_url.assert_called_once() + mock_init_api_client.assert_called_once() + + # ------------------------------------------------------------------ # + # get_bearer_token + # ------------------------------------------------------------------ # + + def test_get_bearer_token_with_api_key(self): + result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_API_KEY) + self.assertEqual(result, "dummy_api_key") + + def test_get_bearer_token_with_static_token(self): + result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_TOKEN) + self.assertEqual(result, "dummy_static_token") + + @patch("common.vault.base_vault_client.generate_bearer_token", return_value=("sa_token", None)) + def test_get_bearer_token_generates_from_path_on_first_call(self, mock_generate): + result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_PATH) + mock_generate.assert_called_once() + self.assertEqual(result, "sa_token") + self.assertEqual(self.vault_client._bearer_token, "sa_token") + + @patch("common.vault.base_vault_client.generate_bearer_token_from_creds", return_value=("sa_token_str", None)) + @patch("common.vault.base_vault_client.log_info") + def test_get_bearer_token_generates_from_credentials_string(self, mock_log, mock_generate): + result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_STRING) + mock_generate.assert_called_once() + self.assertEqual(result, "sa_token_str") + + @patch("common.vault.base_vault_client.generate_bearer_token", return_value=("new_token", None)) + @patch("common.vault.base_vault_client.is_expired", return_value=True) + @patch("common.vault.base_vault_client.log_info") + def test_get_bearer_token_regenerates_on_expiry(self, mock_log, mock_is_expired, mock_generate): + """Expired token is regenerated silently — no exception raised.""" + self.vault_client._bearer_token = "expired_token" + result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_PATH) + mock_generate.assert_called_once() + self.assertEqual(result, "new_token") + + @patch("common.vault.base_vault_client.generate_bearer_token") + @patch("common.vault.base_vault_client.is_expired", return_value=False) + @patch("common.vault.base_vault_client.log_info") + def test_get_bearer_token_reuses_valid_cached_token(self, mock_log, mock_is_expired, mock_generate): + """Valid cached token is reused without calling generate_bearer_token.""" + self.vault_client._bearer_token = "valid_token" + result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_PATH) + mock_generate.assert_not_called() + self.assertEqual(result, "valid_token") + + # ------------------------------------------------------------------ # + # update_config + # ------------------------------------------------------------------ # + + def test_update_config_sets_flag(self): + self.vault_client.update_config({"credentials": "new_credentials"}) + self.assertTrue(self.vault_client._is_config_updated) + self.assertEqual(self.vault_client.get_config()["credentials"], "new_credentials") + + # ------------------------------------------------------------------ # + # get_current_bearer_token (new accessor -- did not exist pre-split) + # ------------------------------------------------------------------ # + + def test_get_current_bearer_token_none_before_first_fetch(self): + self.assertIsNone(self.vault_client.get_current_bearer_token()) + + def test_get_current_bearer_token_returns_cached_value(self): + self.vault_client._bearer_token = "cached_value" + self.assertEqual(self.vault_client.get_current_bearer_token(), "cached_value") + + # ------------------------------------------------------------------ # + # resolve_vault_url is abstract -- a subclass that forgets it can't instantiate + # ------------------------------------------------------------------ # + + def test_resolve_vault_url_is_a_required_abstract_hook(self): + class MissingResolveVaultUrl(BaseVaultClient): + def initialize_api_client(self, vault_url, bearer_token): + pass + + with self.assertRaises(TypeError): + MissingResolveVaultUrl(dict(CONFIG)) + + +if __name__ == "__main__": + unittest.main() diff --git a/common/tests/vault/test_base_vault_controller.py b/common/tests/vault/test_base_vault_controller.py new file mode 100644 index 00000000..8f0509cb --- /dev/null +++ b/common/tests/vault/test_base_vault_controller.py @@ -0,0 +1,122 @@ +import unittest + +from common.errors import SkyflowError +from common.utils import SkyflowMessages +from common.vault.base_vault_controller import BaseVaultController + + +class DummyVaultController(BaseVaultController): + _skyflow_messages = SkyflowMessages + + def insert(self, request): + raise NotImplementedError + + def get(self, request): + raise NotImplementedError + + def update(self, request): + raise NotImplementedError + + def delete(self, request): + raise NotImplementedError + + def query(self, request): + raise NotImplementedError + + def detokenize(self, request): + raise NotImplementedError + + +class TestBaseVaultControllerAbstractContract(unittest.TestCase): + def test_cannot_instantiate_without_insert(self): + class Incomplete(BaseVaultController): + pass + + with self.assertRaises(TypeError): + Incomplete(vault_client=None) + + def test_cannot_instantiate_missing_any_single_method(self): + """Java-interface-style: every one of the six operations is independently required -- + omitting any single one (not just insert) blocks instantiation.""" + for missing in ("insert", "get", "update", "delete", "query", "detokenize"): + methods = {name: (lambda self, request: None) for name in + ("insert", "get", "update", "delete", "query", "detokenize") if name != missing} + Incomplete = type("Incomplete", (BaseVaultController,), methods) + with self.assertRaises(TypeError, msg=f"missing only '{missing}' should still fail to instantiate"): + Incomplete(vault_client=None) + + def test_concrete_subclass_instantiates(self): + vault = DummyVaultController(vault_client=None) + self.assertIsInstance(vault, BaseVaultController) + + +class TestValidateTableNameIfPresent(unittest.TestCase): + """Shared rule used identically by both variants (see PdbVaultController/flowvault's + VaultController.insert()): a table value, IF given, must be a non-empty string. Whether + table is required at all is variant-specific and stays out of this helper.""" + + def setUp(self): + self.vault = DummyVaultController(vault_client=None) + + def test_none_is_allowed(self): + self.vault._validate_table_name_if_present(None) # should not raise + + def test_valid_string_is_allowed(self): + self.vault._validate_table_name_if_present("table1") # should not raise + + def test_empty_string_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_table_name_if_present("") + + def test_whitespace_only_string_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_table_name_if_present(" ") + + def test_non_string_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_table_name_if_present(123) + + +class TestValidateFieldValues(unittest.TestCase): + """Shared rule: a record's field-value map must be a non-empty dict of non-empty string + keys -- values may be anything, including None/empty string.""" + + def setUp(self): + self.vault = DummyVaultController(vault_client=None) + + def test_valid_values_pass(self): + self.vault._validate_field_values({"name": "John", "age": 30}) # should not raise + + def test_none_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_field_values(None) + + def test_non_dict_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_field_values(["not", "a", "dict"]) + + def test_empty_dict_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_field_values({}) + + def test_empty_key_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_field_values({"": "value"}) + + def test_whitespace_only_key_raises(self): + with self.assertRaises(SkyflowError): + self.vault._validate_field_values({" ": "value"}) + + def test_none_value_is_valid(self): + self.vault._validate_field_values({"a": None}) # should not raise + + def test_empty_string_value_is_valid(self): + self.vault._validate_field_values({"a": ""}) # should not raise + + def test_falsy_non_string_values_are_valid(self): + """0, False, [], {} are all legitimate values.""" + self.vault._validate_field_values({"a": 0, "b": False, "c": [], "d": {}}) # should not raise + + +if __name__ == "__main__": + unittest.main() diff --git a/common/utils/__init__.py b/common/utils/__init__.py new file mode 100644 index 00000000..3dbcd1db --- /dev/null +++ b/common/utils/__init__.py @@ -0,0 +1,4 @@ +from .enums import LogLevel, Env, TokenType +from ._skyflow_messages import SkyflowMessages +from ._helpers import get_base_url, format_scope, is_valid_url +from ._utils import get_credentials, get_vault_url, validate_api_key diff --git a/skyflow/utils/_helpers.py b/common/utils/_helpers.py similarity index 100% rename from skyflow/utils/_helpers.py rename to common/utils/_helpers.py diff --git a/common/utils/_skyflow_messages.py b/common/utils/_skyflow_messages.py new file mode 100644 index 00000000..30f9d533 --- /dev/null +++ b/common/utils/_skyflow_messages.py @@ -0,0 +1,443 @@ +import sys +from enum import Enum + +# `common` has no SDK_VERSION of its own -- picks up whichever variant is installed. Reads +# sys.modules (never forces a fresh import) to avoid triggering a circular import back into +# common.* mid-init. +_version_module = sys.modules.get("skyflow.utils._version") +SDK_VERSION = getattr(_version_module, "SDK_VERSION", "0.0.0") + +error_prefix = f"Skyflow Python SDK {SDK_VERSION}" +INFO = "INFO" +WARN = "WARN" +ERROR = "ERROR" + +class SkyflowMessages: + class ErrorCodes(Enum): + INVALID_INPUT = 400 + INVALID_INDEX = 404 + SERVER_ERROR = 500 + PARTIAL_SUCCESS = 500 + TOKENS_GET_COLUMN_NOT_SUPPORTED = 400 + REDACTION_WITH_TOKENS_NOT_SUPPORTED = 400 + + class Error(Enum): + GENERIC_API_ERROR = f"{error_prefix} API error. Error occurred." + + EMPTY_VAULT_ID = f"{error_prefix} Initialization failed. Invalid vault Id. Specify a valid vault Id." + INVALID_VAULT_ID = f"{error_prefix} Initialization failed. Invalid vault Id. Specify a valid vault Id as a string." + EMPTY_CLUSTER_ID = f"{error_prefix} Initialization failed. Invalid cluster Id for vault with id {{}}. Specify a valid cluster Id." + INVALID_CLUSTER_ID = f"{error_prefix} Initialization failed. Invalid cluster Id for vault with id {{}}. Specify cluster Id as a string." + INVALID_ENV = f"{error_prefix} Initialization failed. Invalid env for vault with id {{}}. Specify a valid env." + INVALID_KEY = f"{error_prefix} Initialization failed. Invalid {{}}. Specify a valid key" + VAULT_ID_NOT_IN_CONFIG_LIST = f"{error_prefix} Validation error. Vault id {{}} is missing from the config. Specify the vault id from configs." + EMPTY_VAULT_CONFIGS = f"{error_prefix} Validation error. Specify at least one vault config." + EMPTY_CONNECTION_CONFIGS = f"{error_prefix} Validation error. Specify at least one connection config." + VAULT_ID_ALREADY_EXISTS =f"{error_prefix} Initialization failed. vault with id {{}} already exists." + CONNECTION_ID_ALREADY_EXISTS = f"{error_prefix} Initialization failed. Connection with id {{}} already exists." + + EMPTY_CREDENTIALS = f"{error_prefix} Validation error. Invalid credentials for {{}} with id {{}}. Credentials must not be empty." + INVALID_CREDENTIALS_IN_CONFIG = f"{error_prefix} Validation error. Invalid credentials for {{}} with id {{}}. Specify a valid credentials." + INVALID_CREDENTIALS = f"{error_prefix} Validation error. Invalid credentials. Specify a valid credentials." + MULTIPLE_CREDENTIALS_PASSED_IN_CONFIG = f"{error_prefix} Validation error. Multiple credentials provided for {{}} with id {{}}. Please specify only one valid credential." + MULTIPLE_CREDENTIALS_PASSED = f"{error_prefix} Validation error. Multiple credentials provided. Please specify only one valid credential." + EMPTY_CREDENTIALS_STRING_IN_CONFIG = f"{error_prefix} Validation error. Invalid credentials for {{}} with id {{}}. Specify valid credentials." + EMPTY_CREDENTIALS_STRING = f"{error_prefix} Validation error. Invalid credentials. Specify valid credentials." + INVALID_CREDENTIALS_STRING_IN_CONFIG = f"{error_prefix} Validation error. Invalid credentials for {{}} with id {{}}. Specify credentials as a string." + INVALID_CREDENTIALS_STRING = f"{error_prefix} Validation error. Invalid credentials. Specify credentials as a string." + EMPTY_CREDENTIAL_FILE_PATH_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid credentials for {{}} with id {{}}. Specify a valid file path." + EMPTY_CREDENTIAL_FILE_PATH = f"{error_prefix} Initialization failed. Invalid credentials. Specify a valid file path." + INVALID_CREDENTIAL_FILE_PATH_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid credentials for {{}} with id {{}}. Expected file path to be a string." + INVALID_CREDENTIAL_FILE_PATH = f"{error_prefix} Initialization failed. Invalid credentials. Expected file path to be a valid file path." + EMPTY_CREDENTIALS_TOKEN_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid token for {{}} with id {{}}.Specify a valid credentials token." + EMPTY_CREDENTIALS_TOKEN = f"{error_prefix} Initialization failed. Invalid token.Specify a valid credentials token." + INVALID_CREDENTIALS_TOKEN_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid credentials token for {{}} with id {{}}. Expected token to be a string." + INVALID_CREDENTIALS_TOKEN = f"{error_prefix} Initialization failed. Invalid credentials token. Expected token to be a string." + EXPIRED_BEARER_TOKEN = f"{error_prefix} Initialization failed. Bearer token is invalid or expired." + EXPIRED_TOKEN = f"{error_prefix} Initialization failed. Given token is expired. Specify a valid credentials token." + EMPTY_API_KEY_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid api key for {{}} with id {{}}.Specify a valid api key." + EMPTY_API_KEY= f"{error_prefix} Initialization failed. Invalid api key.Specify a valid api key." + INVALID_API_KEY_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid api key for {{}} with id {{}}. Expected api key to be a string." + INVALID_API_KEY = f"{error_prefix} Initialization failed. Invalid api key. Expected api key to be a string." + INVALID_ROLES_KEY_TYPE_IN_CONFIG = f"{error_prefix} Validation error. Invalid roles for {{}} with id {{}}. Specify roles as an array." + INVALID_ROLES_KEY_TYPE = f"{error_prefix} Validation error. Invalid roles. Specify roles as an array." + EMPTY_ROLES_IN_CONFIG = f"{error_prefix} Validation error. Invalid roles for {{}} with id {{}}. Specify at least one role." + EMPTY_ROLES = f"{error_prefix} Validation error. Invalid roles. Specify at least one role." + EMPTY_CONTEXT_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid context provided for {{}} with id {{}}. Specify context as type Context." + EMPTY_CONTEXT = f"{error_prefix} Initialization failed. Invalid context provided. Specify context as type Context." + INVALID_CONTEXT_IN_CONFIG = f"{error_prefix} Initialization failed. Invalid context for {{}} with id {{}}. Specify a valid context." + INVALID_CONTEXT = f"{error_prefix} Initialization failed. Invalid context. Specify a valid context." + INVALID_CTX_TYPE = f"{error_prefix} Initialization failed. Invalid ctx type. Specify ctx as a string or a dict." + INVALID_CTX_MAP_KEY = f"{error_prefix} Initialization failed. Invalid key '{{}}' in ctx dict. Keys must contain only alphanumeric characters and underscores." + INVALID_LOG_LEVEL = f"{error_prefix} Initialization failed. Invalid log level. Specify a valid log level." + EMPTY_LOG_LEVEL = f"{error_prefix} Initialization failed. Specify a valid log level." + + EMPTY_CONNECTION_ID = f"{error_prefix} Initialization failed. Invalid connection Id. Specify a valid connection Id." + INVALID_CONNECTION_ID = f"{error_prefix} Initialization failed. Invalid connection Id. Specify connection Id as a string." + EMPTY_CONNECTION_URL = f"{error_prefix} Initialization failed. Invalid connection Url for connection with id {{}}. Specify a valid connection Url." + INVALID_CONNECTION_URL = f"{error_prefix} Initialization failed. Invalid connection Url for connection with id {{}}. Specify connection Url as a string." + CONNECTION_ID_NOT_IN_CONFIG_LIST = f"{error_prefix} Validation error. {{}} is missing from the config. Specify the connectionIds from config." + RESPONSE_NOT_JSON = f"{error_prefix} Response {{}} is not valid JSON." + API_ERROR = f"{error_prefix} Server returned status code {{}}" + + INVALID_JSON_RESPONSE = f"{error_prefix} Invalid JSON response received." + UNKNOWN_ERROR_DEFAULT_MESSAGE = f"{error_prefix} An unknown error occurred." + + INVALID_FILE_INPUT = f"{error_prefix} Validation error. Invalid file input. Specify a valid file input." + INVALID_DETECT_ENTITIES_TYPE = f"{error_prefix} Validation error. Invalid type of detect entities. Specify detect entities as list of DetectEntities enum." + INVALID_TYPE_FOR_DEFAULT_TOKEN_TYPE = f"{error_prefix} Validation error. Invalid type of default token type. Specify default token type as TokenType enum." + INVALID_TOKEN_TYPE_VALUE = f"{error_prefix} Validation error. Invalid value for token type {{}}. Specify as list of DetectEntities enum." + INVALID_MAXIMUM_RESOLUTION = f"{error_prefix} Validation error. Invalid type of maximum resolution. Specify maximum resolution as a number." + INVALID_OUTPUT_DIRECTORY_VALUE = f"{error_prefix} Validation error. Invalid type of output directory. Specify output directory as a string." + WAIT_TIME_GREATER_THEN_64 = f"{error_prefix} Validation error. Invalid wait time. The waitTime value must be between 0 and 64 seconds." + OUTPUT_DIRECTORY_NOT_FOUND = f"{error_prefix} Validation error. Invalid output directory. Directory {{}} not found." + + MISSING_TABLE_NAME_IN_INSERT = f"{error_prefix} Validation error. Table name cannot be empty in insert request. Specify a table name." + INVALID_TABLE_NAME_IN_INSERT = f"{error_prefix} Validation error. Invalid table name in insert request. Specify a valid table name." + INVALID_TYPE_OF_DATA_IN_INSERT = f"{error_prefix} Validation error. Invalid type of data in insert request. Specify data as a object array." + EMPTY_DATA_IN_INSERT = f"{error_prefix} Validation error. Data array cannot be empty. Specify data in insert request." + INVALID_RECORD_DATA_IN_INSERT = f"{error_prefix} Validation error. Each record's field values must be a non-empty dict." + EMPTY_KEY_IN_INSERT_DATA = f"{error_prefix} Validation error. A record must not contain a null or empty key." + EMPTY_VALUE_IN_INSERT_DATA = f"{error_prefix} Validation error. A record must not contain a null or empty value." + INVALID_UPSERT_OPTIONS_TYPE = f"{error_prefix} Validation error. Invalid 'upsert' value in options. Specify 'upsert' as a non-empty string containing the column name." + INVALID_HOMOGENEOUS_TYPE = f"{error_prefix} Validation error. Invalid type of homogeneous. Specify homogeneous as a string." + INVALID_TOKEN_MODE_TYPE = f"{error_prefix} Validation error. Invalid type of token mode. Specify token mode as a TokenMode enum." + INVALID_RETURN_TOKENS_TYPE = f"{error_prefix} Validation error. Invalid type of return tokens. Specify return tokens as a boolean." + INVALID_CONTINUE_ON_ERROR_TYPE = f"{error_prefix} Validation error. Invalid type of continue on error. Specify continue on error as a boolean." + TOKENS_PASSED_FOR_TOKEN_MODE_DISABLE = f"{error_prefix} Validation error. 'token_mode' wasn't specified. Set 'token_mode' to 'ENABLE' to insert tokens." + INSUFFICIENT_TOKENS_PASSED_FOR_TOKEN_MODE_ENABLE_STRICT = f"{error_prefix} Validation error. 'token_mode' is set to 'ENABLE_STRICT', but some fields are missing tokens. Specify tokens for all fields." + MISMATCH_OF_FIELDS_AND_TOKENS = f"{error_prefix} Validation error. Keys for values and tokens are not matching. Ensure each values entry and its corresponding tokens entry have the same keys." + NO_TOKENS_IN_INSERT = f"{error_prefix} Validation error. Tokens weren't specified for records while 'token_mode' was {{}}. Specify tokens." + BATCH_INSERT_FAILURE = f"{error_prefix} Insert operation failed." + GET_FAILURE = f"{error_prefix} Get operation failed." + HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT = f"{error_prefix} Validation error. Homogenous is not supported when upsert is passed." + + EMPTY_TABLE_VALUE = f"{error_prefix} Validation error. 'table' can't be empty. Specify a table." + INVALID_TABLE_VALUE = f"{error_prefix} Validation error. Invalid type of table. Specify table as a string" + EMPTY_RECORD_IDS_IN_DELETE = f"{error_prefix} Validation error. 'record ids' array can't be empty. Specify one or more record ids." + BULK_DELETE_FAILURE = f"{error_prefix} Delete operation failed." + EMPTY_SKYFLOW_ID= f"{error_prefix} Validation error. skyflow_id can't be empty." + INVALID_FILE_COLUMN_NAME= f"{error_prefix} Validation error. 'column_name' can't be empty." + + INVALID_QUERY_TYPE = f"{error_prefix} Validation error. Query parameter is of type {{}}. Specify as a string." + EMPTY_QUERY = f"{error_prefix} Validation error. Query parameter can't be empty. Specify as a string." + INVALID_QUERY_COMMAND = f"{error_prefix} Validation error. {{}} command was passed instead, but only SELECT commands are supported. Specify the SELECT command." + SERVER_ERROR = f"{error_prefix} Validation error. Check SkyflowError.data for details." + QUERY_FAILED = f"{error_prefix} Query operation failed." + DETOKENIZE_FIELD = f"{error_prefix} Detokenize operation failed." + UPDATE_FAILED = f"{error_prefix} Update operation failed." + TOKENIZE_FAILED = f"{error_prefix} Tokenize operation failed." + INVOKE_CONNECTION_FAILED = f"{error_prefix} Invoke Connection operation failed." + + INVALID_IDS_TYPE = f"{error_prefix} Validation error. 'ids' has a value of type {{}}. Specify 'ids' as list." + INVALID_REDACTION_TYPE = f"{error_prefix} Validation error. 'redaction_type' has a value of type {{}}. Specify 'redaction_type' as type Skyflow.RedactionType." + INVALID_COLUMN_NAME = f"{error_prefix} Validation error. column_name has a value of type {{}}. Specify 'column' as a string." + INVALID_COLUMN_VALUE = f"{error_prefix} Validation error. column_values key has a value of type {{}}. Specify column_values key as list." + INVALID_COLUMN_VALUES = f"{error_prefix} Validation error. column_values key is an empty list. Specify at least one column value when column_name is passed." + INVALID_FIELDS_VALUE = f"{error_prefix} Validation error. fields key has a value of type{{}}. Specify fields key as list." + BOTH_OFFSET_AND_LIMIT_SPECIFIED = f"{error_prefix} Validation error. Both offset and limit cannot be present at the same time" + INVALID_OFF_SET_VALUE = f"{error_prefix} Validation error. offset key has a value of type {{}}. Specify offset key as integer." + INVALID_LIMIT_VALUE = f"{error_prefix} Validation error. limit key has a value of type {{}}. Specify limit key as integer." + INVALID_DOWNLOAD_URL_VALUE = f"{error_prefix} Validation error. download_url key has a value of type {{}}. Specify download_url key as boolean." + REDACTION_WITH_TOKENS_NOT_SUPPORTED = f"{error_prefix} Validation error. 'redaction_type' can't be used when tokens are specified. Remove 'redaction_type' from payload if tokens are specified." + TOKENS_GET_COLUMN_NOT_SUPPORTED = f"{error_prefix} Validation error. Column name and/or column values can't be used when tokens are specified. Remove unique column values or tokens from the payload." + BOTH_IDS_AND_COLUMN_DETAILS_SPECIFIED = f"{error_prefix} Validation error. Both Skyflow IDs and column details can't be specified. Either specify Skyflow IDs or unique column details." + INVALID_ORDER_BY_VALUE = f"{error_prefix} Validation error. order_by key has a value of type {{}}. Specify order_by key as Skyflow.OrderBy" + + UPDATE_FIELD_KEY_ERROR = f"{error_prefix} Validation error. Fields are empty in an update payload. Specify at least one field." + INVALID_FIELDS_TYPE = f"{error_prefix} Validation error. The 'data' key has a value of type {{}}. Specify 'data' as a dictionary." + IDS_KEY_ERROR = f"{error_prefix} Validation error. 'ids' key is missing from the payload. Specify an 'ids' key." + INVALID_TOKENS_LIST_VALUE = f"{error_prefix} Validation error. The 'data' field is invalid. Specify 'data' as a list of dictionaries containing 'token' and 'redaction_type'." + INVALID_DATA_FOR_DETOKENIZE = f"{error_prefix}" + EMPTY_TOKENS_LIST_VALUE = f"{error_prefix} Validation error. Tokens are empty in detokenize payload. Specify at lease one token" + INVALID_TOKEN_TYPE = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens should be of type string." + + INVALID_TOKENIZE_PARAMETERS = f"{error_prefix} Validation error. The 'values' key has a value of type {{}}. Specify 'tokenize_parameters' as a list." + EMPTY_TOKENIZE_PARAMETERS = f"{error_prefix} Validation error. Tokenize values are empty in tokenize payload. Specify at least one parameter." + INVALID_TOKENIZE_PARAMETER = f"{error_prefix} Validation error. Tokenize value at index {{}} has a value of type {{}}. Specify as a dictionary." + EMPTY_TOKENIZE_PARAMETER_VALUE = f"{error_prefix} Validation error. Tokenize value at index {{}} is empty. Specify a valid value." + EMPTY_TOKENIZE_PARAMETER_COLUMN_GROUP = f"{error_prefix} Validation error. Tokenize column group at index {{}} is empty. Specify a valid column group." + INVALID_TOKENIZE_PARAMETER_KEY = f"{error_prefix} Validation error. Tokenize value key at index {{}} is invalid. Specify a valid key value." + + INVALID_REQUEST_BODY = f"{error_prefix} Validation error. Invalid request body. Specify the request body as an object." + INVALID_REQUEST_HEADERS = f"{error_prefix} Validation error. Invalid request headers. Specify the request as an object." + INVALID_URL = f"{error_prefix} Validation error. Connection url {{}} is invalid. Specify a valid connection url." + INVALID_PATH_PARAMS = f"{error_prefix} Validation error. Path parameters aren't valid. Specify valid path parameters." + INVALID_QUERY_PARAMS = f"{error_prefix} Validation error. Query parameters aren't valid. Specify valid query parameters." + INVALID_REQUEST_METHOD = f"{error_prefix} Validation error. Invalid request method. Specify the request method as enum RequestMethod" + + MISSING_PRIVATE_KEY = f"{error_prefix} Initialization failed. Unable to read private key in credentials. Verify your private key." + MISSING_CLIENT_ID = f"{error_prefix} Initialization failed. Unable to read client ID in credentials. Verify your client ID." + MISSING_KEY_ID = f"{error_prefix} Initialization failed. Unable to read key ID in credentials. Verify your key ID." + MISSING_TOKEN_URI = f"{error_prefix} Initialization failed. Unable to read token URI in credentials. Verify your token URI." + INVALID_TOKEN_URI = f"{error_prefix} Initialization failed. Invalid Skyflow credentials. The token URI must be a string and a valid URL." + JWT_INVALID_FORMAT = f"{error_prefix} Initialization failed. Invalid private key format. Verify your credentials." + JWT_DECODE_ERROR = f"{error_prefix} Validation error. Invalid access token. Verify your credentials." + FILE_INVALID_JSON = f"{error_prefix} Initialization failed. File at {{}} is not in valid JSON format. Verify the file contents." + INVALID_JSON_FORMAT_IN_CREDENTIALS_ENV = f"{error_prefix} Validation error. Invalid JSON format in SKYFLOW_CREDENTIALS environment variable." + FAILED_TO_GET_BEARER_TOKEN = f"{ERROR}: [{error_prefix}] Failed to generate bearer token." + UNAUTHORIZED_ERROR_IN_GETTING_BEARER_TOKEN = f"{ERROR}: [{error_prefix}] Authorization failed while retrieving the bearer token." + + INVALID_TEXT_IN_DEIDENTIFY= f"{error_prefix} Validation error. The text field is required and must be a non-empty string. Specify a valid text." + INVALID_ENTITIES_IN_DEIDENTIFY= f"{error_prefix} Validation error. The entities field must be an array of DetectEntities enums. Specify a valid entities." + INVALID_ALLOW_REGEX_LIST= f"{error_prefix} Validation error. The allowRegexList field must be an array of strings. Specify a valid allow_regex_list." + INVALID_RESTRICT_REGEX_LIST= f"{error_prefix} Validation error. The restrictRegexList field must be an array of strings. Specify a valid restrict_regex_list." + INVALID_TOKEN_FORMAT= f"{error_prefix} Validation error. The tokenFormat key must be an instance of TokenFormat. Specify a valid token format." + INVALID_TRANSFORMATIONS= f"{error_prefix} Validation error. The transformations key must be an instance of Transformations. Specify a valid transformations." + + INVALID_TEXT_IN_REIDENTIFY= f"{error_prefix} Validation error. The text field is required and must be a non-empty string. Specify a valid text." + INVALID_REDACTED_ENTITIES_IN_REIDENTIFY= f"{error_prefix} Validation error. The redactedEntities field must be an array of DetectEntities enums. Specify a valid redactedEntities." + INVALID_MASKED_ENTITIES_IN_REIDENTIFY= f"{error_prefix} Validation error. The maskedEntities field must be an array of DetectEntities enums. Specify a valid maskedEntities." + INVALID_PLAIN_TEXT_ENTITIES_IN_REIDENTIFY= f"{error_prefix} Validation error. The plainTextEntities field must be an array of DetectEntities enums. Specify a valid plainTextEntities." + + INVALID_DEIDENTIFY_FILE_REQUEST= f"{error_prefix} Validation error. Invalid deidentify file request. Specify a valid deidentify file request." + INVALID_DEIDENTIFY_FILE_INPUT= f"{error_prefix} Validation error. Invalid deidentify file input. Please provide either a file or a file path." + EMPTY_FILE_OBJECT= f"{error_prefix} Validation error. File object cannot be empty. Specify a valid file object." + INVALID_FILE_FORMAT= f"{error_prefix} Validation error. Invalid file format. Specify a valid file format." + MISSING_FILE_SOURCE= f"{error_prefix} Validation error. Provide exactly one of filePath, base64, or fileObject." + INVALID_FILE_OBJECT= f"{error_prefix} Validation error. Invalid file object. Specify a valid file object." + INVALID_BASE64_STRING= f"{error_prefix} Validation error. Invalid base64 string. Specify a valid base64 string." + INVALID_DEIDENTIFY_FILE_OPTIONS= f"{error_prefix} Validation error. Invalid deidentify file options. Specify a valid deidentify file options." + INVALID_ENTITIES= f"{error_prefix} Validation error. Invalid entities. Specify valid entities as string array." + EMPTY_ENTITIES= f"{error_prefix} Validation error. Entities cannot be empty. Specify valid entities." + EMPTY_ALLOW_REGEX_LIST= f"{error_prefix} Validation error. Allow regex list cannot be empty. Specify valid allow regex list." + INVALID_ALLOW_REGEX= f"{error_prefix} Validation error. Invalid allow regex. Specify valid allow regex at index {{}}." + EMPTY_RESTRICT_REGEX_LIST= f"{error_prefix} Validation error. Restrict regex list cannot be empty. Specify valid restrict regex list." + INVALID_RESTRICT_REGEX= f"{error_prefix} Validation error. Invalid restrict regex. Specify valid restrict regex at index {{}}." + INVALID_OUTPUT_PROCESSED_IMAGE= f"{error_prefix} Validation error. Invalid output processed image. Specify valid output processed image as boolean." + INVALID_OUTPUT_OCR_TEXT= f"{error_prefix} Validation error. Invalid output ocr text. Specify valid output ocr text as boolean." + INVALID_MASKING_METHOD= f"{error_prefix} Validation error. Invalid masking method. Specify valid masking method as MaskingMethod enum." + INVALID_PIXEL_DENSITY= f"{error_prefix} Validation error. Invalid pixel density. Specify valid pixel density as number." + INVALID_OUTPUT_TRANSCRIPTION= f"{error_prefix} Validation error. Invalid output transcription. Specify valid output transcription as DetectOutputTranscriptions enum." + INVALID_BLEEP_TYPE= f"{error_prefix} Validation error. Invalid type of bleep. Specify bleep as Bleep object." + INVALID_BLEEP_GAIN= f"{error_prefix} Validation error. Invalid bleep gain. Specify valid bleep gain as a number." + INVALID_BLEEP_FREQUENCY= f"{error_prefix} Validation error. Invalid bleep frequency. Specify valid bleep frequency as a number." + INVALID_BLEEP_START_PADDING= f"{error_prefix} Validation error. Invalid bleep start padding. Specify valid bleep start padding as a number." + INVALID_BLEEP_STOP_PADDING= f"{error_prefix} Validation error. Invalid bleep stop padding. Specify valid bleep stop padding as a number." + INVALID_OUTPUT_PROCESSED_AUDIO= f"{error_prefix} Validation error. Invalid output processed audio. Specify valid output processed audio as boolean." + INVALID_MAX_RESOLUTION= f"{error_prefix} Validation error. Invalid max resolution. Specify valid max resolution as string." + INVALID_BLEEP= f"{error_prefix} Validation error. Invalid bleep. Specify valid bleep as object." + INVALID_FILE_OR_ENCODED_FILE= f"{error_prefix} . Error while decoding base64 and saving file" + INVALID_FILE_TYPE = f"{error_prefix} Validation error. Invalid file type. Specify a valid file type." + INVALID_FILE_NAME= f"{error_prefix} Validation error. Invalid file name. Specify a valid file name." + INVALID_FILE_PATH= f"{error_prefix} Validation error. Invalid file path. Specify a valid file path." + INVALID_DEIDENTIFY_FILE_PATH= f"{error_prefix} Validation error. Invalid file path. Specify a valid file path." + INVALID_BASE64_HEADER= f"{error_prefix} Validation error. Invalid base64 header. Specify a valid base64 header." + INVALID_WAIT_TIME= f"{error_prefix} Validation error. Invalid wait time. Specify a valid wait time as number and should not be greater than 64 secs." + INVALID_OUTPUT_DIRECTORY= f"{error_prefix} Validation error. Invalid output directory. Specify a valid output directory as string." + INVALID_OUTPUT_DIRECTORY_PATH= f"{error_prefix} Validation error. Invalid output directory path. Specify a valid output directory path as string." + EMPTY_RUN_ID= f"{error_prefix} Validation error. Run id cannot be empty. Specify a valid run id." + INVALID_RUN_ID= f"{error_prefix} Validation error. Invalid run id. Specify a valid run id as string." + INTERNAL_SERVER_ERROR= f"{error_prefix}. Internal server error. {{}}." + GET_DETECT_RUN_FAILED = f"{error_prefix} Get detect run operation failed." + BASE_SKYFLOW_INSTANTIATION_NOT_ALLOWED = f"{error_prefix} BaseSkyflow cannot be instantiated directly. Build a concrete Skyflow class via make_skyflow_class() instead." + + class Info(Enum): + CLIENT_INITIALIZED = f"{INFO}: [{error_prefix}] Initialized skyflow client." + VALIDATING_VAULT_CONFIG = f"{INFO}: [{error_prefix}] Validating vault config." + VALIDATING_CONNECTION_CONFIG = f"{INFO}: [{error_prefix}] Validating connection config." + UNABLE_TO_GENERATE_SDK_METRIC = f"{INFO}: [{error_prefix}] Unable to generate {{}} metric." + VAULT_CONTROLLER_INITIALIZED = f"{INFO}: [{error_prefix}] Initialized vault controller with vault ID {{}}." + CONNECTION_CONTROLLER_INITIALIZED = f"{INFO}: [{error_prefix}] Initialized connection controller with connection ID {{}}." + DETECT_CONTROLLER_INITIALIZED = f"{INFO}: [{error_prefix}] Initialized detect controller with vault ID {{}}." + VAULT_CONFIG_EXISTS = f"{INFO}: [{error_prefix}] Vault config with vault ID {{}} already exists." + VAULT_CONFIG_DOES_NOT_EXIST = f"{INFO}: [{error_prefix}] Vault config with vault ID {{}} doesn't exist." + CONNECTION_CONFIG_EXISTS = f"{INFO}: [{error_prefix}] Connection config with connection ID {{}} already exists." + CONNECTION_CONFIG_DOES_NOT_EXIST = f"{INFO}: [{error_prefix}] Connection config with connection ID {{}} doesn't exist." + LOGGER_SETUP_DONE = f"{INFO}: [{error_prefix}] Set up logger." + CURRENT_LOG_LEVEL = f"{INFO}: [{error_prefix}] Current log level is {{}}." + + BEARER_TOKEN_EXPIRED = f"{INFO}: [{error_prefix}] Bearer token is expired." + GET_BEARER_TOKEN_TRIGGERED = f"{INFO}: [{error_prefix}] generate_bearer_token method triggered." + GET_BEARER_TOKEN_SUCCESS = f"{INFO}: [{error_prefix}] Bearer token generated." + GET_SIGNED_DATA_TOKENS_TRIGGERED = f"{INFO}: [{error_prefix}] generate_signed_data_tokens method triggered." + GET_SIGNED_DATA_TOKEN_SUCCESS = f"{INFO}: [{error_prefix}] Signed data tokens generated." + GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_STRING_TRIGGERED = f"{INFO}: [{error_prefix}] generate bearer_token_from_credential_string method triggered." + REUSE_BEARER_TOKEN = f"{INFO}: [{error_prefix}] Reusing bearer token." + + VALIDATE_DEIDENTIFY_FILE_REQUEST = f"{INFO}: [{error_prefix}] Validating deidentify file request." + DETECT_FILE_TRIGGERED = f"{INFO}: [{error_prefix}] Detect file method triggered." + DETECT_FILE_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Deidentify file request resolved." + DETECT_FILE_SUCCESS = f"{INFO}: [{error_prefix}] File deidentified." + + VALIDATE_INSERT_REQUEST = f"{INFO}: [{error_prefix}] Validating insert request." + INSERT_TRIGGERED = f"{INFO}: [{error_prefix}] Insert method triggered." + INSERT_SUCCESS = f"{INFO}: [{error_prefix}] Data inserted." + INSERT_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Insert request resolved." + + VALIDATE_UPDATE_REQUEST = f"{INFO}: [{error_prefix}] Validating update request." + UPDATE_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Update request resolved." + UPDATE_SUCCESS = f"{INFO}: [{error_prefix}] Data updated." + UPDATE_TRIGGERED = f"{INFO}: [{error_prefix}] Update method triggered." + + DELETE_TRIGGERED = f"{INFO}: [{error_prefix}] Delete method triggered." + VALIDATING_DELETE_REQUEST = f"{INFO}: [{error_prefix}] Validating delete request." + DELETE_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Delete request resolved." + DELETE_SUCCESS = f"{INFO}: [{error_prefix}] Data deleted." + + GET_TRIGGERED = f"{INFO}: [{error_prefix}] Get method triggered." + VALIDATE_GET_REQUEST = f"{INFO}: [{error_prefix}] Validating get request." + GET_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Get request resolved." + GET_SUCCESS = f"{INFO}: [{error_prefix}] Data revealed." + + QUERY_TRIGGERED = f"{INFO}: [{error_prefix}] Query method triggered." + VALIDATING_QUERY_REQUEST = f"{INFO}: [{error_prefix}] Validating query request." + QUERY_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Query request resolved." + QUERY_SUCCESS = f"{INFO}: [{error_prefix}] Query executed." + + DETOKENIZE_TRIGGERED = f"{INFO}: [{error_prefix}] Detokenize method triggered." + VALIDATE_DETOKENIZE_REQUEST = f"{INFO}: [{error_prefix}] Validating detokenize request." + DETOKENIZE_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Detokenize request resolved." + DETOKENIZE_SUCCESS = f"{INFO}: [{error_prefix}] Data detokenized." + + TOKENIZE_TRIGGERED = f"{INFO}: [{error_prefix}] Tokenize method triggered." + VALIDATING_TOKENIZE_REQUEST = f"{INFO}: [{error_prefix}] Validating tokenize request." + TOKENIZE_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Tokenize request resolved." + TOKENIZE_SUCCESS = f"{INFO}: [{error_prefix}] Data tokenized." + + FILE_UPLOAD_TRIGGERED = f"{INFO}: [{error_prefix}] File upload method triggered." + VALIDATING_FILE_UPLOAD_REQUEST = f"{INFO}: [{error_prefix}] Validating file upload request." + FILE_UPLOAD_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] File upload request resolved." + FILE_UPLOAD_SUCCESS = f"{INFO}: [{error_prefix}] File uploaded successfully." + + INVOKE_CONNECTION_TRIGGERED = f"{INFO}: [{error_prefix}] Invoke connection method triggered." + VALIDATING_INVOKE_CONNECTION_REQUEST = f"{INFO}: [{error_prefix}] Validating invoke connection request." + INVOKE_CONNECTION_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Invoke connection request resolved." + INVOKE_CONNECTION_SUCCESS = f"{INFO}: [{error_prefix}] Invoke Connection Success." + + DEIDENTIFY_TEXT_TRIGGERED = f"{INFO}: [{error_prefix}] Deidentify text method triggered." + VALIDATING_DEIDENTIFY_TEXT_INPUT = f"{INFO}: [{error_prefix}] Validating deidentify text input." + DEIDENTIFY_TEXT_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Deidentify text request is resolved." + DEIDENTIFY_TEXT_SUCCESS = f"{INFO}: [{error_prefix}] Data deidentified." + + REIDENTIFY_TEXT_TRIGGERED = f"{INFO}: [{error_prefix}] Reidentify text method triggered." + VALIDATING_REIDENTIFY_TEXT_INPUT = f"{INFO}: [{error_prefix}] Validating reidentify text input." + REIDENTIFY_TEXT_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Reidentify text request is resolved." + REIDENTIFY_TEXT_SUCCESS = f"{INFO}: [{error_prefix}] Data reidentified." + + DEIDENTIFY_FILE_TRIGGERED = f"{INFO}: [{error_prefix}] Deidentify file triggered." + VALIDATING_DETECT_FILE_INPUT = f"{INFO}: [{error_prefix}] Validating deidentify file input." + DEIDENTIFY_FILE_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Deidentify file request is resolved." + DEIDENTIFY_FILE_SUCCESS = f"{INFO}: [{error_prefix}] File deidentified." + + GET_DETECT_RUN_TRIGGERED = f"{INFO}: [{error_prefix}] Get detect run triggered." + VALIDATING_GET_DETECT_RUN_INPUT = f"{INFO}: [{error_prefix}] Validating get detect run input." + GET_DETECT_RUN_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Get detect run request is resolved." + GET_DETECT_RUN_SUCCESS = f"{INFO}: [{error_prefix}] Get detect run success." + + DETECT_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Detect request is resolved." + + class ErrorLogs(Enum): + INVALID_LOG_LEVEL = f"{ERROR}: [{error_prefix}] Invalid log level. Specify a valid log level." + INVALID_KEY = f"{ERROR}: [{error_prefix}] Invalid key {{}} in config." + VAULTID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid vault config. Vault ID is required." + EMPTY_VAULTID = f"{ERROR}: [{error_prefix}] Invalid vault config. Vault ID can not be empty." + CLUSTER_ID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid vault config. Cluster ID is required." + EMPTY_CLUSTER_ID = f"{ERROR}: [{error_prefix}] Invalid vault config. Cluster ID can not be empty." + ENV_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid vault config. Env is required." + CONNECTION_ID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid connection config. Connection ID is required." + EMPTY_CONNECTION_ID = f"{ERROR}: [{error_prefix}] Invalid connection config. Connection ID can not be empty." + CONNECTION_URL_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid connection config. Connection URL is required." + EMPTY_CONNECTION_URL = f"{ERROR}: [{error_prefix}] Invalid connection config. Connection URL can not be empty." + INVALID_CONNECTION_URL = f"{ERROR}: [{error_prefix}] Invalid connection config. Connection URL is not a valid URL." + EMPTY_CREDENTIALS_PATH = f"{ERROR}: [{error_prefix}] Invalid credentials. Credentials path can not be empty." + EMPTY_CREDENTIALS_STRING = f"{ERROR}: [{error_prefix}] Invalid credentials. Credentials string can not be empty." + EMPTY_TOKEN_VALUE = f"{ERROR}: [{error_prefix}] Invalid credentials. Token can not be empty." + EMPTY_API_KEY_VALUE = f"{ERROR}: [{error_prefix}] Invalid credentials. Api key can not be empty." + INVALID_API_KEY = f"{ERROR}: [{error_prefix}] Invalid credentials. Api key is invalid." + + INVALID_BEARER_TOKEN = f"{ERROR}: [{error_prefix}] Bearer token is invalid or expired." + INVALID_CREDENTIALS_FILE = f"{ERROR}: [{error_prefix}] Credentials file is either null or an invalid file." + INVALID_CREDENTIALS_STRING_FORMAT = f"{ERROR}: [{error_prefix}] Credentials string in not in a valid JSON string format." + PRIVATE_KEY_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Private key is required." + CLIENT_ID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Client ID is required." + KEY_ID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Key ID is required." + TOKEN_URI_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Token URI is required." + INVALID_TOKEN_URI = f"{ERROR}: [{error_prefix}] Invalid value for token URI in credentials." + FAILED_TO_GET_BEARER_TOKEN = f"{ERROR}: [{error_prefix}] Failed to generate bearer token." + UNAUTHORIZED_ERROR_IN_GETTING_BEARER_TOKEN = f"{ERROR}: [{error_prefix}] Authorization failed while retrieving the bearer token." + + + TABLE_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Table is required." + EMPTY_TABLE_NAME =f"{ERROR}: [{error_prefix}] Invalid {{}} request. Table name can not be empty." + VALUES_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Values are required." + EMPTY_VALUES = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Values can not be empty." + EMPTY_OR_NULL_VALUE_IN_VALUES = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Value can not be null or empty in values for key {{}}." + EMPTY_OR_NULL_KEY_IN_VALUES = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Key can not be null or empty in values." + EMPTY_UPSERT = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Upsert can not be empty." + HOMOGENOUS_NOT_SUPPORTED_WITH_UPSERT = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Homogenous is not supported when upsert is passed." + EMPTY_TOKENS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens can not be empty." + EMPTY_OR_NULL_VALUE_IN_TOKENS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Value can not be null or empty in tokens for key {{}}." + EMPTY_OR_NULL_KEY_IN_TOKENS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Key can not be null or empty in tokens." + MISMATCH_OF_FIELDS_AND_TOKENS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Keys for values and tokens are not matching." + FILE_UPLOAD_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] File upload failed." + + EMPTY_IDS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Ids can not be empty." + EMPTY_OR_NULL_ID_IN_IDS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Id can not be null or empty in ids at index {{}}." + TOKENIZATION_NOT_SUPPORTED_WITH_REDACTION= f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokenization is not supported when redaction is applied." + TOKENIZATION_SUPPORTED_ONLY_WITH_IDS=f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokenization is not supported when column name and values are passed." + TOKENS_NOT_ALLOWED_WITH_BYOT_DISABLE = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens are not allowed when token_mode is DISABLE." + INSUFFICIENT_TOKENS_PASSED_FOR_BYOT_ENABLE_STRICT =f"{ERROR}: [{error_prefix}] Invalid {{}} request. For token_mode as ENABLE_STRICT, tokens should be passed for all fields." + TOKENS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Tokens are required." + EMPTY_FIELDS = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Fields can not be empty." + EMPTY_OFFSET = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Offset ca not be empty." + NEITHER_IDS_NOR_COLUMN_NAME_PASSED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Neither ids nor column name and values are passed." + BOTH_IDS_AND_COLUMN_NAME_PASSED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Both ids and column name and values are passed." + COLUMN_NAME_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Column name is required when column values are passed." + COLUMN_VALUES_IS_REQUIRED_GET = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Column values are required when column name is passed." + SKYFLOW_ID_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Skyflow Id is required." + EMPTY_SKYFLOW_ID = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Skyflow Id can not be empty." + + COLUMN_VALUES_IS_REQUIRED_TOKENIZE = f"{ERROR}: [{error_prefix}] Invalid {{}} request. column_values are required." + EMPTY_COLUMN_GROUP_IN_COLUMN_VALUES = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Column group can not be null or empty in column values at index %s2." + + EMPTY_QUERY= f"{ERROR}: [{error_prefix}] Invalid {{}} request. Query can not be empty." + QUERY_IS_REQUIRED = f"{ERROR}: [{error_prefix}] Invalid {{}} request. Query is required." + + INSERT_RECORDS_REJECTED = f"{ERROR}: [{error_prefix}] Insert call resulted in failure." + DETOKENIZE_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Detokenize request resulted in failure." + DELETE_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Delete request resulted in failure." + TOKENIZE_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Tokenize request resulted in failure." + UPDATE_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Update request resulted in failure." + QUERY_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Query request resulted in failure." + GET_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Get request resulted in failure." + INVOKE_CONNECTION_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Invoke connection request resulted in failure." + + EMPTY_RUN_ID = f"{ERROR}: [{error_prefix}] Validation error. Run id cannot be empty. Specify a valid run id." + INVALID_RUN_ID = f"{ERROR}: [{error_prefix}] Validation error. Invalid run id. Specify a valid run id as string." + DEIDENTIFY_FILE_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Deidentify file resulted in failure." + DETECT_RUN_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Detect get run resulted in failure." + DEIDENTIFY_TEXT_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Deidentify text resulted in failure." + SAVING_DEIDENTIFY_FILE_FAILED = f"{ERROR}: [{error_prefix}] Error while saving deidentified file to output directory." + REIDENTIFY_TEXT_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Reidentify text resulted in failure." + DETECT_FILE_REQUEST_REJECTED = f"{ERROR}: [{error_prefix}] Deidentify file resulted in failure." + EMPTY_FILE_COLUMN_NAME = f"{ERROR}: [{error_prefix}] Empty column name in FILE_UPLOAD" + + class Interface(Enum): + INSERT = "INSERT" + GET = "GET" + QUERY = "QUERY" + DETOKENIZE = " DETOKENIZE" + TOKENIZE = "TOKENIZE" + UPDATE = "UPDATE" + DELETE = "DELETE" + + class HttpStatus(Enum): + BAD_REQUEST = "Bad Request" + + class Warning(Enum): + DETOKENIZE_REDACTION_KEY_DEPRECATED = ( + f"{WARN}: [{error_prefix}] 'redaction' key in detokenize data is deprecated and will be removed in a future version. Use 'redaction_type' instead." + ) + UPDATE_LOG_LEVEL_DEPRECATED = ( + f"{WARN}: [{error_prefix}] Skyflow.update_log_level() is deprecated. " + "Use Skyflow.set_log_level() instead." + ) + FILE_UPLOAD_REQUEST_ARG_ORDER_DEPRECATED = ( + f"{WARN}: [{error_prefix}] FileUploadRequest: argument order changed. " + "Old positional order: (table, skyflow_id, column_name). " + "New order: FileUploadRequest(table, column_name=..., skyflow_id=...)." + ) + + + diff --git a/common/utils/_utils.py b/common/utils/_utils.py new file mode 100644 index 00000000..4a1c0637 --- /dev/null +++ b/common/utils/_utils.py @@ -0,0 +1,50 @@ +import os +import re + +import dotenv +from dotenv import load_dotenv + +from common.errors import SkyflowError +from . import SkyflowMessages +from .constants import PROTOCOL, ApiKey +from .enums import Env, EnvUrls +from .logger import log_error_log + +invalid_input_error_code = SkyflowMessages.ErrorCodes.INVALID_INPUT.value + + +def get_credentials(config_level_creds=None, common_skyflow_creds=None, logger=None): + if config_level_creds is not None: + return config_level_creds + if common_skyflow_creds is not None: + return common_skyflow_creds + dotenv_path = dotenv.find_dotenv(usecwd=True) + if dotenv_path: + load_dotenv(dotenv_path) + env_skyflow_credentials = os.getenv("SKYFLOW_CREDENTIALS") + if env_skyflow_credentials: + env_creds = env_skyflow_credentials.strip().replace('\n', '\\n') + return {'credentials_string': env_creds} + raise SkyflowError(SkyflowMessages.Error.INVALID_CREDENTIALS.value, invalid_input_error_code) + + +def validate_api_key(api_key: str, logger=None) -> bool: + if len(api_key) != ApiKey.LENGTH: + log_error_log(SkyflowMessages.ErrorLogs.INVALID_API_KEY.value, logger=logger) + return False + api_key_pattern = re.compile(r'^sky-[a-zA-Z0-9]{5}-[a-fA-F0-9]{32}$') + + return bool(api_key_pattern.match(api_key)) + + +def get_vault_url(cluster_id, env, vault_id, logger=None): + if not cluster_id or not isinstance(cluster_id, str) or not cluster_id.strip(): + raise SkyflowError(SkyflowMessages.Error.INVALID_CLUSTER_ID.value.format(vault_id), invalid_input_error_code) + + if env not in Env: + raise SkyflowError(SkyflowMessages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code) + + base_url = EnvUrls[env.name].value + protocol = PROTOCOL + + return f"{protocol}://{cluster_id}.{base_url}" diff --git a/skyflow/utils/constants.py b/common/utils/constants.py similarity index 100% rename from skyflow/utils/constants.py rename to common/utils/constants.py diff --git a/skyflow/utils/enums/__init__.py b/common/utils/enums/__init__.py similarity index 100% rename from skyflow/utils/enums/__init__.py rename to common/utils/enums/__init__.py diff --git a/skyflow/utils/enums/content_types.py b/common/utils/enums/content_types.py similarity index 100% rename from skyflow/utils/enums/content_types.py rename to common/utils/enums/content_types.py diff --git a/skyflow/utils/enums/detect_entities.py b/common/utils/enums/detect_entities.py similarity index 100% rename from skyflow/utils/enums/detect_entities.py rename to common/utils/enums/detect_entities.py diff --git a/skyflow/utils/enums/detect_output_transcriptions.py b/common/utils/enums/detect_output_transcriptions.py similarity index 100% rename from skyflow/utils/enums/detect_output_transcriptions.py rename to common/utils/enums/detect_output_transcriptions.py diff --git a/skyflow/utils/enums/env.py b/common/utils/enums/env.py similarity index 100% rename from skyflow/utils/enums/env.py rename to common/utils/enums/env.py diff --git a/skyflow/utils/enums/log_level.py b/common/utils/enums/log_level.py similarity index 100% rename from skyflow/utils/enums/log_level.py rename to common/utils/enums/log_level.py diff --git a/skyflow/utils/enums/masking_method.py b/common/utils/enums/masking_method.py similarity index 100% rename from skyflow/utils/enums/masking_method.py rename to common/utils/enums/masking_method.py diff --git a/skyflow/utils/enums/redaction_type.py b/common/utils/enums/redaction_type.py similarity index 100% rename from skyflow/utils/enums/redaction_type.py rename to common/utils/enums/redaction_type.py diff --git a/skyflow/utils/enums/request_method.py b/common/utils/enums/request_method.py similarity index 100% rename from skyflow/utils/enums/request_method.py rename to common/utils/enums/request_method.py diff --git a/skyflow/utils/enums/token_mode.py b/common/utils/enums/token_mode.py similarity index 100% rename from skyflow/utils/enums/token_mode.py rename to common/utils/enums/token_mode.py diff --git a/skyflow/utils/enums/token_type.py b/common/utils/enums/token_type.py similarity index 100% rename from skyflow/utils/enums/token_type.py rename to common/utils/enums/token_type.py diff --git a/skyflow/utils/logger/__init__.py b/common/utils/logger/__init__.py similarity index 100% rename from skyflow/utils/logger/__init__.py rename to common/utils/logger/__init__.py diff --git a/skyflow/utils/logger/_log_helpers.py b/common/utils/logger/_log_helpers.py similarity index 100% rename from skyflow/utils/logger/_log_helpers.py rename to common/utils/logger/_log_helpers.py diff --git a/skyflow/utils/logger/_logger.py b/common/utils/logger/_logger.py similarity index 100% rename from skyflow/utils/logger/_logger.py rename to common/utils/logger/_logger.py diff --git a/common/utils/validations/__init__.py b/common/utils/validations/__init__.py new file mode 100644 index 00000000..d49cc5de --- /dev/null +++ b/common/utils/validations/__init__.py @@ -0,0 +1,9 @@ +from ._validations import ( + validate_required_field, + validate_api_key, + validate_credentials, + validate_log_level, + validate_keys, + validate_vault_config, + validate_update_vault_config, +) diff --git a/common/utils/validations/_validations.py b/common/utils/validations/_validations.py new file mode 100644 index 00000000..f600b6a0 --- /dev/null +++ b/common/utils/validations/_validations.py @@ -0,0 +1,233 @@ +from common.errors import SkyflowError +from common.service_account import is_expired +from common.utils import SkyflowMessages +from common.utils.constants import ApiKey, ConfigField, CredentialField, OptionField +from common.utils.enums import Env, LogLevel +from common.utils.logger import log_error_log, log_info +from common.utils._helpers import is_valid_url + +invalid_input_error_code = SkyflowMessages.ErrorCodes.INVALID_INPUT.value + +VALID_VAULT_CONFIG_KEYS = [ + ConfigField.VAULT_ID, + ConfigField.CLUSTER_ID, + ConfigField.CREDENTIALS, + ConfigField.ENV, +] + + +def validate_required_field(logger, config, field_name, expected_type, empty_error, invalid_error, messages=None): + messages = messages or SkyflowMessages + field_value = config.get(field_name) + + if field_name not in config or not isinstance(field_value, expected_type): + if field_name == ConfigField.VAULT_ID: + log_error_log(messages.ErrorLogs.VAULTID_IS_REQUIRED.value, logger) + if field_name == ConfigField.CLUSTER_ID: + log_error_log(messages.ErrorLogs.CLUSTER_ID_IS_REQUIRED.value, logger) + if field_name == OptionField.CONNECTION_ID: + log_error_log(messages.ErrorLogs.CONNECTION_ID_IS_REQUIRED.value, logger) + if field_name == OptionField.CONNECTION_URL: + log_error_log(messages.ErrorLogs.INVALID_CONNECTION_URL.value, logger) + raise SkyflowError(invalid_error, invalid_input_error_code) + + if isinstance(field_value, str) and not field_value.strip(): + if field_name == ConfigField.VAULT_ID: + log_error_log(messages.ErrorLogs.EMPTY_VAULTID.value, logger) + if field_name == ConfigField.CLUSTER_ID: + log_error_log(messages.ErrorLogs.EMPTY_CLUSTER_ID.value, logger) + if field_name == OptionField.CONNECTION_ID: + log_error_log(messages.ErrorLogs.EMPTY_CONNECTION_ID.value, logger) + if field_name == OptionField.CONNECTION_URL: + log_error_log(messages.ErrorLogs.EMPTY_CONNECTION_URL.value, logger) + if field_name == CredentialField.PATH: + log_error_log(messages.ErrorLogs.EMPTY_CREDENTIALS_PATH.value, logger) + if field_name == CredentialField.CREDENTIALS_STRING: + log_error_log(messages.ErrorLogs.EMPTY_CREDENTIALS_STRING.value, logger) + if field_name == CredentialField.TOKEN: + log_error_log(messages.ErrorLogs.EMPTY_TOKEN_VALUE.value, logger) + if field_name == CredentialField.API_KEY: + log_error_log(messages.ErrorLogs.EMPTY_API_KEY_VALUE.value, logger) + raise SkyflowError(empty_error, invalid_input_error_code) + + +def validate_api_key(api_key: str, logger=None, messages=None) -> bool: + messages = messages or SkyflowMessages + if not api_key.startswith(ApiKey.SKY_PREFIX): + log_error_log(messages.ErrorLogs.INVALID_API_KEY.value, logger=logger) + return False + + if len(api_key) != ApiKey.LENGTH: + log_error_log(messages.ErrorLogs.INVALID_API_KEY.value, logger=logger) + return False + + return True + + +def validate_credentials(logger, credentials, config_id_type=None, config_id=None, messages=None): + messages = messages or SkyflowMessages + key_present = [k for k in [CredentialField.PATH, CredentialField.TOKEN, CredentialField.CREDENTIALS_STRING, CredentialField.API_KEY] if credentials.get(k)] + + if len(key_present) == 0: + error_message = ( + messages.Error.INVALID_CREDENTIALS_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else + messages.Error.INVALID_CREDENTIALS.value + ) + log_error_log(error_message, logger) + raise SkyflowError(error_message, invalid_input_error_code) + elif len(key_present) > 1: + error_message = ( + messages.Error.MULTIPLE_CREDENTIALS_PASSED_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else + messages.Error.MULTIPLE_CREDENTIALS_PASSED.value + ) + log_error_log(error_message, logger) + raise SkyflowError(error_message, invalid_input_error_code) + + if CredentialField.ROLES in credentials: + validate_required_field( + logger, credentials, CredentialField.ROLES, list, + messages.Error.INVALID_ROLES_KEY_TYPE_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_ROLES_KEY_TYPE.value, + messages.Error.EMPTY_ROLES_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.EMPTY_ROLES.value, + messages=messages) + + if CredentialField.CONTEXT in credentials: + validate_required_field( + logger, credentials, CredentialField.CONTEXT, str, + messages.Error.EMPTY_CONTEXT_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.EMPTY_CONTEXT.value, + messages.Error.INVALID_CONTEXT_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_CONTEXT.value, + messages=messages) + + if CredentialField.CREDENTIALS_STRING in credentials: + validate_required_field( + logger, credentials, CredentialField.CREDENTIALS_STRING, str, + messages.Error.EMPTY_CREDENTIALS_STRING_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.EMPTY_CREDENTIALS_STRING.value, + messages.Error.INVALID_CREDENTIALS_STRING_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_CREDENTIALS_STRING.value, + messages=messages) + elif CredentialField.PATH in credentials: + validate_required_field( + logger, credentials, CredentialField.PATH, str, + messages.Error.EMPTY_CREDENTIAL_FILE_PATH_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.EMPTY_CREDENTIAL_FILE_PATH.value, + messages.Error.INVALID_CREDENTIAL_FILE_PATH_IN_CONFIG.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_CREDENTIAL_FILE_PATH.value, + messages=messages) + elif CredentialField.TOKEN in credentials: + validate_required_field( + logger, credentials, CredentialField.TOKEN, str, + messages.Error.EMPTY_CREDENTIALS_TOKEN.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.EMPTY_CREDENTIALS_TOKEN.value, + messages.Error.INVALID_CREDENTIALS_TOKEN.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_CREDENTIALS_TOKEN.value, + messages=messages) + if is_expired(credentials.get(CredentialField.TOKEN), logger): + log_error_log(messages.ErrorLogs.INVALID_BEARER_TOKEN.value, logger) + raise SkyflowError( + messages.Error.EXPIRED_BEARER_TOKEN.value + if config_id_type and config_id else messages.Error.EXPIRED_BEARER_TOKEN.value, + invalid_input_error_code + ) + elif CredentialField.API_KEY in credentials: + validate_required_field( + logger, credentials, CredentialField.API_KEY, str, + messages.Error.EMPTY_API_KEY.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.EMPTY_API_KEY.value, + messages.Error.INVALID_API_KEY.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_API_KEY.value, + messages=messages) + if not validate_api_key(credentials.get(CredentialField.API_KEY), logger, messages=messages): + raise SkyflowError(messages.Error.INVALID_API_KEY.value.format(config_id_type, config_id) + if config_id_type and config_id else messages.Error.INVALID_API_KEY.value, + invalid_input_error_code) + + if CredentialField.TOKEN_URI_OPTION in credentials: + token_uri = credentials.get(CredentialField.TOKEN_URI_OPTION) + if ( + token_uri is None + or not isinstance(token_uri, str) + or not is_valid_url(token_uri) + ): + log_error_log(messages.ErrorLogs.INVALID_TOKEN_URI.value, logger) + raise SkyflowError(messages.Error.INVALID_TOKEN_URI.value, invalid_input_error_code) + + +def validate_log_level(logger, log_level, messages=None): + messages = messages or SkyflowMessages + if not isinstance(log_level, LogLevel): + log_error_log(messages.ErrorLogs.INVALID_LOG_LEVEL.value, logger) + raise SkyflowError(messages.Error.INVALID_LOG_LEVEL.value, invalid_input_error_code) + + +def validate_keys(logger, config, config_keys, messages=None): + messages = messages or SkyflowMessages + for key in config.keys(): + if key not in config_keys: + log_error_log(messages.ErrorLogs.INVALID_KEY.value.format(key), logger) + raise SkyflowError(messages.Error.INVALID_KEY.value.format(key), invalid_input_error_code) + + +def validate_vault_config(logger, config, messages=None): + messages = messages or SkyflowMessages + log_info(messages.Info.VALIDATING_VAULT_CONFIG.value, logger) + validate_keys(logger, config, VALID_VAULT_CONFIG_KEYS, messages=messages) + + validate_required_field( + logger, config, ConfigField.VAULT_ID, str, + messages.Error.EMPTY_VAULT_ID.value, + messages.Error.INVALID_VAULT_ID.value, + messages=messages + ) + vault_id = config.get(ConfigField.VAULT_ID) + + validate_required_field( + logger, config, ConfigField.CLUSTER_ID, str, + messages.Error.EMPTY_CLUSTER_ID.value.format(vault_id), + messages.Error.INVALID_CLUSTER_ID.value.format(vault_id), + messages=messages + ) + + if ConfigField.CREDENTIALS in config and not config.get(ConfigField.CREDENTIALS): + raise SkyflowError(messages.Error.EMPTY_CREDENTIALS.value.format("vault", vault_id), invalid_input_error_code) + + if ConfigField.CREDENTIALS in config and config.get(ConfigField.CREDENTIALS): + validate_credentials(logger, config.get(ConfigField.CREDENTIALS), "vault", vault_id, messages=messages) + + if ConfigField.ENV in config and config.get(ConfigField.ENV) not in Env: + log_error_log(messages.ErrorLogs.ENV_IS_REQUIRED.value, logger) + raise SkyflowError(messages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code) + + return True + + +def validate_update_vault_config(logger, config, messages=None): + """Credentials are required on update (unlike on initial add, where they're optional).""" + messages = messages or SkyflowMessages + validate_keys(logger, config, VALID_VAULT_CONFIG_KEYS, messages=messages) + + validate_required_field( + logger, config, ConfigField.VAULT_ID, str, + messages.Error.EMPTY_VAULT_ID.value, + messages.Error.INVALID_VAULT_ID.value, + messages=messages + ) + vault_id = config.get(ConfigField.VAULT_ID) + + if ConfigField.CLUSTER_ID in config and not config.get(ConfigField.CLUSTER_ID): + raise SkyflowError(messages.Error.INVALID_CLUSTER_ID.value.format(vault_id), invalid_input_error_code) + + if ConfigField.ENV in config and config.get(ConfigField.ENV) not in Env: + raise SkyflowError(messages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code) + + if ConfigField.CREDENTIALS not in config: + raise SkyflowError(messages.Error.EMPTY_CREDENTIALS.value.format("vault", vault_id), invalid_input_error_code) + + validate_credentials(logger, config.get(ConfigField.CREDENTIALS), "vault", vault_id, messages=messages) + + return True diff --git a/common/vault/base_vault_client.py b/common/vault/base_vault_client.py new file mode 100644 index 00000000..8ba044ac --- /dev/null +++ b/common/vault/base_vault_client.py @@ -0,0 +1,116 @@ +from abc import ABC, abstractmethod + +from common.service_account import generate_bearer_token, generate_bearer_token_from_creds, is_expired +from common.utils import get_credentials, SkyflowMessages +from common.utils.logger import log_info +from common.utils.constants import OptionField, CredentialField, ConfigField + + +class IVaultClient(ABC): + @abstractmethod + def resolve_vault_url(self, cluster_id, env, vault_id, logger=None): + raise NotImplementedError + + @abstractmethod + def initialize_api_client(self, vault_url, bearer_token): + raise NotImplementedError + + +class BaseVaultClient(IVaultClient): + def __init__(self, config): + self._config = config + self._common_skyflow_credentials = None + self._log_level = None + self._api_client = None + self._logger = None + self._is_config_updated = False + self._bearer_token = None + self._credentials = None + self._vault_url = None + self._is_static_token = None + + def set_common_skyflow_credentials(self, credentials): + self._common_skyflow_credentials = credentials + + def set_logger(self, log_level, logger): + self._log_level = log_level + self._logger = logger + + def initialize_client_configuration(self): + if self._api_client is not None and not self._is_config_updated: + if self._is_static_token: + return + if self._bearer_token is not None and not is_expired(self._bearer_token): + return + + needs_reinit = self._api_client is None or self._is_config_updated + if needs_reinit: + self._credentials = get_credentials(self._config.get(ConfigField.CREDENTIALS), self._common_skyflow_credentials, logger=self._logger) + self._vault_url = self.resolve_vault_url(self._config.get(ConfigField.CLUSTER_ID), + self._config.get(ConfigField.ENV), + self._config.get(ConfigField.VAULT_ID), + logger=self._logger) + self._is_static_token = CredentialField.TOKEN in self._credentials or CredentialField.API_KEY in self._credentials + bearer_token = self.get_bearer_token(self._credentials) + self._bearer_token = bearer_token + if needs_reinit: + self.initialize_api_client(self._vault_url, bearer_token) + + def get_current_bearer_token(self): + return self._bearer_token + + def get_current_vault_url(self): + return self._vault_url + + def get_vault_id(self): + return self._config.get(ConfigField.VAULT_ID) + + def get_bearer_token(self, credentials): + if CredentialField.API_KEY in credentials: + return credentials.get(CredentialField.API_KEY) + elif CredentialField.TOKEN in credentials: + return credentials.get(CredentialField.TOKEN) + + options = { + OptionField.ROLE_IDS: self._config.get(OptionField.ROLES), + OptionField.CTX: self._config.get(OptionField.CTX) + } + if CredentialField.TOKEN_URI_OPTION in credentials and credentials.get(CredentialField.TOKEN_URI_OPTION): + options[CredentialField.TOKEN_URI_OPTION] = credentials.get(CredentialField.TOKEN_URI_OPTION) + + if self._bearer_token is None or self._is_config_updated or is_expired(self._bearer_token): + if CredentialField.PATH in credentials: + self._bearer_token, _ = generate_bearer_token( + credentials.get(CredentialField.PATH), + options, + self._logger + ) + else: + credentials_string = credentials.get(CredentialField.CREDENTIALS_STRING) + log_info(SkyflowMessages.Info.GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_STRING_TRIGGERED.value, self._logger) + self._bearer_token, _ = generate_bearer_token_from_creds( + credentials_string, + options, + self._logger + ) + self._is_config_updated = False + else: + log_info(SkyflowMessages.Info.REUSE_BEARER_TOKEN.value, self._logger) + + return self._bearer_token + + def update_config(self, config): + self._config.update(config) + self._is_config_updated = True + + def get_config(self): + return self._config + + def get_common_skyflow_credentials(self): + return self._common_skyflow_credentials + + def get_log_level(self): + return self._log_level + + def get_logger(self): + return self._logger diff --git a/common/vault/base_vault_controller.py b/common/vault/base_vault_controller.py new file mode 100644 index 00000000..f95dce11 --- /dev/null +++ b/common/vault/base_vault_controller.py @@ -0,0 +1,62 @@ +from abc import ABC, abstractmethod + +from common.errors import SkyflowError +from common.utils import SkyflowMessages as _CommonSkyflowMessages +from common.vault.data import BaseInsertRequest, BaseInsertResponse + +_INVALID_INPUT_ERROR_CODE = _CommonSkyflowMessages.ErrorCodes.INVALID_INPUT.value + + +class IVaultController(ABC): + + @abstractmethod + def insert(self, request: BaseInsertRequest) -> BaseInsertResponse: + raise NotImplementedError + + @abstractmethod + def get(self, request): + raise NotImplementedError + + @abstractmethod + def update(self, request): + raise NotImplementedError + + @abstractmethod + def delete(self, request): + raise NotImplementedError + + @abstractmethod + def query(self, request): + raise NotImplementedError + + @abstractmethod + def detokenize(self, request): + raise NotImplementedError + + +class BaseVaultController(IVaultController): + + _skyflow_messages = None + + def __init__(self, vault_client): + self._vault_client = vault_client + + def _validate_table_name_if_present(self, table): + if table is not None and (not isinstance(table, str) or not table.strip()): + raise SkyflowError( + self._skyflow_messages.Error.INVALID_TABLE_NAME_IN_INSERT.value, + _INVALID_INPUT_ERROR_CODE, + ) + + def _validate_field_values(self, values): + if not isinstance(values, dict) or not values: + raise SkyflowError( + self._skyflow_messages.Error.INVALID_RECORD_DATA_IN_INSERT.value, + _INVALID_INPUT_ERROR_CODE, + ) + for key, value in values.items(): + if not isinstance(key, str) or not key.strip(): + raise SkyflowError( + self._skyflow_messages.Error.EMPTY_KEY_IN_INSERT_DATA.value, + _INVALID_INPUT_ERROR_CODE, + ) diff --git a/common/vault/data/__init__.py b/common/vault/data/__init__.py new file mode 100644 index 00000000..23b585aa --- /dev/null +++ b/common/vault/data/__init__.py @@ -0,0 +1,2 @@ +from ._base_insert_request import BaseInsertRequest +from ._base_insert_response import BaseInsertResponse diff --git a/common/vault/data/_base_insert_request.py b/common/vault/data/_base_insert_request.py new file mode 100644 index 00000000..4172fd69 --- /dev/null +++ b/common/vault/data/_base_insert_request.py @@ -0,0 +1,7 @@ +from typing import Union + +class BaseInsertRequest: + def __init__(self, table: str, values: list, upsert: Union[str, dict] = None): + self.table = table + self.values = values + self.upsert = upsert \ No newline at end of file diff --git a/common/vault/data/_base_insert_response.py b/common/vault/data/_base_insert_response.py new file mode 100644 index 00000000..b0b60377 --- /dev/null +++ b/common/vault/data/_base_insert_response.py @@ -0,0 +1,11 @@ +class BaseInsertResponse: + + def __init__(self, inserted_fields=None, errors=None): + self.inserted_fields = inserted_fields + self.errors = errors + + def __repr__(self): + return f"{type(self).__name__}(inserted_fields={self.inserted_fields}, errors={self.errors})" + + def __str__(self): + return self.__repr__() diff --git a/flowvault/requirements.txt b/flowvault/requirements.txt new file mode 100644 index 00000000..7d2fb3a8 --- /dev/null +++ b/flowvault/requirements.txt @@ -0,0 +1,5 @@ +httpx>=0.21.2 +pydantic>= 1.9.2 +pydantic-core>=2.18.2 +typing_extensions>= 4.0.0 +coverage >= 7.8.0 diff --git a/flowvault/setup.py b/flowvault/setup.py new file mode 100644 index 00000000..e3fa571e --- /dev/null +++ b/flowvault/setup.py @@ -0,0 +1,84 @@ +''' + Copyright (c) 2022 Skyflow, Inc. +''' +import os +import shutil +import sys + +from setuptools import setup, find_packages +from setuptools.command.build_py import build_py as _build_py + + +if sys.version_info < (3, 9): + raise RuntimeError("skyflow requires Python 3.9+") +current_version = '1.0.0' + +HERE = os.path.abspath(os.path.dirname(__file__)) +REPO_ROOT = os.path.dirname(HERE) +COMMON_SRC = os.path.join(REPO_ROOT, 'common') + +with open(os.path.join(REPO_ROOT, 'README.md'), 'r', encoding='utf-8') as f: + long_description = f.read() + +_COMMON_EXCLUDE_DIRS = {'__pycache__', '.pytest_cache', 'tests', '.mypy_cache'} +_COMMON_EXCLUDE_FILES = {'setup.py', 'pyproject.toml', 'requirements.txt', '.gitignore'} +_COMMON_EXCLUDE_SUFFIXES = ('.egg-info',) + + +def _ignore_common_files(_directory, names): + ignored = set() + for name in names: + if name in _COMMON_EXCLUDE_DIRS or name in _COMMON_EXCLUDE_FILES: + ignored.add(name) + elif name.endswith(_COMMON_EXCLUDE_SUFFIXES): + ignored.add(name) + return ignored + + +class CustomBuildPy(_build_py): + """SK-2938 Option C bundling mechanism -- see v2/setup.py for full rationale. Bundles the + sibling common/ source tree into this variant's wheel; wheel builds only, not sdist.""" + + def run(self): + super().run() + dest = os.path.join(self.build_lib, 'common') + if os.path.exists(dest): + shutil.rmtree(dest) + shutil.copytree(COMMON_SRC, dest, ignore=_ignore_common_files) + + +setup( + name='skyflow-flowvault', + version=current_version, + author='Skyflow', + author_email='service-ops@skyflow.com', + packages=find_packages(where='.', exclude=['test*', 'samples*']), + package_data={ + 'skyflow_flowvault': ['py.typed'], + 'skyflow_flowvault.generated.rest': ['py.typed'], + }, + cmdclass={'build_py': CustomBuildPy}, + url='https://github.com/skyflowapi/skyflow-python/', + license='LICENSE', + description='Skyflow SDK for the Python programming language (v3 / flowservice API)', + long_description=long_description, + long_description_content_type='text/markdown', + install_requires=[ + 'pydantic >= 2.0.0', + 'typing-extensions >= 4.0.0', + 'PyJWT >= 2.12, < 3', + 'cryptography >= 44.0.2', + 'httpx >= 0.21.2', + 'python-dotenv >= 1.1.0, < 2', + # NOTE: 'requests' intentionally omitted -- only used today by v2's Connection + # controller, which isn't part of v3's scope this round. + ], + extras_require={ + 'dev': [ + 'codespell >= 2.4.1', + 'ruff >= 0.9.0', + 'pre-commit >= 4.3.0', + ] + }, + python_requires=">=3.9", +) diff --git a/skyflow/__init__.py b/flowvault/skyflow_flowvault/__init__.py similarity index 100% rename from skyflow/__init__.py rename to flowvault/skyflow_flowvault/__init__.py diff --git a/skyflow/client/__init__.py b/flowvault/skyflow_flowvault/client/__init__.py similarity index 100% rename from skyflow/client/__init__.py rename to flowvault/skyflow_flowvault/client/__init__.py diff --git a/flowvault/skyflow_flowvault/client/skyflow.py b/flowvault/skyflow_flowvault/client/skyflow.py new file mode 100644 index 00000000..59ff4d5f --- /dev/null +++ b/flowvault/skyflow_flowvault/client/skyflow.py @@ -0,0 +1,10 @@ +from common.client.utils import make_skyflow_class +from common.utils import SkyflowMessages +from skyflow_flowvault.vault.client.client import VaultClient +from skyflow_flowvault.vault.controller import VaultController + +Skyflow = make_skyflow_class( + vault_client_cls=VaultClient, + vault_controller_cls=VaultController, + skyflow_messages=SkyflowMessages, +) diff --git a/flowvault/skyflow_flowvault/error/__init__.py b/flowvault/skyflow_flowvault/error/__init__.py new file mode 100644 index 00000000..dd3ea070 --- /dev/null +++ b/flowvault/skyflow_flowvault/error/__init__.py @@ -0,0 +1,3 @@ +from common.errors import SkyflowError + +__all__ = ["SkyflowError"] diff --git a/tests/vault/__init__.py b/flowvault/skyflow_flowvault/generated/__init__.py similarity index 100% rename from tests/vault/__init__.py rename to flowvault/skyflow_flowvault/generated/__init__.py diff --git a/flowvault/skyflow_flowvault/generated/rest/__init__.py b/flowvault/skyflow_flowvault/generated/rest/__init__.py new file mode 100644 index 00000000..14ae395a --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/__init__.py @@ -0,0 +1,77 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .types import ( + FlowEnumUpdateType, + FlowTokenizeResponseObjectToken, + GoogleprotobufAny, + ProtobufNullValue, + RpcStatus, + V1ColumnRedactions, + V1DeleteResponse, + V1DeleteResponseObject, + V1DeleteTokenResponseObject, + V1ExecuteQueryRecordResponse, + V1ExecuteQueryResponse, + V1ExecuteQueryResponseMetadata, + V1FlowDeleteTokenResponse, + V1FlowDetokenizeResponse, + V1FlowDetokenizeResponseObject, + V1FlowTokenizeRequestObject, + V1FlowTokenizeResponse, + V1FlowTokenizeResponseObject, + V1FlowVaultMetricsData, + V1FlowVaultMetricsResponse, + V1GetRequestData, + V1GetResponse, + V1InsertRecordData, + V1InsertResponse, + V1RecordResponseObject, + V1TokenGroupRedactions, + V1UniqueValue, + V1UpdateRecordData, + V1UpdateResponse, + V1Upsert, +) +from . import flowservice, records +from .client import AsyncSkyflowAuth, SkyflowAuth +from .version import __version__ + +__all__ = [ + "AsyncSkyflowAuth", + "FlowEnumUpdateType", + "FlowTokenizeResponseObjectToken", + "GoogleprotobufAny", + "ProtobufNullValue", + "RpcStatus", + "SkyflowAuth", + "V1ColumnRedactions", + "V1DeleteResponse", + "V1DeleteResponseObject", + "V1DeleteTokenResponseObject", + "V1ExecuteQueryRecordResponse", + "V1ExecuteQueryResponse", + "V1ExecuteQueryResponseMetadata", + "V1FlowDeleteTokenResponse", + "V1FlowDetokenizeResponse", + "V1FlowDetokenizeResponseObject", + "V1FlowTokenizeRequestObject", + "V1FlowTokenizeResponse", + "V1FlowTokenizeResponseObject", + "V1FlowVaultMetricsData", + "V1FlowVaultMetricsResponse", + "V1GetRequestData", + "V1GetResponse", + "V1InsertRecordData", + "V1InsertResponse", + "V1RecordResponseObject", + "V1TokenGroupRedactions", + "V1UniqueValue", + "V1UpdateRecordData", + "V1UpdateResponse", + "V1Upsert", + "__version__", + "flowservice", + "records", +] diff --git a/flowvault/skyflow_flowvault/generated/rest/client.py b/flowvault/skyflow_flowvault/generated/rest/client.py new file mode 100644 index 00000000..0dc7a48c --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/client.py @@ -0,0 +1,120 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from .flowservice.client import AsyncFlowserviceClient, FlowserviceClient +from .records.client import AsyncRecordsClient, RecordsClient + + +class SkyflowAuth: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.Client] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.Client] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = SyncClientWrapper( + base_url=base_url, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.Client(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.Client(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.records = RecordsClient(client_wrapper=self._client_wrapper) + self.flowservice = FlowserviceClient(client_wrapper=self._client_wrapper) + + +class AsyncSkyflowAuth: + """ + Use this class to access the different functions within the SDK. You can instantiate any number of clients with different configuration that will propagate to these functions. + + Parameters + ---------- + base_url : str + The base url to use for requests from the client. + + headers : typing.Optional[typing.Dict[str, str]] + Additional headers to send with every request. + + timeout : typing.Optional[float] + The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced. + + follow_redirects : typing.Optional[bool] + Whether the default httpx client follows redirects or not, this is irrelevant if a custom httpx client is passed in. + + httpx_client : typing.Optional[httpx.AsyncClient] + The httpx client to use for making requests, a preconfigured client is used by default, however this is useful should you want to pass in any custom httpx configuration. + + Examples + -------- + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + """ + + def __init__( + self, + *, + base_url: str, + headers: typing.Optional[typing.Dict[str, str]] = None, + timeout: typing.Optional[float] = None, + follow_redirects: typing.Optional[bool] = True, + httpx_client: typing.Optional[httpx.AsyncClient] = None, + ): + _defaulted_timeout = ( + timeout if timeout is not None else 60 if httpx_client is None else httpx_client.timeout.read + ) + self._client_wrapper = AsyncClientWrapper( + base_url=base_url, + headers=headers, + httpx_client=httpx_client + if httpx_client is not None + else httpx.AsyncClient(timeout=_defaulted_timeout, follow_redirects=follow_redirects) + if follow_redirects is not None + else httpx.AsyncClient(timeout=_defaulted_timeout), + timeout=_defaulted_timeout, + ) + self.records = AsyncRecordsClient(client_wrapper=self._client_wrapper) + self.flowservice = AsyncFlowserviceClient(client_wrapper=self._client_wrapper) diff --git a/flowvault/skyflow_flowvault/generated/rest/core/__init__.py b/flowvault/skyflow_flowvault/generated/rest/core/__init__.py new file mode 100644 index 00000000..31bbb818 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/__init__.py @@ -0,0 +1,52 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .api_error import ApiError +from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper +from .datetime_utils import serialize_datetime +from .file import File, convert_file_dict_to_httpx_tuples, with_content_type +from .http_client import AsyncHttpClient, HttpClient +from .http_response import AsyncHttpResponse, HttpResponse +from .jsonable_encoder import jsonable_encoder +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, +) +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "AsyncHttpResponse", + "BaseClientWrapper", + "FieldMetadata", + "File", + "HttpClient", + "HttpResponse", + "IS_PYDANTIC_V2", + "RequestOptions", + "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", +] diff --git a/flowvault/skyflow_flowvault/generated/rest/core/api_error.py b/flowvault/skyflow_flowvault/generated/rest/core/api_error.py new file mode 100644 index 00000000..6f850a60 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/flowvault/skyflow_flowvault/generated/rest/core/client_wrapper.py b/flowvault/skyflow_flowvault/generated/rest/core/client_wrapper.py new file mode 100644 index 00000000..8f63f6ee --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/client_wrapper.py @@ -0,0 +1,73 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import httpx +from .http_client import AsyncHttpClient, HttpClient + + +class BaseClientWrapper: + def __init__( + self, + *, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + ): + self._headers = headers + self._base_url = base_url + self._timeout = timeout + + def get_headers(self) -> typing.Dict[str, str]: + headers: typing.Dict[str, str] = { + "X-Fern-Language": "Python", + "X-Fern-SDK-Name": "skyflow.generated.rest", + "X-Fern-SDK-Version": "0.0.10", + **(self.get_custom_headers() or {}), + } + return headers + + def get_custom_headers(self) -> typing.Optional[typing.Dict[str, str]]: + return self._headers + + def get_base_url(self) -> str: + return self._base_url + + def get_timeout(self) -> typing.Optional[float]: + return self._timeout + + +class SyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.Client, + ): + super().__init__(headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = HttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) + + +class AsyncClientWrapper(BaseClientWrapper): + def __init__( + self, + *, + headers: typing.Optional[typing.Dict[str, str]] = None, + base_url: str, + timeout: typing.Optional[float] = None, + httpx_client: httpx.AsyncClient, + ): + super().__init__(headers=headers, base_url=base_url, timeout=timeout) + self.httpx_client = AsyncHttpClient( + httpx_client=httpx_client, + base_headers=self.get_headers, + base_timeout=self.get_timeout, + base_url=self.get_base_url, + ) diff --git a/flowvault/skyflow_flowvault/generated/rest/core/datetime_utils.py b/flowvault/skyflow_flowvault/generated/rest/core/datetime_utils.py new file mode 100644 index 00000000..7c9864a9 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/flowvault/skyflow_flowvault/generated/rest/core/file.py b/flowvault/skyflow_flowvault/generated/rest/core/file.py new file mode 100644 index 00000000..44b0d27c --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/flowvault/skyflow_flowvault/generated/rest/core/force_multipart.py b/flowvault/skyflow_flowvault/generated/rest/core/force_multipart.py new file mode 100644 index 00000000..ae24ccff --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/force_multipart.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + + +class ForceMultipartDict(dict): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self): + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/flowvault/skyflow_flowvault/generated/rest/core/http_client.py b/flowvault/skyflow_flowvault/generated/rest/core/http_client.py new file mode 100644 index 00000000..e4173f99 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/http_client.py @@ -0,0 +1,543 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +import urllib.parse +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retryable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retryable_400s + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + # If you have an empty JSON body, you should just send None + return (json_body if json_body != {} else None), data_body if data_body != {} else None + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + response = self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + # Add the input to each of these and do None-safety checks + response = await self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + async with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/flowvault/skyflow_flowvault/generated/rest/core/http_response.py b/flowvault/skyflow_flowvault/generated/rest/core/http_response.py new file mode 100644 index 00000000..48a1798a --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/http_response.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +T = TypeVar("T") +"""Generic to represent the underlying type of the data wrapped by the HTTP response.""" + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/flowvault/skyflow_flowvault/generated/rest/core/jsonable_encoder.py b/flowvault/skyflow_flowvault/generated/rest/core/jsonable_encoder.py new file mode 100644 index 00000000..afee3662 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/jsonable_encoder.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/flowvault/skyflow_flowvault/generated/rest/core/pydantic_utilities.py b/flowvault/skyflow_flowvault/generated/rest/core/pydantic_utilities.py new file mode 100644 index 00000000..7db29500 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/pydantic_utilities.py @@ -0,0 +1,255 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +from collections import defaultdict +from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, Union, cast + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + from pydantic.v1.datetime_parse import parse_date as parse_date + from pydantic.v1.datetime_parse import parse_datetime as parse_datetime + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[attr-defined] + from pydantic.v1.typing import get_args as get_args + from pydantic.v1.typing import get_origin as get_origin + from pydantic.v1.typing import is_literal_type as is_literal_type + from pydantic.v1.typing import is_union as is_union +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + return adapter.validate_python(dealiased_object) + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.model_dump() + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.model_validator(mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/flowvault/skyflow_flowvault/generated/rest/core/query_encoder.py b/flowvault/skyflow_flowvault/generated/rest/core/query_encoder.py new file mode 100644 index 00000000..3183001d --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/flowvault/skyflow_flowvault/generated/rest/core/remove_none_from_dict.py b/flowvault/skyflow_flowvault/generated/rest/core/remove_none_from_dict.py new file mode 100644 index 00000000..c2298143 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/flowvault/skyflow_flowvault/generated/rest/core/request_options.py b/flowvault/skyflow_flowvault/generated/rest/core/request_options.py new file mode 100644 index 00000000..1b388044 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/request_options.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] diff --git a/flowvault/skyflow_flowvault/generated/rest/core/serialization.py b/flowvault/skyflow_flowvault/generated/rest/core/serialization.py new file mode 100644 index 00000000..c36e865c --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/core/serialization.py @@ -0,0 +1,276 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + try: + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The TypedDict contains a circular reference, so + # we use the __annotations__ attribute directly. + annotations = getattr(expected_type, "__annotations__", {}) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/skyflow/generated/rest/bin_lookup/__init__.py b/flowvault/skyflow_flowvault/generated/rest/flowservice/__init__.py similarity index 100% rename from skyflow/generated/rest/bin_lookup/__init__.py rename to flowvault/skyflow_flowvault/generated/rest/flowservice/__init__.py diff --git a/flowvault/skyflow_flowvault/generated/rest/flowservice/client.py b/flowvault/skyflow_flowvault/generated/rest/flowservice/client.py new file mode 100644 index 00000000..321f69bd --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/flowservice/client.py @@ -0,0 +1,855 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.flow_enum_update_type import FlowEnumUpdateType +from ..types.v_1_column_redactions import V1ColumnRedactions +from ..types.v_1_delete_response import V1DeleteResponse +from ..types.v_1_flow_delete_token_response import V1FlowDeleteTokenResponse +from ..types.v_1_flow_detokenize_response import V1FlowDetokenizeResponse +from ..types.v_1_flow_tokenize_request_object import V1FlowTokenizeRequestObject +from ..types.v_1_flow_tokenize_response import V1FlowTokenizeResponse +from ..types.v_1_flow_vault_metrics_response import V1FlowVaultMetricsResponse +from ..types.v_1_get_request_data import V1GetRequestData +from ..types.v_1_get_response import V1GetResponse +from ..types.v_1_insert_record_data import V1InsertRecordData +from ..types.v_1_insert_response import V1InsertResponse +from ..types.v_1_token_group_redactions import V1TokenGroupRedactions +from ..types.v_1_unique_value import V1UniqueValue +from ..types.v_1_update_record_data import V1UpdateRecordData +from ..types.v_1_update_response import V1UpdateResponse +from ..types.v_1_upsert import V1Upsert +from .raw_client import AsyncRawFlowserviceClient, RawFlowserviceClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class FlowserviceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawFlowserviceClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawFlowserviceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawFlowserviceClient + """ + return self._raw_client + + def delete( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1DeleteResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being deleted + + table_name : typing.Optional[str] + Name of the table where data is being deleted + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be deleted + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1DeleteResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.delete() + """ + _response = self._raw_client.delete( + vault_id=vault_id, + table_name=table_name, + skyflow_i_ds=skyflow_i_ds, + unique_values=unique_values, + request_options=request_options, + ) + return _response.data + + def get( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + column_redactions: typing.Optional[typing.Sequence[V1ColumnRedactions]] = OMIT, + columns: typing.Optional[typing.Sequence[str]] = OMIT, + limit: typing.Optional[int] = OMIT, + offset: typing.Optional[int] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + records: typing.Optional[typing.Sequence[V1GetRequestData]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1GetResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being fetched + + table_name : typing.Optional[str] + Name of the table where data is being fetched + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be fetched + + column_redactions : typing.Optional[typing.Sequence[V1ColumnRedactions]] + List of columns to be redacted. + + columns : typing.Optional[typing.Sequence[str]] + List of columns to be fetched. + + limit : typing.Optional[int] + Limit for the number of records to be fetched + + offset : typing.Optional[int] + Offset for the number of records to be fetched + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + records : typing.Optional[typing.Sequence[V1GetRequestData]] + List of records to be fetched. This field contains tableName and skyflowIDs belonging to the table. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1GetResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.get() + """ + _response = self._raw_client.get( + vault_id=vault_id, + table_name=table_name, + skyflow_i_ds=skyflow_i_ds, + column_redactions=column_redactions, + columns=columns, + limit=limit, + offset=offset, + unique_values=unique_values, + records=records, + request_options=request_options, + ) + return _response.data + + def insert( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1InsertRecordData]] = OMIT, + upsert: typing.Optional[V1Upsert] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1InsertResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + table_name : typing.Optional[str] + Name of the table where data is being inserted + + records : typing.Optional[typing.Sequence[V1InsertRecordData]] + List of data row wise that is to be inserted in the vault + + upsert : typing.Optional[V1Upsert] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1InsertResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.insert() + """ + _response = self._raw_client.insert( + vault_id=vault_id, table_name=table_name, records=records, upsert=upsert, request_options=request_options + ) + return _response.data + + def update( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1UpdateRecordData]] = OMIT, + update_type: typing.Optional[FlowEnumUpdateType] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1UpdateResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being updated + + table_name : typing.Optional[str] + Name of the table where data is being updated + + records : typing.Optional[typing.Sequence[V1UpdateRecordData]] + List of data row wise that is to be updated in the vault + + update_type : typing.Optional[FlowEnumUpdateType] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1UpdateResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.update() + """ + _response = self._raw_client.update( + vault_id=vault_id, + table_name=table_name, + records=records, + update_type=update_type, + request_options=request_options, + ) + return _response.data + + def deletetoken( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1FlowDeleteTokenResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID + + tokens : typing.Optional[typing.Sequence[str]] + Token value + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowDeleteTokenResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.deletetoken() + """ + _response = self._raw_client.deletetoken(vault_id=vault_id, tokens=tokens, request_options=request_options) + return _response.data + + def detokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + token_group_redactions: typing.Optional[typing.Sequence[V1TokenGroupRedactions]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1FlowDetokenizeResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where detokenizing + + tokens : typing.Optional[typing.Sequence[str]] + Token to be detokenized + + token_group_redactions : typing.Optional[typing.Sequence[V1TokenGroupRedactions]] + List of token groups to be redacted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowDetokenizeResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.detokenize() + """ + _response = self._raw_client.detokenize( + vault_id=vault_id, + tokens=tokens, + token_group_redactions=token_group_redactions, + request_options=request_options, + ) + return _response.data + + def tokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + data: typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1FlowTokenizeResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID. + + data : typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] + Data to be tokenized + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowTokenizeResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.tokenize() + """ + _response = self._raw_client.tokenize(vault_id=vault_id, data=data, request_options=request_options) + return _response.data + + def flowvaultmetrics( + self, *, vault_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> V1FlowVaultMetricsResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault to get metrics for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowVaultMetricsResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.flowservice.flowvaultmetrics() + """ + _response = self._raw_client.flowvaultmetrics(vault_id=vault_id, request_options=request_options) + return _response.data + + +class AsyncFlowserviceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawFlowserviceClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawFlowserviceClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawFlowserviceClient + """ + return self._raw_client + + async def delete( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1DeleteResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being deleted + + table_name : typing.Optional[str] + Name of the table where data is being deleted + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be deleted + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1DeleteResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.delete() + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete( + vault_id=vault_id, + table_name=table_name, + skyflow_i_ds=skyflow_i_ds, + unique_values=unique_values, + request_options=request_options, + ) + return _response.data + + async def get( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + column_redactions: typing.Optional[typing.Sequence[V1ColumnRedactions]] = OMIT, + columns: typing.Optional[typing.Sequence[str]] = OMIT, + limit: typing.Optional[int] = OMIT, + offset: typing.Optional[int] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + records: typing.Optional[typing.Sequence[V1GetRequestData]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1GetResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being fetched + + table_name : typing.Optional[str] + Name of the table where data is being fetched + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be fetched + + column_redactions : typing.Optional[typing.Sequence[V1ColumnRedactions]] + List of columns to be redacted. + + columns : typing.Optional[typing.Sequence[str]] + List of columns to be fetched. + + limit : typing.Optional[int] + Limit for the number of records to be fetched + + offset : typing.Optional[int] + Offset for the number of records to be fetched + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + records : typing.Optional[typing.Sequence[V1GetRequestData]] + List of records to be fetched. This field contains tableName and skyflowIDs belonging to the table. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1GetResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.get() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get( + vault_id=vault_id, + table_name=table_name, + skyflow_i_ds=skyflow_i_ds, + column_redactions=column_redactions, + columns=columns, + limit=limit, + offset=offset, + unique_values=unique_values, + records=records, + request_options=request_options, + ) + return _response.data + + async def insert( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1InsertRecordData]] = OMIT, + upsert: typing.Optional[V1Upsert] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1InsertResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + table_name : typing.Optional[str] + Name of the table where data is being inserted + + records : typing.Optional[typing.Sequence[V1InsertRecordData]] + List of data row wise that is to be inserted in the vault + + upsert : typing.Optional[V1Upsert] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1InsertResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.insert() + + + asyncio.run(main()) + """ + _response = await self._raw_client.insert( + vault_id=vault_id, table_name=table_name, records=records, upsert=upsert, request_options=request_options + ) + return _response.data + + async def update( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1UpdateRecordData]] = OMIT, + update_type: typing.Optional[FlowEnumUpdateType] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1UpdateResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being updated + + table_name : typing.Optional[str] + Name of the table where data is being updated + + records : typing.Optional[typing.Sequence[V1UpdateRecordData]] + List of data row wise that is to be updated in the vault + + update_type : typing.Optional[FlowEnumUpdateType] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1UpdateResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.update() + + + asyncio.run(main()) + """ + _response = await self._raw_client.update( + vault_id=vault_id, + table_name=table_name, + records=records, + update_type=update_type, + request_options=request_options, + ) + return _response.data + + async def deletetoken( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1FlowDeleteTokenResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID + + tokens : typing.Optional[typing.Sequence[str]] + Token value + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowDeleteTokenResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.deletetoken() + + + asyncio.run(main()) + """ + _response = await self._raw_client.deletetoken( + vault_id=vault_id, tokens=tokens, request_options=request_options + ) + return _response.data + + async def detokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + token_group_redactions: typing.Optional[typing.Sequence[V1TokenGroupRedactions]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1FlowDetokenizeResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where detokenizing + + tokens : typing.Optional[typing.Sequence[str]] + Token to be detokenized + + token_group_redactions : typing.Optional[typing.Sequence[V1TokenGroupRedactions]] + List of token groups to be redacted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowDetokenizeResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.detokenize() + + + asyncio.run(main()) + """ + _response = await self._raw_client.detokenize( + vault_id=vault_id, + tokens=tokens, + token_group_redactions=token_group_redactions, + request_options=request_options, + ) + return _response.data + + async def tokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + data: typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1FlowTokenizeResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID. + + data : typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] + Data to be tokenized + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowTokenizeResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.tokenize() + + + asyncio.run(main()) + """ + _response = await self._raw_client.tokenize(vault_id=vault_id, data=data, request_options=request_options) + return _response.data + + async def flowvaultmetrics( + self, *, vault_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> V1FlowVaultMetricsResponse: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault to get metrics for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1FlowVaultMetricsResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.flowservice.flowvaultmetrics() + + + asyncio.run(main()) + """ + _response = await self._raw_client.flowvaultmetrics(vault_id=vault_id, request_options=request_options) + return _response.data diff --git a/flowvault/skyflow_flowvault/generated/rest/flowservice/raw_client.py b/flowvault/skyflow_flowvault/generated/rest/flowservice/raw_client.py new file mode 100644 index 00000000..7b005ea6 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/flowservice/raw_client.py @@ -0,0 +1,1033 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..types.flow_enum_update_type import FlowEnumUpdateType +from ..types.v_1_column_redactions import V1ColumnRedactions +from ..types.v_1_delete_response import V1DeleteResponse +from ..types.v_1_flow_delete_token_response import V1FlowDeleteTokenResponse +from ..types.v_1_flow_detokenize_response import V1FlowDetokenizeResponse +from ..types.v_1_flow_tokenize_request_object import V1FlowTokenizeRequestObject +from ..types.v_1_flow_tokenize_response import V1FlowTokenizeResponse +from ..types.v_1_flow_vault_metrics_response import V1FlowVaultMetricsResponse +from ..types.v_1_get_request_data import V1GetRequestData +from ..types.v_1_get_response import V1GetResponse +from ..types.v_1_insert_record_data import V1InsertRecordData +from ..types.v_1_insert_response import V1InsertResponse +from ..types.v_1_token_group_redactions import V1TokenGroupRedactions +from ..types.v_1_unique_value import V1UniqueValue +from ..types.v_1_update_record_data import V1UpdateRecordData +from ..types.v_1_update_response import V1UpdateResponse +from ..types.v_1_upsert import V1Upsert + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawFlowserviceClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def delete( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1DeleteResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being deleted + + table_name : typing.Optional[str] + Name of the table where data is being deleted + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be deleted + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1DeleteResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/records/delete", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "skyflowIDs": skyflow_i_ds, + "uniqueValues": convert_and_respect_annotation_metadata( + object_=unique_values, annotation=typing.Sequence[V1UniqueValue], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1DeleteResponse, + parse_obj_as( + type_=V1DeleteResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def get( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + column_redactions: typing.Optional[typing.Sequence[V1ColumnRedactions]] = OMIT, + columns: typing.Optional[typing.Sequence[str]] = OMIT, + limit: typing.Optional[int] = OMIT, + offset: typing.Optional[int] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + records: typing.Optional[typing.Sequence[V1GetRequestData]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1GetResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being fetched + + table_name : typing.Optional[str] + Name of the table where data is being fetched + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be fetched + + column_redactions : typing.Optional[typing.Sequence[V1ColumnRedactions]] + List of columns to be redacted. + + columns : typing.Optional[typing.Sequence[str]] + List of columns to be fetched. + + limit : typing.Optional[int] + Limit for the number of records to be fetched + + offset : typing.Optional[int] + Offset for the number of records to be fetched + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + records : typing.Optional[typing.Sequence[V1GetRequestData]] + List of records to be fetched. This field contains tableName and skyflowIDs belonging to the table. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1GetResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/records/get", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "skyflowIDs": skyflow_i_ds, + "columnRedactions": convert_and_respect_annotation_metadata( + object_=column_redactions, annotation=typing.Sequence[V1ColumnRedactions], direction="write" + ), + "columns": columns, + "limit": limit, + "offset": offset, + "uniqueValues": convert_and_respect_annotation_metadata( + object_=unique_values, annotation=typing.Sequence[V1UniqueValue], direction="write" + ), + "records": convert_and_respect_annotation_metadata( + object_=records, annotation=typing.Sequence[V1GetRequestData], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1GetResponse, + parse_obj_as( + type_=V1GetResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def insert( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1InsertRecordData]] = OMIT, + upsert: typing.Optional[V1Upsert] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1InsertResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + table_name : typing.Optional[str] + Name of the table where data is being inserted + + records : typing.Optional[typing.Sequence[V1InsertRecordData]] + List of data row wise that is to be inserted in the vault + + upsert : typing.Optional[V1Upsert] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1InsertResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/records/insert", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "records": convert_and_respect_annotation_metadata( + object_=records, annotation=typing.Sequence[V1InsertRecordData], direction="write" + ), + "upsert": convert_and_respect_annotation_metadata( + object_=upsert, annotation=V1Upsert, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1InsertResponse, + parse_obj_as( + type_=V1InsertResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def update( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1UpdateRecordData]] = OMIT, + update_type: typing.Optional[FlowEnumUpdateType] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1UpdateResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being updated + + table_name : typing.Optional[str] + Name of the table where data is being updated + + records : typing.Optional[typing.Sequence[V1UpdateRecordData]] + List of data row wise that is to be updated in the vault + + update_type : typing.Optional[FlowEnumUpdateType] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1UpdateResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/records/update", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "records": convert_and_respect_annotation_metadata( + object_=records, annotation=typing.Sequence[V1UpdateRecordData], direction="write" + ), + "updateType": update_type, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1UpdateResponse, + parse_obj_as( + type_=V1UpdateResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def deletetoken( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1FlowDeleteTokenResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID + + tokens : typing.Optional[typing.Sequence[str]] + Token value + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1FlowDeleteTokenResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/tokens/delete", + method="POST", + json={ + "vaultID": vault_id, + "tokens": tokens, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowDeleteTokenResponse, + parse_obj_as( + type_=V1FlowDeleteTokenResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def detokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + token_group_redactions: typing.Optional[typing.Sequence[V1TokenGroupRedactions]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1FlowDetokenizeResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where detokenizing + + tokens : typing.Optional[typing.Sequence[str]] + Token to be detokenized + + token_group_redactions : typing.Optional[typing.Sequence[V1TokenGroupRedactions]] + List of token groups to be redacted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1FlowDetokenizeResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/tokens/detokenize", + method="POST", + json={ + "vaultID": vault_id, + "tokens": tokens, + "tokenGroupRedactions": convert_and_respect_annotation_metadata( + object_=token_group_redactions, + annotation=typing.Sequence[V1TokenGroupRedactions], + direction="write", + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowDetokenizeResponse, + parse_obj_as( + type_=V1FlowDetokenizeResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def tokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + data: typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1FlowTokenizeResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID. + + data : typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] + Data to be tokenized + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1FlowTokenizeResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/tokens/tokenize", + method="POST", + json={ + "vaultID": vault_id, + "data": convert_and_respect_annotation_metadata( + object_=data, annotation=typing.Sequence[V1FlowTokenizeRequestObject], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowTokenizeResponse, + parse_obj_as( + type_=V1FlowTokenizeResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def flowvaultmetrics( + self, *, vault_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[V1FlowVaultMetricsResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault to get metrics for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1FlowVaultMetricsResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/vaults/metrics", + method="POST", + json={ + "vaultID": vault_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowVaultMetricsResponse, + parse_obj_as( + type_=V1FlowVaultMetricsResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawFlowserviceClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def delete( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1DeleteResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being deleted + + table_name : typing.Optional[str] + Name of the table where data is being deleted + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be deleted + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1DeleteResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/records/delete", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "skyflowIDs": skyflow_i_ds, + "uniqueValues": convert_and_respect_annotation_metadata( + object_=unique_values, annotation=typing.Sequence[V1UniqueValue], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1DeleteResponse, + parse_obj_as( + type_=V1DeleteResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def get( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + skyflow_i_ds: typing.Optional[typing.Sequence[str]] = OMIT, + column_redactions: typing.Optional[typing.Sequence[V1ColumnRedactions]] = OMIT, + columns: typing.Optional[typing.Sequence[str]] = OMIT, + limit: typing.Optional[int] = OMIT, + offset: typing.Optional[int] = OMIT, + unique_values: typing.Optional[typing.Sequence[V1UniqueValue]] = OMIT, + records: typing.Optional[typing.Sequence[V1GetRequestData]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1GetResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being fetched + + table_name : typing.Optional[str] + Name of the table where data is being fetched + + skyflow_i_ds : typing.Optional[typing.Sequence[str]] + Skyflow ID for the record to be fetched + + column_redactions : typing.Optional[typing.Sequence[V1ColumnRedactions]] + List of columns to be redacted. + + columns : typing.Optional[typing.Sequence[str]] + List of columns to be fetched. + + limit : typing.Optional[int] + Limit for the number of records to be fetched + + offset : typing.Optional[int] + Offset for the number of records to be fetched + + unique_values : typing.Optional[typing.Sequence[V1UniqueValue]] + List of unique constraint values to query records by data + + records : typing.Optional[typing.Sequence[V1GetRequestData]] + List of records to be fetched. This field contains tableName and skyflowIDs belonging to the table. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1GetResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/records/get", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "skyflowIDs": skyflow_i_ds, + "columnRedactions": convert_and_respect_annotation_metadata( + object_=column_redactions, annotation=typing.Sequence[V1ColumnRedactions], direction="write" + ), + "columns": columns, + "limit": limit, + "offset": offset, + "uniqueValues": convert_and_respect_annotation_metadata( + object_=unique_values, annotation=typing.Sequence[V1UniqueValue], direction="write" + ), + "records": convert_and_respect_annotation_metadata( + object_=records, annotation=typing.Sequence[V1GetRequestData], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1GetResponse, + parse_obj_as( + type_=V1GetResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def insert( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1InsertRecordData]] = OMIT, + upsert: typing.Optional[V1Upsert] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1InsertResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + table_name : typing.Optional[str] + Name of the table where data is being inserted + + records : typing.Optional[typing.Sequence[V1InsertRecordData]] + List of data row wise that is to be inserted in the vault + + upsert : typing.Optional[V1Upsert] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1InsertResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/records/insert", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "records": convert_and_respect_annotation_metadata( + object_=records, annotation=typing.Sequence[V1InsertRecordData], direction="write" + ), + "upsert": convert_and_respect_annotation_metadata( + object_=upsert, annotation=V1Upsert, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1InsertResponse, + parse_obj_as( + type_=V1InsertResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def update( + self, + *, + vault_id: typing.Optional[str] = OMIT, + table_name: typing.Optional[str] = OMIT, + records: typing.Optional[typing.Sequence[V1UpdateRecordData]] = OMIT, + update_type: typing.Optional[FlowEnumUpdateType] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1UpdateResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being updated + + table_name : typing.Optional[str] + Name of the table where data is being updated + + records : typing.Optional[typing.Sequence[V1UpdateRecordData]] + List of data row wise that is to be updated in the vault + + update_type : typing.Optional[FlowEnumUpdateType] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1UpdateResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/records/update", + method="POST", + json={ + "vaultID": vault_id, + "tableName": table_name, + "records": convert_and_respect_annotation_metadata( + object_=records, annotation=typing.Sequence[V1UpdateRecordData], direction="write" + ), + "updateType": update_type, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1UpdateResponse, + parse_obj_as( + type_=V1UpdateResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def deletetoken( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1FlowDeleteTokenResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID + + tokens : typing.Optional[typing.Sequence[str]] + Token value + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1FlowDeleteTokenResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/tokens/delete", + method="POST", + json={ + "vaultID": vault_id, + "tokens": tokens, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowDeleteTokenResponse, + parse_obj_as( + type_=V1FlowDeleteTokenResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def detokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + tokens: typing.Optional[typing.Sequence[str]] = OMIT, + token_group_redactions: typing.Optional[typing.Sequence[V1TokenGroupRedactions]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1FlowDetokenizeResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where detokenizing + + tokens : typing.Optional[typing.Sequence[str]] + Token to be detokenized + + token_group_redactions : typing.Optional[typing.Sequence[V1TokenGroupRedactions]] + List of token groups to be redacted. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1FlowDetokenizeResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/tokens/detokenize", + method="POST", + json={ + "vaultID": vault_id, + "tokens": tokens, + "tokenGroupRedactions": convert_and_respect_annotation_metadata( + object_=token_group_redactions, + annotation=typing.Sequence[V1TokenGroupRedactions], + direction="write", + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowDetokenizeResponse, + parse_obj_as( + type_=V1FlowDetokenizeResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def tokenize( + self, + *, + vault_id: typing.Optional[str] = OMIT, + data: typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1FlowTokenizeResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + Vault ID. + + data : typing.Optional[typing.Sequence[V1FlowTokenizeRequestObject]] + Data to be tokenized + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1FlowTokenizeResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/tokens/tokenize", + method="POST", + json={ + "vaultID": vault_id, + "data": convert_and_respect_annotation_metadata( + object_=data, annotation=typing.Sequence[V1FlowTokenizeRequestObject], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowTokenizeResponse, + parse_obj_as( + type_=V1FlowTokenizeResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def flowvaultmetrics( + self, *, vault_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[V1FlowVaultMetricsResponse]: + """ + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault to get metrics for + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1FlowVaultMetricsResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/vaults/metrics", + method="POST", + json={ + "vaultID": vault_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1FlowVaultMetricsResponse, + parse_obj_as( + type_=V1FlowVaultMetricsResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/skyflow/py.typed b/flowvault/skyflow_flowvault/generated/rest/py.typed similarity index 100% rename from skyflow/py.typed rename to flowvault/skyflow_flowvault/generated/rest/py.typed diff --git a/skyflow/generated/rest/guardrails/__init__.py b/flowvault/skyflow_flowvault/generated/rest/records/__init__.py similarity index 100% rename from skyflow/generated/rest/guardrails/__init__.py rename to flowvault/skyflow_flowvault/generated/rest/records/__init__.py diff --git a/flowvault/skyflow_flowvault/generated/rest/records/client.py b/flowvault/skyflow_flowvault/generated/rest/records/client.py new file mode 100644 index 00000000..0503a99e --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/records/client.py @@ -0,0 +1,131 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..types.v_1_execute_query_response import V1ExecuteQueryResponse +from .raw_client import AsyncRawRecordsClient, RawRecordsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RecordsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawRecordsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawRecordsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawRecordsClient + """ + return self._raw_client + + def flow_service_execute_query( + self, + *, + vault_id: typing.Optional[str] = OMIT, + query: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1ExecuteQueryResponse: + """ + Executes a query on the specified vault. + + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + query : typing.Optional[str] + Query to execute. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1ExecuteQueryResponse + A successful response. + + Examples + -------- + from skyflow import SkyflowAuth + + client = SkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + client.records.flow_service_execute_query() + """ + _response = self._raw_client.flow_service_execute_query( + vault_id=vault_id, query=query, request_options=request_options + ) + return _response.data + + +class AsyncRecordsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawRecordsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawRecordsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawRecordsClient + """ + return self._raw_client + + async def flow_service_execute_query( + self, + *, + vault_id: typing.Optional[str] = OMIT, + query: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> V1ExecuteQueryResponse: + """ + Executes a query on the specified vault. + + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + query : typing.Optional[str] + Query to execute. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + V1ExecuteQueryResponse + A successful response. + + Examples + -------- + import asyncio + + from skyflow import AsyncSkyflowAuth + + client = AsyncSkyflowAuth( + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.records.flow_service_execute_query() + + + asyncio.run(main()) + """ + _response = await self._raw_client.flow_service_execute_query( + vault_id=vault_id, query=query, request_options=request_options + ) + return _response.data diff --git a/flowvault/skyflow_flowvault/generated/rest/records/raw_client.py b/flowvault/skyflow_flowvault/generated/rest/records/raw_client.py new file mode 100644 index 00000000..98a1365a --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/records/raw_client.py @@ -0,0 +1,132 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from ..types.v_1_execute_query_response import V1ExecuteQueryResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawRecordsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def flow_service_execute_query( + self, + *, + vault_id: typing.Optional[str] = OMIT, + query: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[V1ExecuteQueryResponse]: + """ + Executes a query on the specified vault. + + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + query : typing.Optional[str] + Query to execute. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[V1ExecuteQueryResponse] + A successful response. + """ + _response = self._client_wrapper.httpx_client.request( + "v2/query", + method="POST", + json={ + "vaultID": vault_id, + "query": query, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1ExecuteQueryResponse, + parse_obj_as( + type_=V1ExecuteQueryResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawRecordsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def flow_service_execute_query( + self, + *, + vault_id: typing.Optional[str] = OMIT, + query: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[V1ExecuteQueryResponse]: + """ + Executes a query on the specified vault. + + Parameters + ---------- + vault_id : typing.Optional[str] + ID of the vault where data is being inserted + + query : typing.Optional[str] + Query to execute. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[V1ExecuteQueryResponse] + A successful response. + """ + _response = await self._client_wrapper.httpx_client.request( + "v2/query", + method="POST", + json={ + "vaultID": vault_id, + "query": query, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + V1ExecuteQueryResponse, + parse_obj_as( + type_=V1ExecuteQueryResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/flowvault/skyflow_flowvault/generated/rest/types/__init__.py b/flowvault/skyflow_flowvault/generated/rest/types/__init__.py new file mode 100644 index 00000000..b088dc4c --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/__init__.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .flow_enum_update_type import FlowEnumUpdateType +from .flow_tokenize_response_object_token import FlowTokenizeResponseObjectToken +from .googleprotobuf_any import GoogleprotobufAny +from .protobuf_null_value import ProtobufNullValue +from .rpc_status import RpcStatus +from .v_1_column_redactions import V1ColumnRedactions +from .v_1_delete_response import V1DeleteResponse +from .v_1_delete_response_object import V1DeleteResponseObject +from .v_1_delete_token_response_object import V1DeleteTokenResponseObject +from .v_1_execute_query_record_response import V1ExecuteQueryRecordResponse +from .v_1_execute_query_response import V1ExecuteQueryResponse +from .v_1_execute_query_response_metadata import V1ExecuteQueryResponseMetadata +from .v_1_flow_delete_token_response import V1FlowDeleteTokenResponse +from .v_1_flow_detokenize_response import V1FlowDetokenizeResponse +from .v_1_flow_detokenize_response_object import V1FlowDetokenizeResponseObject +from .v_1_flow_tokenize_request_object import V1FlowTokenizeRequestObject +from .v_1_flow_tokenize_response import V1FlowTokenizeResponse +from .v_1_flow_tokenize_response_object import V1FlowTokenizeResponseObject +from .v_1_flow_vault_metrics_data import V1FlowVaultMetricsData +from .v_1_flow_vault_metrics_response import V1FlowVaultMetricsResponse +from .v_1_get_request_data import V1GetRequestData +from .v_1_get_response import V1GetResponse +from .v_1_insert_record_data import V1InsertRecordData +from .v_1_insert_response import V1InsertResponse +from .v_1_record_response_object import V1RecordResponseObject +from .v_1_token_group_redactions import V1TokenGroupRedactions +from .v_1_unique_value import V1UniqueValue +from .v_1_update_record_data import V1UpdateRecordData +from .v_1_update_response import V1UpdateResponse +from .v_1_upsert import V1Upsert + +__all__ = [ + "FlowEnumUpdateType", + "FlowTokenizeResponseObjectToken", + "GoogleprotobufAny", + "ProtobufNullValue", + "RpcStatus", + "V1ColumnRedactions", + "V1DeleteResponse", + "V1DeleteResponseObject", + "V1DeleteTokenResponseObject", + "V1ExecuteQueryRecordResponse", + "V1ExecuteQueryResponse", + "V1ExecuteQueryResponseMetadata", + "V1FlowDeleteTokenResponse", + "V1FlowDetokenizeResponse", + "V1FlowDetokenizeResponseObject", + "V1FlowTokenizeRequestObject", + "V1FlowTokenizeResponse", + "V1FlowTokenizeResponseObject", + "V1FlowVaultMetricsData", + "V1FlowVaultMetricsResponse", + "V1GetRequestData", + "V1GetResponse", + "V1InsertRecordData", + "V1InsertResponse", + "V1RecordResponseObject", + "V1TokenGroupRedactions", + "V1UniqueValue", + "V1UpdateRecordData", + "V1UpdateResponse", + "V1Upsert", +] diff --git a/flowvault/skyflow_flowvault/generated/rest/types/flow_enum_update_type.py b/flowvault/skyflow_flowvault/generated/rest/types/flow_enum_update_type.py new file mode 100644 index 00000000..01b2bab9 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/flow_enum_update_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +FlowEnumUpdateType = typing.Union[typing.Literal["UPDATE", "REPLACE"], typing.Any] diff --git a/flowvault/skyflow_flowvault/generated/rest/types/flow_tokenize_response_object_token.py b/flowvault/skyflow_flowvault/generated/rest/types/flow_tokenize_response_object_token.py new file mode 100644 index 00000000..928a6606 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/flow_tokenize_response_object_token.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class FlowTokenizeResponseObjectToken(UniversalBaseModel): + token_group_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tokenGroupName")] = ( + pydantic.Field(default=None) + ) + """ + Token group Name + """ + + token: typing.Optional[str] = pydantic.Field(default=None) + """ + Token value + """ + + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Error if tokenization failed + """ + + http_code: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="httpCode")] = pydantic.Field( + default=None + ) + """ + HTTP status code of the response + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/googleprotobuf_any.py b/flowvault/skyflow_flowvault/generated/rest/types/googleprotobuf_any.py new file mode 100644 index 00000000..aebcc5b9 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/googleprotobuf_any.py @@ -0,0 +1,139 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class GoogleprotobufAny(UniversalBaseModel): + """ + `Any` contains an arbitrary serialized protocol buffer message along with a + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + 'type.googleapis.com/full.type.name' as the type URL and the unpack + methods only use the fully qualified type name after the last '/' + in the type URL, for example "foo.bar.com/x/y.z" will yield type + name "y.z". + + JSON + ==== + The JSON representation of an `Any` value uses the regular + representation of the deserialized, embedded message, with an + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + representation, that representation will be embedded adding a field + `value` which holds the custom JSON in addition to the `@type` + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + """ + + type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="@type")] = pydantic.Field(default=None) + """ + A URL/resource name that uniquely identifies the type of the serialized + protocol buffer message. This string must contain at least + one "/" character. The last segment of the URL's path must represent + the fully qualified name of the type (as in + `path/google.protobuf.Duration`). The name should be in a canonical form + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + expect it to use in the context of Any. However, for URLs which use the + scheme `http`, `https`, or no scheme, one can optionally set up a type + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + protobuf release, and it is not used for type URLs beginning with + type.googleapis.com. As of May 2023, there are no widely used type server + implementations and no plans to implement one. + + Schemes other than `http`, `https` (or the empty scheme) might be + used with implementation specific semantics. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/protobuf_null_value.py b/flowvault/skyflow_flowvault/generated/rest/types/protobuf_null_value.py new file mode 100644 index 00000000..7a4d590f --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/protobuf_null_value.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ProtobufNullValue = typing.Literal["NULL_VALUE"] diff --git a/flowvault/skyflow_flowvault/generated/rest/types/rpc_status.py b/flowvault/skyflow_flowvault/generated/rest/types/rpc_status.py new file mode 100644 index 00000000..cf324547 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/rpc_status.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .googleprotobuf_any import GoogleprotobufAny + + +class RpcStatus(UniversalBaseModel): + code: typing.Optional[int] = None + message: typing.Optional[str] = None + details: typing.Optional[typing.List[GoogleprotobufAny]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_column_redactions.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_column_redactions.py new file mode 100644 index 00000000..65d089a5 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_column_redactions.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1ColumnRedactions(UniversalBaseModel): + column_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="columnName")] = pydantic.Field( + default=None + ) + """ + Name of the column to be redacted + """ + + redaction: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the redaction. Eg: `plain_text`, `redacted`, `mask1` + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_response.py new file mode 100644 index 00000000..9b281978 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_delete_response_object import V1DeleteResponseObject + + +class V1DeleteResponse(UniversalBaseModel): + records: typing.Optional[typing.List[V1DeleteResponseObject]] = pydantic.Field(default=None) + """ + List of deleted records with skyflow ID and any partial errors. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_response_object.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_response_object.py new file mode 100644 index 00000000..eda4c5ab --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_response_object.py @@ -0,0 +1,38 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1DeleteResponseObject(UniversalBaseModel): + skyflow_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="skyflowID")] = pydantic.Field( + default=None + ) + """ + Skyflow ID for the deleted record + """ + + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Partial Error message if any + """ + + http_code: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="httpCode")] = pydantic.Field( + default=None + ) + """ + HTTP status code of the response + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_token_response_object.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_token_response_object.py new file mode 100644 index 00000000..2a482ec0 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_delete_token_response_object.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1DeleteTokenResponseObject(UniversalBaseModel): + value: typing.Optional[str] = pydantic.Field(default=None) + """ + Token value + """ + + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Error if deletion failed + """ + + http_code: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="httpCode")] = pydantic.Field( + default=None + ) + """ + HTTP status code of the response + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_record_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_record_response.py new file mode 100644 index 00000000..30de3867 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_record_response.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class V1ExecuteQueryRecordResponse(UniversalBaseModel): + data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Fields and values for the record. For example, `{'field_1':'value_1', 'field_2':'value_2'}`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_response.py new file mode 100644 index 00000000..17caef33 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_response.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_execute_query_record_response import V1ExecuteQueryRecordResponse +from .v_1_execute_query_response_metadata import V1ExecuteQueryResponseMetadata + + +class V1ExecuteQueryResponse(UniversalBaseModel): + records: typing.Optional[typing.List[V1ExecuteQueryRecordResponse]] = pydantic.Field(default=None) + """ + Records corresponding to the specified query. + """ + + metadata: typing.Optional[V1ExecuteQueryResponseMetadata] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_response_metadata.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_response_metadata.py new file mode 100644 index 00000000..3eb0e86c --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_execute_query_response_metadata.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class V1ExecuteQueryResponseMetadata(UniversalBaseModel): + columns: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + Return columns for the query + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_delete_token_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_delete_token_response.py new file mode 100644 index 00000000..9129dbff --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_delete_token_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_delete_token_response_object import V1DeleteTokenResponseObject + + +class V1FlowDeleteTokenResponse(UniversalBaseModel): + tokens: typing.Optional[typing.List[V1DeleteTokenResponseObject]] = pydantic.Field(default=None) + """ + Tokens data for Delete + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_detokenize_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_detokenize_response.py new file mode 100644 index 00000000..47ab50dd --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_detokenize_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_flow_detokenize_response_object import V1FlowDetokenizeResponseObject + + +class V1FlowDetokenizeResponse(UniversalBaseModel): + response: typing.Optional[typing.List[V1FlowDetokenizeResponseObject]] = pydantic.Field(default=None) + """ + Detokenized data + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_detokenize_response_object.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_detokenize_response_object.py new file mode 100644 index 00000000..382a2b1a --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_detokenize_response_object.py @@ -0,0 +1,53 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1FlowDetokenizeResponseObject(UniversalBaseModel): + token: typing.Optional[str] = pydantic.Field(default=None) + """ + Token to be detokenized + """ + + value: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) + """ + Detokenized value for the token + """ + + token_group_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tokenGroupName")] = ( + pydantic.Field(default=None) + ) + """ + Token group name + """ + + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Error if detokenization failed + """ + + http_code: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="httpCode")] = pydantic.Field( + default=None + ) + """ + HTTP status code of the response + """ + + metadata: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Additional metadata associated with the token, such as tableName or skyflowID + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_request_object.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_request_object.py new file mode 100644 index 00000000..42a926ee --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_request_object.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1FlowTokenizeRequestObject(UniversalBaseModel): + value: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) + """ + Token Value + """ + + token_group_names: typing_extensions.Annotated[ + typing.Optional[typing.List[str]], FieldMetadata(alias="tokenGroupNames") + ] = pydantic.Field(default=None) + """ + List of token group names + """ + + token: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) + """ + Token for the value, in case of BYOT. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_response.py new file mode 100644 index 00000000..88616410 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_flow_tokenize_response_object import V1FlowTokenizeResponseObject + + +class V1FlowTokenizeResponse(UniversalBaseModel): + response: typing.Optional[typing.List[V1FlowTokenizeResponseObject]] = pydantic.Field(default=None) + """ + Tokenized data + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_response_object.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_response_object.py new file mode 100644 index 00000000..e77e153b --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_tokenize_response_object.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .flow_tokenize_response_object_token import FlowTokenizeResponseObjectToken + + +class V1FlowTokenizeResponseObject(UniversalBaseModel): + value: typing.Optional[typing.Optional[typing.Any]] = pydantic.Field(default=None) + """ + Value of token + """ + + tokens: typing.Optional[typing.List[FlowTokenizeResponseObjectToken]] = pydantic.Field(default=None) + """ + Token value + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_vault_metrics_data.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_vault_metrics_data.py new file mode 100644 index 00000000..f8611c37 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_vault_metrics_data.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class V1FlowVaultMetricsData(UniversalBaseModel): + tables: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Map of table names to their metrics + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_vault_metrics_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_vault_metrics_response.py new file mode 100644 index 00000000..5234fd91 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_flow_vault_metrics_response.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_flow_vault_metrics_data import V1FlowVaultMetricsData + + +class V1FlowVaultMetricsResponse(UniversalBaseModel): + data: typing.Optional[V1FlowVaultMetricsData] = None + error: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Error information, if any + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_get_request_data.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_get_request_data.py new file mode 100644 index 00000000..caf815b6 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_get_request_data.py @@ -0,0 +1,54 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata +from .v_1_column_redactions import V1ColumnRedactions +from .v_1_unique_value import V1UniqueValue + + +class V1GetRequestData(UniversalBaseModel): + table_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tableName")] = pydantic.Field( + default=None + ) + """ + Name of the table where data is being fetched + """ + + skyflow_i_ds: typing_extensions.Annotated[typing.Optional[typing.List[str]], FieldMetadata(alias="skyflowIDs")] = ( + pydantic.Field(default=None) + ) + """ + Skyflow ID for the record to be fetched + """ + + column_redactions: typing_extensions.Annotated[ + typing.Optional[typing.List[V1ColumnRedactions]], FieldMetadata(alias="columnRedactions") + ] = pydantic.Field(default=None) + """ + List of columns to be redacted. + """ + + columns: typing.Optional[typing.List[str]] = pydantic.Field(default=None) + """ + List of columns to be fetched. + """ + + unique_values: typing_extensions.Annotated[ + typing.Optional[typing.List[V1UniqueValue]], FieldMetadata(alias="uniqueValues") + ] = pydantic.Field(default=None) + """ + List of unique constraint values to query records by data + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_get_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_get_response.py new file mode 100644 index 00000000..ab966469 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_get_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_record_response_object import V1RecordResponseObject + + +class V1GetResponse(UniversalBaseModel): + records: typing.Optional[typing.List[V1RecordResponseObject]] = pydantic.Field(default=None) + """ + List of fetched records with skyflow ID, tokens, data, and any partial errors + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_insert_record_data.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_insert_record_data.py new file mode 100644 index 00000000..063626d3 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_insert_record_data.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata +from .v_1_upsert import V1Upsert + + +class V1InsertRecordData(UniversalBaseModel): + data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Columns names and values + """ + + tokens: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + undocumented_field; Tokens data for the columns if any + """ + + table_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tableName")] = pydantic.Field( + default=None + ) + """ + Table name for the record + """ + + upsert: typing.Optional[V1Upsert] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_insert_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_insert_response.py new file mode 100644 index 00000000..bac58b52 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_insert_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_record_response_object import V1RecordResponseObject + + +class V1InsertResponse(UniversalBaseModel): + records: typing.Optional[typing.List[V1RecordResponseObject]] = pydantic.Field(default=None) + """ + List of inserted records with skyflow ID, tokens, data, and any partial errors. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_record_response_object.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_record_response_object.py new file mode 100644 index 00000000..0f02a93f --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_record_response_object.py @@ -0,0 +1,62 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1RecordResponseObject(UniversalBaseModel): + skyflow_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="skyflowID")] = pydantic.Field( + default=None + ) + """ + Skyflow ID for the inserted record + """ + + tokens: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Tokens data for the columns if any + """ + + data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Columns names and values + """ + + hashed_data: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]], FieldMetadata(alias="hashedData") + ] = pydantic.Field(default=None) + """ + Hashed Data for the columns if any + """ + + error: typing.Optional[str] = pydantic.Field(default=None) + """ + Partial Error message if any + """ + + http_code: typing_extensions.Annotated[typing.Optional[int], FieldMetadata(alias="httpCode")] = pydantic.Field( + default=None + ) + """ + HTTP status code of the response + """ + + table_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tableName")] = pydantic.Field( + default=None + ) + """ + Name of the table record belongs to + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_token_group_redactions.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_token_group_redactions.py new file mode 100644 index 00000000..69263a19 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_token_group_redactions.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1TokenGroupRedactions(UniversalBaseModel): + token_group_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tokenGroupName")] = ( + pydantic.Field(default=None) + ) + """ + Name of the token group to be redacted + """ + + redaction: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the redaction. Eg: `plain_text`, `redacted`, `mask1` + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_unique_value.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_unique_value.py new file mode 100644 index 00000000..e0cfa021 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_unique_value.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class V1UniqueValue(UniversalBaseModel): + data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + Columns names and values for unique value entry + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_update_record_data.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_update_record_data.py new file mode 100644 index 00000000..19622eab --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_update_record_data.py @@ -0,0 +1,43 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1UpdateRecordData(UniversalBaseModel): + skyflow_id: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="skyflowID")] = pydantic.Field( + default=None + ) + """ + Skyflow ID for the record to be updated + """ + + data: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + List of data row wise that is to be updated in the vault + """ + + tokens: typing.Optional[typing.Dict[str, typing.Optional[typing.Any]]] = pydantic.Field(default=None) + """ + undocumented_field; Tokens data for the columns if any + """ + + table_name: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tableName")] = pydantic.Field( + default=None + ) + """ + Table name for the record + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_update_response.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_update_response.py new file mode 100644 index 00000000..4f4eb228 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_update_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .v_1_record_response_object import V1RecordResponseObject + + +class V1UpdateResponse(UniversalBaseModel): + records: typing.Optional[typing.List[V1RecordResponseObject]] = pydantic.Field(default=None) + """ + List of updated records with skyflow ID, tokens, data, and any partial errors + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/types/v_1_upsert.py b/flowvault/skyflow_flowvault/generated/rest/types/v_1_upsert.py new file mode 100644 index 00000000..f9531a37 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/types/v_1_upsert.py @@ -0,0 +1,30 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata +from .flow_enum_update_type import FlowEnumUpdateType + + +class V1Upsert(UniversalBaseModel): + update_type: typing_extensions.Annotated[typing.Optional[FlowEnumUpdateType], FieldMetadata(alias="updateType")] = ( + None + ) + unique_columns: typing_extensions.Annotated[ + typing.Optional[typing.List[str]], FieldMetadata(alias="uniqueColumns") + ] = pydantic.Field(default=None) + """ + Name of a unique columns in the table. Uses upsert operations to check if a record exists based on the unique column's value. If a matching record exists, the record updates with the values you provide. If a matching record doesn't exist, the upsert operation inserts a new record. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/flowvault/skyflow_flowvault/generated/rest/version.py b/flowvault/skyflow_flowvault/generated/rest/version.py new file mode 100644 index 00000000..82f62b47 --- /dev/null +++ b/flowvault/skyflow_flowvault/generated/rest/version.py @@ -0,0 +1,6 @@ +# NOTE: hand-patched, not Fern-generated content. Fern originally emitted a runtime +# metadata.version("skyflow.generated.rest") lookup here, but this code is bundled into the +# skyflow (v2) and v3 wheels via a build_py hook rather than published under that distribution +# name, so the lookup always raised PackageNotFoundError on import. Hardcoded until the Fern +# generator config (skyflow-fern-config) is updated to stop emitting a runtime lookup here. +__version__ = "0.0.10" diff --git a/flowvault/skyflow_flowvault/service_account/__init__.py b/flowvault/skyflow_flowvault/service_account/__init__.py new file mode 100644 index 00000000..1563c2fe --- /dev/null +++ b/flowvault/skyflow_flowvault/service_account/__init__.py @@ -0,0 +1,15 @@ +from common.service_account import ( + generate_bearer_token, + generate_bearer_token_from_creds, + is_expired, + generate_signed_data_tokens, + generate_signed_data_tokens_from_creds, +) + +__all__ = [ + "generate_bearer_token", + "generate_bearer_token_from_creds", + "is_expired", + "generate_signed_data_tokens", + "generate_signed_data_tokens_from_creds", +] diff --git a/flowvault/skyflow_flowvault/utils/__init__.py b/flowvault/skyflow_flowvault/utils/__init__.py new file mode 100644 index 00000000..3b008cc5 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/__init__.py @@ -0,0 +1,9 @@ +# Must be imported before anything that pulls in common.utils -- see its _skyflow_messages.py +# comment for why the load order matters. +from ._version import SDK_VERSION + +from common.utils import LogLevel, Env + +from .enums import UpsertType, EnvUrls +from ._skyflow_messages import SkyflowMessages +from ._utils import get_metrics, get_vault_url diff --git a/flowvault/skyflow_flowvault/utils/_skyflow_messages.py b/flowvault/skyflow_flowvault/utils/_skyflow_messages.py new file mode 100644 index 00000000..35596296 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/_skyflow_messages.py @@ -0,0 +1,57 @@ +from enum import Enum + +try: + from ._version import SDK_VERSION +except ImportError: # pragma: no cover + SDK_VERSION = "0.0.0" + +error_prefix = f"Skyflow Python SDK {SDK_VERSION}" +INFO = "INFO" +WARN = "WARN" +ERROR = "ERROR" + + +class SkyflowMessages: + """v3's own operation-specific message catalog, mirroring v2's per-variant pattern. Generic + infrastructure text lives in common.utils.SkyflowMessages instead.""" + + class Error(Enum): + EMPTY_RECORDS_IN_INSERT = f"{error_prefix} Insert failed. Specify at least one record to insert." + INVALID_RECORDS_TYPE_IN_INSERT = f"{error_prefix} Insert failed. 'records' must be a list of dicts." + INVALID_RECORD_DATA_IN_INSERT = f"{error_prefix} Insert failed. Each record's 'values' must be a non-empty dict." + INVALID_TABLE_NAME_IN_INSERT = f"{error_prefix} Insert failed. 'table' must be a non-empty string." + INVALID_UPSERT_TYPE_IN_INSERT = f"{error_prefix} Insert failed. 'upsert' must be a dict." + INVALID_UPSERT_UNIQUE_COLUMNS_IN_INSERT = f"{error_prefix} Insert failed. Upsert's 'unique_columns' must be a non-empty list of strings." + INVALID_UPSERT_UPDATE_TYPE_IN_INSERT = f"{error_prefix} Insert failed. Upsert's 'update_type' must be an UpsertType value." + TOO_MANY_RECORDS_IN_INSERT = f"{error_prefix} Insert failed. A single insert request cannot contain more than 10000 records." + TABLE_NAME_IN_BOTH_PLACES_IN_INSERT = ( + f"{error_prefix} Insert failed. 'table' cannot be set on InsertRequest at the same " + "time as any record's 'table' -- the vault accepts a table name outside the records " + "(request-level, applying to all of them) or inside each record, but not both at once." + ) + TABLE_NAME_MISSING_IN_INSERT = ( + f"{error_prefix} Insert failed. 'table' is not set on InsertRequest, so every record " + "must set its own 'table' -- either set 'table' once at the request level, or set it " + "individually on every record." + ) + RECORD_LEVEL_UPSERT_NOT_ALLOWED_IN_INSERT = ( + f"{error_prefix} Insert failed. 'table' is set on InsertRequest (request-level), so " + "'upsert' must also be provided at the request level -- a record cannot set its own " + "'upsert' while 'table' is set at the request level." + ) + REQUEST_LEVEL_UPSERT_NOT_ALLOWED_IN_INSERT = ( + f"{error_prefix} Insert failed. 'table' is set per-record, so 'upsert' must also be " + "provided per-record -- InsertRequest's request-level 'upsert' cannot be used while " + "'table' is set on individual records." + ) + EMPTY_KEY_IN_INSERT_DATA = f"{error_prefix} Insert failed. Each record's 'values' must not contain a null or empty key." + EMPTY_VALUE_IN_INSERT_DATA = f"{error_prefix} Insert failed. Each record's 'values' must not contain a null or empty value." + + class Info(Enum): + VALIDATE_INSERT_REQUEST = f"{INFO}: [{error_prefix}] Validating insert request." + INSERT_TRIGGERED = f"{INFO}: [{error_prefix}] Insert method triggered." + INSERT_REQUEST_RESOLVED = f"{INFO}: [{error_prefix}] Insert request resolved." + INSERT_SUCCESS = f"{INFO}: [{error_prefix}] Data inserted." + + class ErrorLogs(Enum): + INSERT_RECORDS_REJECTED = f"{ERROR}: [{error_prefix}] Insert call resulted in failure." diff --git a/flowvault/skyflow_flowvault/utils/_utils.py b/flowvault/skyflow_flowvault/utils/_utils.py new file mode 100644 index 00000000..6bebb796 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/_utils.py @@ -0,0 +1,54 @@ +import platform +import sys + +from common.errors import SkyflowError +from common.utils import SkyflowMessages as CommonMessages +from common.utils.constants import PROTOCOL, SdkMetricsKey, SdkPrefix +from common.utils.enums import Env +from ._version import SDK_VERSION +from .enums import EnvUrls + +_CACHED_METRICS: dict = {} + +invalid_input_error_code = CommonMessages.ErrorCodes.INVALID_INPUT.value + + +def get_vault_url(cluster_id, env, vault_id, logger=None): + """Mirrors common.utils.get_vault_url with v3's own EnvUrls (different subdomain).""" + if not cluster_id or not isinstance(cluster_id, str) or not cluster_id.strip(): + raise SkyflowError(CommonMessages.Error.INVALID_CLUSTER_ID.value.format(vault_id), invalid_input_error_code) + + if env not in Env: + raise SkyflowError(CommonMessages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code) + + base_url = EnvUrls[env.name].value + + return f"{PROTOCOL}://{cluster_id}.{base_url}" + + +def get_metrics(): + if _CACHED_METRICS: + return _CACHED_METRICS + + try: + sdk_client_device_model = platform.node() + except Exception: + sdk_client_device_model = "" + + try: + sdk_client_os_details = sys.platform + except Exception: + sdk_client_os_details = "" + + try: + sdk_runtime_details = sys.version + except Exception: + sdk_runtime_details = "" + + _CACHED_METRICS.update({ + SdkMetricsKey.SDK_NAME_VERSION: SdkPrefix.SKYFLOW_PYTHON + SDK_VERSION, + SdkMetricsKey.SDK_CLIENT_DEVICE_MODEL: sdk_client_device_model, + SdkMetricsKey.SDK_CLIENT_OS_DETAILS: sdk_client_os_details, + SdkMetricsKey.SDK_RUNTIME_DETAILS: SdkPrefix.PYTHON_RUNTIME + sdk_runtime_details, + }) + return _CACHED_METRICS diff --git a/flowvault/skyflow_flowvault/utils/_version.py b/flowvault/skyflow_flowvault/utils/_version.py new file mode 100644 index 00000000..a7755720 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/_version.py @@ -0,0 +1 @@ +SDK_VERSION = '1.0.0' diff --git a/flowvault/skyflow_flowvault/utils/enums/__init__.py b/flowvault/skyflow_flowvault/utils/enums/__init__.py new file mode 100644 index 00000000..b9232bfc --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/enums/__init__.py @@ -0,0 +1,2 @@ +from ._upsert_type import UpsertType +from ._env_urls import EnvUrls diff --git a/flowvault/skyflow_flowvault/utils/enums/_env_urls.py b/flowvault/skyflow_flowvault/utils/enums/_env_urls.py new file mode 100644 index 00000000..ccaa2919 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/enums/_env_urls.py @@ -0,0 +1,9 @@ +from enum import Enum + + +class EnvUrls(Enum): + """v3 vault hosts -- a different subdomain than v2 (skyvault vs. vault). All four confirmed.""" + DEV = "skyvault.skyflowapis.dev" + PROD = "skyvault.skyflowapis.com" + SANDBOX = "skyvault.skyflowapis-preview.com" + STAGE = "skyvault.skyflowapis.tech" diff --git a/flowvault/skyflow_flowvault/utils/enums/_upsert_type.py b/flowvault/skyflow_flowvault/utils/enums/_upsert_type.py new file mode 100644 index 00000000..b2803796 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/enums/_upsert_type.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class UpsertType(Enum): + """Mirrors the wire enum FlowEnumUpdateType (V1Upsert.update_type).""" + REPLACE = "REPLACE" + UPDATE = "UPDATE" diff --git a/flowvault/skyflow_flowvault/utils/validations/__init__.py b/flowvault/skyflow_flowvault/utils/validations/__init__.py new file mode 100644 index 00000000..499bed61 --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/validations/__init__.py @@ -0,0 +1 @@ +from ._validations import validate_vault_config, validate_update_vault_config, validate_insert_request diff --git a/flowvault/skyflow_flowvault/utils/validations/_validations.py b/flowvault/skyflow_flowvault/utils/validations/_validations.py new file mode 100644 index 00000000..a8ea74df --- /dev/null +++ b/flowvault/skyflow_flowvault/utils/validations/_validations.py @@ -0,0 +1,79 @@ +from common.errors import SkyflowError +from common.utils import SkyflowMessages as CommonMessages +from common.utils.validations import ( + validate_keys, + validate_credentials, + validate_vault_config, + validate_update_vault_config, +) +from skyflow_flowvault.utils import SkyflowMessages +from skyflow_flowvault.utils.enums import UpsertType + +VALID_INSERT_RECORD_KEYS = ["values", "table", "upsert"] +VALID_UPSERT_KEYS = ["update_type", "unique_columns"] + +invalid_input_error_code = CommonMessages.ErrorCodes.INVALID_INPUT.value + +# validate_vault_config/validate_update_vault_config/validate_credentials are re-exported +# directly from common.utils.validations -- flowvault's own logic here was field-for-field +# identical to v2's, confirmed, so both variants now share one implementation. + + +def _validate_upsert(logger, upsert): + if upsert is None: + return + if not isinstance(upsert, dict): + raise SkyflowError(SkyflowMessages.Error.INVALID_UPSERT_TYPE_IN_INSERT.value, invalid_input_error_code) + validate_keys(logger, upsert, VALID_UPSERT_KEYS) + unique_columns = upsert.get("unique_columns") + if (not isinstance(unique_columns, list) or not unique_columns + or not all(isinstance(c, str) for c in unique_columns)): + raise SkyflowError(SkyflowMessages.Error.INVALID_UPSERT_UNIQUE_COLUMNS_IN_INSERT.value, invalid_input_error_code) + update_type = upsert.get("update_type") + if update_type is not None and not isinstance(update_type, UpsertType): + raise SkyflowError(SkyflowMessages.Error.INVALID_UPSERT_UPDATE_TYPE_IN_INSERT.value, invalid_input_error_code) + + +MAX_INSERT_RECORDS = 10000 # matches Java's v3 Validations.validateInsertRequest (hardcoded, not configurable) + + +def validate_insert_request(logger, request): + if not isinstance(request.values, list) or not all(isinstance(r, dict) for r in request.values): + raise SkyflowError(SkyflowMessages.Error.INVALID_RECORDS_TYPE_IN_INSERT.value, invalid_input_error_code) + + if not request.values: + raise SkyflowError(SkyflowMessages.Error.EMPTY_RECORDS_IN_INSERT.value, invalid_input_error_code) + + if len(request.values) > MAX_INSERT_RECORDS: + raise SkyflowError(SkyflowMessages.Error.TOO_MANY_RECORDS_IN_INSERT.value, invalid_input_error_code) + + # request.table/record["table"] format and record["values"] emptiness/key/value validity are + # checked by the controller via the shared BaseVaultController._validate_table_name_if_present() + # / _validate_field_values() -- not here, to avoid duplicating that logic. + + _validate_upsert(logger, request.upsert) + + for record in request.values: + validate_keys(logger, record, VALID_INSERT_RECORD_KEYS) + _validate_upsert(logger, record.get("upsert")) + + # table must be set in exactly one place -- request-level (every record) or per-record (no + # partial mix) -- and upsert must live at that same place (mirrors Java's v3 Validations). + table_at_request_level = request.table is not None + + if table_at_request_level: + for record in request.values: + if record.get("table") is not None: + raise SkyflowError(SkyflowMessages.Error.TABLE_NAME_IN_BOTH_PLACES_IN_INSERT.value, invalid_input_error_code) + else: + for record in request.values: + if record.get("table") is None: + raise SkyflowError(SkyflowMessages.Error.TABLE_NAME_MISSING_IN_INSERT.value, invalid_input_error_code) + + if table_at_request_level: + for record in request.values: + if record.get("upsert") is not None: + raise SkyflowError(SkyflowMessages.Error.RECORD_LEVEL_UPSERT_NOT_ALLOWED_IN_INSERT.value, invalid_input_error_code) + else: + if request.upsert is not None: + raise SkyflowError(SkyflowMessages.Error.REQUEST_LEVEL_UPSERT_NOT_ALLOWED_IN_INSERT.value, invalid_input_error_code) diff --git a/tests/vault/client/__init__.py b/flowvault/skyflow_flowvault/vault/__init__.py similarity index 100% rename from tests/vault/client/__init__.py rename to flowvault/skyflow_flowvault/vault/__init__.py diff --git a/tests/vault/connection/__init__.py b/flowvault/skyflow_flowvault/vault/client/__init__.py similarity index 100% rename from tests/vault/connection/__init__.py rename to flowvault/skyflow_flowvault/vault/client/__init__.py diff --git a/flowvault/skyflow_flowvault/vault/client/client.py b/flowvault/skyflow_flowvault/vault/client/client.py new file mode 100644 index 00000000..5dc4c47d --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/client/client.py @@ -0,0 +1,14 @@ +from common.vault.base_vault_client import BaseVaultClient +from skyflow_flowvault.generated.rest.client import SkyflowAuth +from skyflow_flowvault.utils import get_vault_url + + +class VaultClient(BaseVaultClient): + def resolve_vault_url(self, cluster_id, env, vault_id, logger=None): + return get_vault_url(cluster_id, env, vault_id, logger=logger) + + def initialize_api_client(self, vault_url, bearer_token): + self._api_client = SkyflowAuth(base_url=vault_url) + + def get_insert_api(self): + return self._api_client.flowservice diff --git a/flowvault/skyflow_flowvault/vault/controller/__init__.py b/flowvault/skyflow_flowvault/vault/controller/__init__.py new file mode 100644 index 00000000..27660679 --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/controller/__init__.py @@ -0,0 +1 @@ +from ._vault import VaultController diff --git a/flowvault/skyflow_flowvault/vault/controller/_vault.py b/flowvault/skyflow_flowvault/vault/controller/_vault.py new file mode 100644 index 00000000..a6abaa49 --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/controller/_vault.py @@ -0,0 +1,158 @@ +import json + +from common.utils import SkyflowMessages as CommonMessages +from common.utils.constants import SKY_META_DATA_HEADER +from common.utils.logger import log_info, log_error_log +from common.vault.base_vault_controller import BaseVaultController +from skyflow_flowvault.generated.rest import V1InsertRecordData, V1Upsert +from skyflow_flowvault.generated.rest.core import ApiError +from skyflow_flowvault.utils import SkyflowMessages, get_metrics +from skyflow_flowvault.utils.validations import validate_insert_request +from skyflow_flowvault.vault.data import InsertRequest, InsertResponse + +REQUEST_ID_HEADER = "x-request-id" + + +class VaultController(BaseVaultController): + _skyflow_messages = SkyflowMessages + + def __init__(self, vault_client): + super().__init__(vault_client) + + def insert(self, request: InsertRequest) -> InsertResponse: + log_info(SkyflowMessages.Info.VALIDATE_INSERT_REQUEST.value, self._vault_client.get_logger()) + validate_insert_request(self._vault_client.get_logger(), request) + self._validate_table_name_if_present(request.table) + for record in request.values: + self._validate_table_name_if_present(record.get("table")) + self._validate_field_values(record.get("values")) + log_info(SkyflowMessages.Info.INSERT_REQUEST_RESOLVED.value, self._vault_client.get_logger()) + self._vault_client.initialize_client_configuration() + + insert_api = self._vault_client.get_insert_api() + + needs_per_record_table = any(r.get("table") is not None for r in request.values) + needs_per_record_upsert = any(r.get("upsert") is not None for r in request.values) + + wire_records = [ + self.__build_wire_record(record, request, needs_per_record_table, needs_per_record_upsert) + for record in request.values + ] + + try: + log_info(SkyflowMessages.Info.INSERT_TRIGGERED.value, self._vault_client.get_logger()) + headers = self.__build_headers() + top_level_kwargs = self.__omit_none( + table_name=None if needs_per_record_table else request.table, + upsert=None if needs_per_record_upsert else self.__to_v1_upsert(request.upsert), + ) + raw_response = insert_api.with_raw_response.insert( + vault_id=self._vault_client.get_vault_id(), + records=wire_records, + request_options={'additional_headers': headers}, + **top_level_kwargs, + ) + request_id = self.__extract_request_id(raw_response.headers) + inserted_fields, errors = self.__split_success_and_errors(raw_response.data.records or [], 0, request_id) + except Exception as e: + log_error_log(SkyflowMessages.ErrorLogs.INSERT_RECORDS_REJECTED.value, self._vault_client.get_logger()) + inserted_fields, errors = [], self.__errors_from_exception(e, request.values, 0) + + log_info(SkyflowMessages.Info.INSERT_SUCCESS.value, self._vault_client.get_logger()) + return InsertResponse(inserted_fields=inserted_fields, errors=errors if errors else None) + + def get(self, request): + raise NotImplementedError("VaultController.get is not implemented yet") + + def update(self, request): + raise NotImplementedError("VaultController.update is not implemented yet") + + def delete(self, request): + raise NotImplementedError("VaultController.delete is not implemented yet") + + def query(self, request): + raise NotImplementedError("VaultController.query is not implemented yet") + + def detokenize(self, request): + raise NotImplementedError("VaultController.detokenize is not implemented yet") + + def __build_wire_record(self, record, request, needs_per_record_table, needs_per_record_upsert): + return V1InsertRecordData(data=record["values"], **self.__omit_none( + table_name=(record.get("table") or request.table) if needs_per_record_table else None, + upsert=self.__to_v1_upsert(record.get("upsert") or request.upsert) if needs_per_record_upsert else None, + )) + + def __omit_none(self, **kwargs): + return {k: v for k, v in kwargs.items() if v is not None} + + def __build_headers(self): + headers = {SKY_META_DATA_HEADER: json.dumps(get_metrics())} + token = self._vault_client.get_current_bearer_token() + if token: + headers['Authorization'] = f'Bearer {token}' + return headers + + def __to_v1_upsert(self, upsert): + if upsert is None: + return None + update_type = upsert.get("update_type") + return V1Upsert( + update_type=update_type.value if update_type else None, + unique_columns=upsert.get("unique_columns"), + ) + + def __extract_request_id(self, headers): + return headers.get(REQUEST_ID_HEADER) if headers else None + + def __split_success_and_errors(self, records, start_index, request_id): + + successes, errors = [], [] + for offset, record in enumerate(records): + request_index = start_index + offset + if record.error is not None: + errors.append({'request_index': request_index, 'error': record.error, 'code': record.http_code, 'request_id': request_id}) + else: + success = { + 'request_index': request_index, + 'skyflow_id': record.skyflow_id, + } + success.update(self.__flatten_tokens(record.tokens)) + successes.append(success) + return successes, errors + + def __flatten_tokens(self, tokens): + if not tokens: + return {} + flat = {} + for column, entries in tokens.items(): + if isinstance(entries, list): + token_values = [entry.get('token') for entry in entries if isinstance(entry, dict)] + flat[column] = token_values[0] if len(token_values) == 1 else token_values + else: + flat[column] = entries + return flat + + def __errors_from_exception(self, e, records, start_index): + if isinstance(e, ApiError): + request_id = self.__extract_request_id(e.headers) + body = e.body if isinstance(e.body, dict) else None + if body and isinstance(body.get('records'), list) and body['records']: + return [ + self.__error_dict_from_record_map(record, start_index + offset, request_id) + for offset, record in enumerate(body['records']) if isinstance(record, dict) + ] + if body and body.get('error') is not None: + err_field = body['error'] + if isinstance(err_field, dict): + return [self.__error_dict_from_record_map(err_field, start_index + i, request_id) for i in range(len(records))] + return [{'request_index': start_index + i, 'error': str(err_field), 'code': e.status_code, 'request_id': request_id} + for i in range(len(records))] + return [{'request_index': start_index + i, 'error': str(e), 'code': e.status_code, 'request_id': request_id} + for i in range(len(records))] + message = str(e) if e else CommonMessages.Error.GENERIC_API_ERROR.value + return [{'request_index': start_index + i, 'error': message, 'code': None, 'request_id': None} for i in range(len(records))] + + def __error_dict_from_record_map(self, record_map, request_index, request_id): + code = record_map.get('http_code', record_map.get('httpCode', record_map.get('statusCode'))) + message = record_map.get('error', record_map.get('message', 'Unknown error')) + return {'request_index': request_index, 'error': message, 'code': code, 'request_id': request_id} diff --git a/flowvault/skyflow_flowvault/vault/data/__init__.py b/flowvault/skyflow_flowvault/vault/data/__init__.py new file mode 100644 index 00000000..62ae85cc --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/data/__init__.py @@ -0,0 +1,3 @@ +from ._insert_request import InsertRequest +from ._insert_response import InsertResponse +from ._upsert import Upsert diff --git a/flowvault/skyflow_flowvault/vault/data/_insert_request.py b/flowvault/skyflow_flowvault/vault/data/_insert_request.py new file mode 100644 index 00000000..a6da5c11 --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/data/_insert_request.py @@ -0,0 +1,7 @@ +from common.vault.data import BaseInsertRequest +from skyflow_flowvault.vault.data._upsert import Upsert + + +class InsertRequest(BaseInsertRequest): + def __init__(self, values: list, table: str = None, upsert: Upsert = None): + super().__init__(table, values, upsert=upsert) diff --git a/flowvault/skyflow_flowvault/vault/data/_insert_response.py b/flowvault/skyflow_flowvault/vault/data/_insert_response.py new file mode 100644 index 00000000..ddb87134 --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/data/_insert_response.py @@ -0,0 +1,6 @@ +from common.vault.data import BaseInsertResponse + + +class InsertResponse(BaseInsertResponse): + """flowvault's own insert() response class -- currently identical to the shared base, kept as + its own subclass so flowvault-specific fields can be added later without touching PDB.""" diff --git a/flowvault/skyflow_flowvault/vault/data/_upsert.py b/flowvault/skyflow_flowvault/vault/data/_upsert.py new file mode 100644 index 00000000..27d8fdab --- /dev/null +++ b/flowvault/skyflow_flowvault/vault/data/_upsert.py @@ -0,0 +1,6 @@ +from typing import Optional, TypedDict +from skyflow_flowvault.utils.enums import UpsertType + +class Upsert(TypedDict, total=False): + update_type: Optional[UpsertType] + unique_columns: list diff --git a/tests/vault/controller/__init__.py b/flowvault/tests/__init__.py similarity index 100% rename from tests/vault/controller/__init__.py rename to flowvault/tests/__init__.py diff --git a/tests/vault/data/__init__.py b/flowvault/tests/client/__init__.py similarity index 100% rename from tests/vault/data/__init__.py rename to flowvault/tests/client/__init__.py diff --git a/flowvault/tests/client/test_skyflow.py b/flowvault/tests/client/test_skyflow.py new file mode 100644 index 00000000..c7d908f2 --- /dev/null +++ b/flowvault/tests/client/test_skyflow.py @@ -0,0 +1,130 @@ +import unittest +from unittest.mock import patch + +from common.errors import SkyflowError +from common.utils import LogLevel, Env +from skyflow_flowvault.client import Skyflow + +VALID_VAULT_CONFIG = { + "vault_id": "VAULT_ID", + "cluster_id": "CLUSTER_ID", + "env": Env.DEV, + "credentials": {"path": "/path/to/valid_credentials.json"}, +} + +INVALID_VAULT_CONFIG = { + "cluster_id": "CLUSTER_ID", # missing vault_id + "env": Env.DEV, + "credentials": {"path": "/path/to/valid_credentials.json"}, +} + +VALID_CREDENTIALS = {"path": "/path/to/valid_credentials.json"} + + +class TestSkyflowVaultConfig(unittest.TestCase): + """v2 parity: flowvault gained remove_vault_config/update_vault_config/ + update_skyflow_credentials/update_log_level/get_log_level via the shared common base -- + these confirm they actually work here too, not just on v2.""" + + def setUp(self): + self.builder = Skyflow.builder() + + def test_add_vault_config_success(self): + builder = self.builder.add_vault_config(VALID_VAULT_CONFIG) + self.assertEqual(builder, self.builder) + + def test_add_vault_config_invalid_raises(self): + with self.assertRaises(SkyflowError): + self.builder.add_vault_config(INVALID_VAULT_CONFIG) + + def test_build_and_get_vault_config(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + config = client.get_vault_config("VAULT_ID") + self.assertEqual(config.get("vault_id"), "VAULT_ID") + + @patch("skyflow_flowvault.vault.client.client.VaultClient.update_config") + def test_update_vault_config(self, mock_update_config): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + updated = dict(VALID_VAULT_CONFIG) + updated["cluster_id"] = "NEW_CLUSTER" + client.update_vault_config(updated) + mock_update_config.assert_called_once() + + def test_update_vault_config_with_invalid_vault_id_raises(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + invalid = dict(VALID_VAULT_CONFIG) + invalid["vault_id"] = "does_not_exist" + with self.assertRaises(SkyflowError): + client.update_vault_config(invalid) + + def test_remove_vault_config(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + client.remove_vault_config("VAULT_ID") + with self.assertRaises(SkyflowError): + client.get_vault_config("VAULT_ID") + + def test_add_and_update_skyflow_credentials(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + client.add_skyflow_credentials(VALID_CREDENTIALS) + new_credentials = {"path": "/path/to/other_credentials.json"} + client.update_skyflow_credentials(new_credentials) + # no assertion error means both went through the same underlying builder path + + def test_set_and_get_log_level(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + client.set_log_level(LogLevel.INFO) + self.assertEqual(client.get_log_level(), LogLevel.INFO) + + def test_update_log_level_delegates_to_set_log_level(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + client.update_log_level(LogLevel.INFO) + self.assertEqual(client.get_log_level(), LogLevel.INFO) + + @patch("common.client.base_skyflow.log_warn") + def test_update_log_level_emits_deprecation_warning(self, mock_warn): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + client.update_log_level(LogLevel.INFO) + mock_warn.assert_called_once() + self.assertIn("set_log_level", mock_warn.call_args[0][0]) + + def test_vault_returns_vault_controller(self): + client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + vault = client.vault("VAULT_ID") + self.assertTrue(hasattr(vault, "insert")) + + +class TestConnectionAndDetectNotSupported(unittest.TestCase): + """flowvault has no Connection/Detect concept this round -- ConnectionMixin/DetectMixin are + only added to a variant's produced Skyflow class when connection_cls/detect_cls are supplied, + so these methods are genuinely absent here (AttributeError), unlike v2 where they work.""" + + def setUp(self): + self.client = Skyflow.builder().add_vault_config(VALID_VAULT_CONFIG).build() + + def test_connection_raises(self): + with self.assertRaises(AttributeError): + self.client.connection() + + def test_detect_raises(self): + with self.assertRaises(AttributeError): + self.client.detect() + + def test_add_connection_config_raises(self): + with self.assertRaises(AttributeError): + self.client.add_connection_config({}) + + def test_remove_connection_config_raises(self): + with self.assertRaises(AttributeError): + self.client.remove_connection_config("x") + + def test_update_connection_config_raises(self): + with self.assertRaises(AttributeError): + self.client.update_connection_config({}) + + def test_get_connection_config_raises(self): + with self.assertRaises(AttributeError): + self.client.get_connection_config("x") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/vault/detect/__init__.py b/flowvault/tests/utils/__init__.py similarity index 100% rename from tests/vault/detect/__init__.py rename to flowvault/tests/utils/__init__.py diff --git a/tests/vault/tokens/__init__.py b/flowvault/tests/utils/validations/__init__.py similarity index 100% rename from tests/vault/tokens/__init__.py rename to flowvault/tests/utils/validations/__init__.py diff --git a/flowvault/tests/utils/validations/test__validations.py b/flowvault/tests/utils/validations/test__validations.py new file mode 100644 index 00000000..10b36b6b --- /dev/null +++ b/flowvault/tests/utils/validations/test__validations.py @@ -0,0 +1,182 @@ +import unittest + +from common.errors import SkyflowError +from common.utils.enums import Env +from skyflow_flowvault.utils.enums import UpsertType +from skyflow_flowvault.utils.validations import validate_insert_request, validate_vault_config +from skyflow_flowvault.vault.data import InsertRequest + + +class TestValidateInsertRequest(unittest.TestCase): + def test_valid_minimal_request(self): + request = InsertRequest(values=[dict(values={"a": 1})], table="t1") + validate_insert_request(None, request) # should not raise + + def test_valid_rich_request_with_per_record_overrides(self): + """Valid per-record use: no request-level table/upsert at all (the vault rejects + setting it in both places -- see test_table_in_both_places_raises below). Java parity + requires EVERY record to set its own table when there's no request-level table (a + partial mix is invalid -- see test_table_missing_from_one_record_raises), so both + records set their own here.""" + request = InsertRequest( + values=[ + dict(values={"a": 1}, table="t2"), + dict(values={"a": 2}, table="t2", upsert={"update_type": UpsertType.REPLACE, "unique_columns": ["a"]}), + ], + ) + validate_insert_request(None, request) # should not raise + + def test_table_in_both_places_raises(self): + """Confirmed directly against a real vault: 'Table name should be present outside the + records or inside each record. Should be present at one place.'""" + request = InsertRequest( + values=[dict(values={"a": 1}, table="t2")], + table="t1", + ) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_table_in_both_places_raises_even_if_only_one_record_sets_it(self): + request = InsertRequest( + values=[dict(values={"a": 1}, table="t2"), dict(values={"a": 2})], + table="t1", + ) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_record_level_upsert_forbidden_when_table_is_at_request_level(self): + """Java parity: 'upsert' must live at the SAME place as 'table'. Here table is at the + request level, so a record-level upsert is rejected even though this record's own table + placement (none) is fine.""" + request = InsertRequest( + values=[dict(values={"a": 1}, upsert={"unique_columns": ["b"]})], + table="t1", + upsert={"unique_columns": ["a"]}, + ) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_request_level_upsert_forbidden_when_table_is_per_record(self): + request = InsertRequest( + values=[dict(values={"a": 1}, table="t1")], + upsert={"unique_columns": ["a"]}, + ) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_too_many_records_raises(self): + request = InsertRequest(values=[dict(values={"a": 1}) for _ in range(10001)], table="t1") + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_exactly_max_records_is_valid(self): + request = InsertRequest(values=[dict(values={"a": 1}) for _ in range(10000)], table="t1") + validate_insert_request(None, request) # should not raise + + def test_table_missing_from_one_record_raises(self): + """Java parity: when there's no request-level table, EVERY record must set its own -- + a partial mix (some records with a table, some without) is invalid.""" + request = InsertRequest(values=[dict(values={"a": 1}, table="t1"), dict(values={"a": 2})]) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + # Empty/null key or value in a record's 'values', and record 'values' being a non-empty + # dict, are now validated by the controller via the shared + # BaseVaultController._validate_field_values() -- see test__vault.py's + # test_insert_raises_on_empty_key/_on_empty_value/_on_non_dict_values/_on_empty_values, and + # common/tests/vault/test_base_vault_controller.py for the shared helper's own unit tests. + + def test_falsy_non_string_values_are_valid(self): + """0, False, [], {} are all legitimate values -- only None/empty-string should raise + (mirrors Java's value.toString().trim().isEmpty(), which is non-empty for all of these).""" + request = InsertRequest(values=[dict(values={"a": 0, "b": False, "c": [], "d": {}})], table="t1") + validate_insert_request(None, request) # should not raise + + def test_request_level_table_alone_is_valid(self): + request = InsertRequest(values=[dict(values={"a": 1}), dict(values={"a": 2})], table="t1") + validate_insert_request(None, request) # should not raise + + def test_per_record_table_alone_is_valid(self): + request = InsertRequest(values=[dict(values={"a": 1}, table="t1"), dict(values={"a": 2}, table="t2")]) + validate_insert_request(None, request) # should not raise + + def test_records_must_be_a_list(self): + request = InsertRequest(values="not-a-list", table="t1") + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_records_must_be_dicts(self): + request = InsertRequest(values=["not-a-dict"], table="t1") + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_record_with_unknown_key_raises(self): + request = InsertRequest(values=[{"a": 1}], table="t1") + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_records_must_not_be_empty(self): + request = InsertRequest(values=[], table="t1") + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + # Table name format (non-empty string if provided) is now validated by the controller via + # the shared BaseVaultController._validate_table_name_if_present() -- see + # test__vault.py's test_insert_raises_on_invalid_table_name and + # common/tests/vault/test_base_vault_controller.py for the shared helper's own unit tests. + + def test_table_is_optional_when_every_record_has_its_own(self): + request = InsertRequest(values=[dict(values={"a": 1}, table="t2")]) + validate_insert_request(None, request) # should not raise + + def test_upsert_must_be_a_dict(self): + request = InsertRequest(values=[dict(values={"a": 1})], table="t1", upsert="not-an-upsert") + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_upsert_unique_columns_must_be_non_empty_list_of_strings(self): + request = InsertRequest(values=[dict(values={"a": 1})], table="t1", upsert={"unique_columns": []}) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_upsert_update_type_must_be_upsert_type_enum(self): + request = InsertRequest( + values=[dict(values={"a": 1})], table="t1", + upsert={"update_type": "REPLACE", "unique_columns": ["a"]}, # plain string, not the enum + ) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + def test_per_record_upsert_is_also_validated(self): + request = InsertRequest( + values=[dict(values={"a": 1}, upsert={"unique_columns": []})], + table="t1", + ) + with self.assertRaises(SkyflowError): + validate_insert_request(None, request) + + +class TestValidateVaultConfig(unittest.TestCase): + def test_valid_config(self): + config = { + "vault_id": "vault123", + "cluster_id": "cluster1", + "env": Env.PROD, + # api_key (not a JWT-format "token") avoids the expiry check so this only + # exercises validate_vault_config's own structural validation. + "credentials": {"api_key": "sky-abcde-" + "f" * 32}, + } + self.assertTrue(validate_vault_config(None, config)) + + def test_missing_vault_id_raises(self): + with self.assertRaises(SkyflowError): + validate_vault_config(None, {"cluster_id": "cluster1"}) + + def test_unknown_key_raises(self): + config = {"vault_id": "v", "cluster_id": "c", "unexpected_key": True} + with self.assertRaises(SkyflowError): + validate_vault_config(None, config) + + +if __name__ == "__main__": + unittest.main() diff --git a/flowvault/tests/vault/__init__.py b/flowvault/tests/vault/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/flowvault/tests/vault/client/__init__.py b/flowvault/tests/vault/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/flowvault/tests/vault/client/test__client.py b/flowvault/tests/vault/client/test__client.py new file mode 100644 index 00000000..2accba2b --- /dev/null +++ b/flowvault/tests/vault/client/test__client.py @@ -0,0 +1,55 @@ +import unittest +from unittest.mock import patch, MagicMock + +from common.utils.enums import Env +from common.vault.base_vault_client import BaseVaultClient +from skyflow_flowvault.vault.client.client import VaultClient + + +class TestVaultClient(unittest.TestCase): + def setUp(self): + self.vault_client = VaultClient({"vault_id": "test_vault"}) + + def test_is_a_base_vault_client(self): + self.assertIsInstance(self.vault_client, BaseVaultClient) + + # ------------------------------------------------------------------ # + # resolve_vault_url — v3's own domain (skyvault.skyflowapis.*), confirmed + # to differ from v2's (vault.skyflowapis.*) for the same cluster_id/env + # -- reusing v2's derivation here 404'd. All four envs confirmed. + # ------------------------------------------------------------------ # + + def test_resolve_vault_url_uses_v3_skyvault_domain_dev(self): + url = self.vault_client.resolve_vault_url("qhdmceurtnlz", Env.DEV, "myvault") + self.assertEqual(url, "https://qhdmceurtnlz.skyvault.skyflowapis.dev") + + def test_resolve_vault_url_uses_v3_skyvault_domain_prod(self): + url = self.vault_client.resolve_vault_url("qhdmceurtnlz", Env.PROD, "myvault") + self.assertEqual(url, "https://qhdmceurtnlz.skyvault.skyflowapis.com") + + def test_resolve_vault_url_uses_v3_skyvault_domain_sandbox(self): + url = self.vault_client.resolve_vault_url("qhdmceurtnlz", Env.SANDBOX, "myvault") + self.assertEqual(url, "https://qhdmceurtnlz.skyvault.skyflowapis-preview.com") + + def test_resolve_vault_url_uses_v3_skyvault_domain_stage(self): + url = self.vault_client.resolve_vault_url("qhdmceurtnlz", Env.STAGE, "myvault") + self.assertEqual(url, "https://qhdmceurtnlz.skyvault.skyflowapis.tech") + + @patch("skyflow_flowvault.vault.client.client.SkyflowAuth") + def test_initialize_api_client_does_not_pass_token(self, mock_skyflow_auth): + """v3's generated client has no `token` param at all -- unlike v2, nothing should be + baked in at construction time; auth is injected per-call instead (see Vault._build_headers).""" + self.vault_client.initialize_api_client("https://test-vault-url.com", "some_bearer_token") + + _, kwargs = mock_skyflow_auth.call_args + self.assertEqual(kwargs.get("base_url"), "https://test-vault-url.com") + self.assertNotIn("token", kwargs) + + def test_get_insert_api_returns_flowservice(self): + self.vault_client._api_client = MagicMock() + result = self.vault_client.get_insert_api() + self.assertEqual(result, self.vault_client._api_client.flowservice) + + +if __name__ == "__main__": + unittest.main() diff --git a/flowvault/tests/vault/controller/__init__.py b/flowvault/tests/vault/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/flowvault/tests/vault/controller/test__vault.py b/flowvault/tests/vault/controller/test__vault.py new file mode 100644 index 00000000..449ede9d --- /dev/null +++ b/flowvault/tests/vault/controller/test__vault.py @@ -0,0 +1,375 @@ +import unittest +from unittest.mock import MagicMock, Mock, patch + +from common.errors import SkyflowError +from skyflow_flowvault.generated.rest.core import ApiError +from skyflow_flowvault.vault.controller import VaultController +from skyflow_flowvault.vault.data import InsertRequest +from skyflow_flowvault.utils.enums import UpsertType + + +class FakeRecordResponseObject: + def __init__(self, skyflow_id=None, tokens=None, data=None, error=None, http_code=None, table_name=None): + self.skyflow_id = skyflow_id + self.tokens = tokens + self.data = data + self.error = error + self.http_code = http_code + self.table_name = table_name + + +class FakeV1InsertResponse: + def __init__(self, records): + self.records = records + + +class FakeRawResponse: + """Stands in for the HttpResponse wrapper returned by with_raw_response.insert(...) -- + exposes .data (the parsed V1InsertResponse) and .headers, mirroring the real generated + client's RawFlowserviceClient.""" + + def __init__(self, records, headers=None): + self.data = FakeV1InsertResponse(records) + self.headers = headers or {} + + +class TestVault(unittest.TestCase): + def setUp(self): + self.vault_client = Mock() + self.vault_client.get_vault_id.return_value = "vault123" + self.vault_client.get_logger.return_value = Mock() + self.vault_client.get_current_bearer_token.return_value = None + self.insert_api = MagicMock() + self.vault_client.get_insert_api.return_value = self.insert_api + self.vault = VaultController(self.vault_client) + + # ------------------------------------------------------------------ # + # validation / initialization sequencing + # ------------------------------------------------------------------ # + + @patch("skyflow_flowvault.vault.controller._vault.validate_insert_request") + def test_insert_validates_before_initializing_client(self, mock_validate): + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + request = InsertRequest(values=[dict(values={"a": 1})], table="t1") + + self.vault.insert(request) + + mock_validate.assert_called_once_with(self.vault_client.get_logger(), request) + self.vault_client.initialize_client_configuration.assert_called_once() + + def test_insert_raises_for_invalid_request(self): + with self.assertRaises(SkyflowError): + self.vault.insert(InsertRequest(values=[], table="t1")) + self.vault_client.initialize_client_configuration.assert_not_called() + + # ------------------------------------------------------------------ # + # shared BaseVaultController validation helpers, exercised end-to-end via insert() + # (unit-tested in isolation in common/tests/vault/test_base_vault_controller.py) + # ------------------------------------------------------------------ # + + def test_insert_raises_on_empty_key(self): + with self.assertRaises(SkyflowError): + self.vault.insert(InsertRequest(values=[dict(values={"": "value"})], table="t1")) + self.insert_api.with_raw_response.insert.assert_not_called() + + def test_insert_allows_empty_value(self): + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + self.vault.insert(InsertRequest(values=[dict(values={"a": ""})], table="t1")) + self.insert_api.with_raw_response.insert.assert_called_once() + + def test_insert_raises_on_non_dict_values(self): + with self.assertRaises(SkyflowError): + self.vault.insert(InsertRequest(values=[dict(values=["not", "a", "dict"])], table="t1")) + + def test_insert_raises_on_empty_values_dict(self): + with self.assertRaises(SkyflowError): + self.vault.insert(InsertRequest(values=[dict(values={})], table="t1")) + + def test_insert_raises_on_invalid_request_level_table_name(self): + with self.assertRaises(SkyflowError): + self.vault.insert(InsertRequest(values=[dict(values={"a": 1})], table=" ")) + + def test_insert_raises_on_invalid_per_record_table_name(self): + with self.assertRaises(SkyflowError): + self.vault.insert(InsertRequest(values=[dict(values={"a": 1}, table=" ")])) + + # ------------------------------------------------------------------ # + # request -> wire field mapping + # ------------------------------------------------------------------ # + + def test_maps_request_level_table_and_upsert(self): + """When no record sets its own table/upsert, both go ONLY at the request level -- the + vault rejects sending table_name/upsert in both places (see the validation tests), so + the wire records must NOT also carry a resolved copy.""" + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + request = InsertRequest( + values=[dict(values={"a": 1})], + table="t1", + upsert={"update_type": UpsertType.REPLACE, "unique_columns": ["a"]}, + ) + + self.vault.insert(request) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + self.assertEqual(kwargs["vault_id"], "vault123") + self.assertEqual(kwargs["table_name"], "t1") + self.assertEqual(len(kwargs["records"]), 1) + self.assertEqual(kwargs["records"][0].data, {"a": 1}) + self.assertIsNone(kwargs["records"][0].table_name) # NOT resolved onto the record + self.assertEqual(kwargs["upsert"].update_type, "REPLACE") + self.assertEqual(kwargs["upsert"].unique_columns, ["a"]) + + def test_setting_table_at_both_request_and_record_level_raises(self): + """The vault rejects table_name in both places at once -- confirmed directly against a + real vault. validate_insert_request (tested separately) is what actually raises this; + this test just confirms insert() surfaces it rather than silently choosing one.""" + request = InsertRequest( + values=[dict(values={"a": 1}, table="t2")], + table="t1", + ) + + with self.assertRaises(SkyflowError): + self.vault.insert(request) + self.insert_api.with_raw_response.insert.assert_not_called() + + def test_per_record_table_and_upsert_used_when_request_level_unset(self): + """Legitimate per-record use: no request-level table/upsert at all -- Java parity + requires EVERY record to set its own table in this mode (see validation tests), so both + records do; only the second also sets its own upsert.""" + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + request = InsertRequest(values=[ + dict(values={"a": 1}, table="t2", upsert={"unique_columns": ["b"]}), + dict(values={"a": 2}, table="t2"), + ]) + + self.vault.insert(request) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + self.assertNotIn("table_name", kwargs) + self.assertNotIn("upsert", kwargs) + self.assertEqual(kwargs["records"][0].table_name, "t2") + self.assertEqual(kwargs["records"][0].upsert.unique_columns, ["b"]) + self.assertEqual(kwargs["records"][1].table_name, "t2") + self.assertIsNone(kwargs["records"][1].upsert) + + def test_no_request_level_table_is_omitted_not_sent_as_none(self): + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + request = InsertRequest(values=[dict(values={"a": 1}, table="t2")]) # no request-level table + + self.vault.insert(request) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + self.assertNotIn("table_name", kwargs) + + def test_wire_shape_matches_confirmed_working_request(self): + """Regression pin for a real bug: a request with only per-record table/upsert (no + request-level table/upsert at all) previously sent explicit `"tableName": null` / + `"upsert": null` at the top level, which diverged from a hand-verified working request + against a real vault (confirmed to have neither key present when unset).""" + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + request = InsertRequest(values=[ + dict( + values={"name": "saileshwar", "email": "nanana@gmail.com"}, + table="table1", + upsert={"update_type": UpsertType.UPDATE, "unique_columns": ["email"]}, + ), + ]) + + self.vault.insert(request) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + self.assertNotIn("table_name", kwargs) + self.assertNotIn("upsert", kwargs) + self.assertEqual(kwargs["records"][0].table_name, "table1") + self.assertEqual(kwargs["records"][0].upsert.update_type, "UPDATE") + self.assertEqual(kwargs["records"][0].upsert.unique_columns, ["email"]) + + def test_no_upsert_is_omitted_not_sent_as_none(self): + """upsert must be OMITTED from the wire call entirely when unset, not passed as None -- + a real vault confirmed a working request never includes a null upsert/tableName key.""" + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + request = InsertRequest(values=[dict(values={"a": 1})], table="t1") + + self.vault.insert(request) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + self.assertNotIn("upsert", kwargs) + self.assertIsNone(kwargs["records"][0].upsert) + + # ------------------------------------------------------------------ # + # response shape -- mirrors PDB's InsertResponse (inserted_fields/errors) + # ------------------------------------------------------------------ # + + def test_successful_records_go_to_inserted_fields(self): + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([ + FakeRecordResponseObject( + skyflow_id="id1", + tokens={"name": [{"token": "tok1", "tokenGroupName": "deterministic_string"}]}, + data={"name": "john doe"}, + table_name="table1", + ), + ], headers={"x-request-id": "req-1"}) + response = self.vault.insert(InsertRequest(values=[dict(values={"name": "john doe"})], table="table1")) + + self.assertEqual(len(response.inserted_fields), 1) + inserted = response.inserted_fields[0] + self.assertEqual(inserted["request_index"], 0) + self.assertEqual(inserted["skyflow_id"], "id1") + self.assertEqual(inserted["name"], "tok1") + self.assertNotIn("data", inserted) + self.assertNotIn("table", inserted) + self.assertNotIn("tokens", inserted) + self.assertIsNone(response.errors) + + def test_multiple_token_groups_for_one_field_flatten_to_a_list(self): + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([ + FakeRecordResponseObject( + skyflow_id="id1", + tokens={"email": [ + {"token": "tok-det", "tokenGroupName": "deterministic_string"}, + {"token": "tok-nondet", "tokenGroupName": "nondeterministic_string"}, + ]}, + ), + ]) + response = self.vault.insert(InsertRequest(values=[dict(values={"email": "a@b.com"})], table="t1")) + + self.assertEqual(response.inserted_fields[0]["email"], ["tok-det", "tok-nondet"]) + + def test_mixed_success_and_error_records_are_split(self): + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([ + FakeRecordResponseObject(skyflow_id="id1", tokens=None), + FakeRecordResponseObject(error="bad row", http_code=400, table_name="t1"), + ], headers={"x-request-id": "req-2"}) + response = self.vault.insert(InsertRequest( + values=[dict(values={"a": 1}), dict(values={"a": 2})], table="t1", + )) + + self.assertEqual(len(response.inserted_fields), 1) + self.assertEqual(response.inserted_fields[0]["request_index"], 0) + self.assertEqual(response.inserted_fields[0]["skyflow_id"], "id1") + self.assertEqual(len(response.errors), 1) + self.assertEqual(response.errors[0]["request_index"], 1) + self.assertEqual(response.errors[0]["error"], "bad row") + self.assertEqual(response.errors[0]["code"], 400) + self.assertEqual(response.errors[0]["request_id"], "req-2") + + def test_error_record_identified_by_error_field_alone(self): + """Mirrors Java's Utils.formatResponse exactly: a record is an error purely by .error + being present -- http_code is read onto the error dict's 'code' key but is not itself + part of the success/error decision.""" + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([ + FakeRecordResponseObject(skyflow_id="id1", http_code=200), + ]) + response = self.vault.insert(InsertRequest(values=[dict(values={"a": 1})], table="t1")) + + self.assertEqual(len(response.inserted_fields), 1) + self.assertIsNone(response.errors) + + # ------------------------------------------------------------------ # + # no batching -- every insert is exactly one API call + # ------------------------------------------------------------------ # + + def test_all_records_sent_in_a_single_api_call_regardless_of_count(self): + self.insert_api.with_raw_response.insert.side_effect = lambda **kwargs: FakeRawResponse( + [FakeRecordResponseObject(skyflow_id=f"id-{i}") for i in range(len(kwargs["records"]))] + ) + records = [dict(values={"a": i}) for i in range(4)] + + response = self.vault.insert(InsertRequest(values=records, table="t1")) + + self.insert_api.with_raw_response.insert.assert_called_once() + call_size = len(self.insert_api.with_raw_response.insert.call_args.kwargs["records"]) + self.assertEqual(call_size, 4) + self.assertEqual(len(response.inserted_fields), 4) + + def test_request_index_matches_position_in_the_original_records_list(self): + self.insert_api.with_raw_response.insert.side_effect = lambda **kwargs: FakeRawResponse( + [FakeRecordResponseObject(skyflow_id=f"id-{i}") for i in range(len(kwargs["records"]))] + ) + records = [dict(values={"a": i}) for i in range(4)] + + response = self.vault.insert(InsertRequest(values=records, table="t1")) + + self.assertEqual(sorted(s["request_index"] for s in response.inserted_fields), [0, 1, 2, 3]) + + # ------------------------------------------------------------------ # + # transport failure + # ------------------------------------------------------------------ # + + def test_transport_exception_marks_every_record_as_an_error(self): + """Without batching, one API call carries every record -- a transport-level exception + on that single call means every record in the request fails, not just some.""" + self.insert_api.with_raw_response.insert.side_effect = Exception("network blip") + records = [dict(values={"a": 1}), dict(values={"a": 2})] + + response = self.vault.insert(InsertRequest(values=records, table="t1")) + + self.insert_api.with_raw_response.insert.assert_called_once() + self.assertEqual(len(response.inserted_fields), 0) + self.assertEqual(len(response.errors), 2) + self.assertTrue(all("network blip" in e["error"] for e in response.errors)) + self.assertEqual([e["request_index"] for e in response.errors], [0, 1]) + + def test_api_error_with_structured_per_record_body_splits_into_one_error_per_row(self): + """Mirrors Java's Utils.handleBatchException: a structured error body (a 'records' list) + is split into individual error dicts instead of repeating one flat message for the + whole batch -- shaped after a real vault's actual 400 response for a partial-batch + failure (e.g. a NOT NULL column violation on one row).""" + api_error = ApiError( + status_code=400, + headers={"x-request-id": "req-3"}, + body={"records": [ + {"error": "Column passport has the notNull attribute, and input contains a null value.", + "httpCode": 400}, + ]}, + ) + self.insert_api.with_raw_response.insert.side_effect = api_error + + response = self.vault.insert(InsertRequest(values=[dict(values={"name": "a"})], table="t1")) + + self.assertEqual(len(response.errors), 1) + self.assertIn("notNull", response.errors[0]["error"]) + self.assertEqual(response.errors[0]["code"], 400) + self.assertEqual(response.errors[0]["request_id"], "req-3") + self.assertEqual(response.errors[0]["request_index"], 0) + + def test_api_error_with_flat_body_falls_back_to_one_error_per_record(self): + api_error = ApiError(status_code=500, headers={}, body={"error": "internal error"}) + self.insert_api.with_raw_response.insert.side_effect = api_error + + response = self.vault.insert(InsertRequest( + values=[dict(values={"a": 1}), dict(values={"a": 2})], table="t1", + )) + + self.assertEqual(len(response.errors), 2) + self.assertTrue(all(e["error"] == "internal error" for e in response.errors)) + self.assertTrue(all(e["code"] == 500 for e in response.errors)) + self.assertEqual([e["request_index"] for e in response.errors], [0, 1]) + + # ------------------------------------------------------------------ # + # per-call Authorization header injection + # ------------------------------------------------------------------ # + + def test_injects_authorization_header_from_current_bearer_token(self): + self.vault_client.get_current_bearer_token.return_value = "the-current-token" + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + + self.vault.insert(InsertRequest(values=[dict(values={"a": 1})], table="t1")) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + headers = kwargs["request_options"]["additional_headers"] + self.assertEqual(headers.get("Authorization"), "Bearer the-current-token") + + def test_no_authorization_header_when_no_token_available(self): + self.vault_client.get_current_bearer_token.return_value = None + self.insert_api.with_raw_response.insert.return_value = FakeRawResponse([]) + + self.vault.insert(InsertRequest(values=[dict(values={"a": 1})], table="t1")) + + _, kwargs = self.insert_api.with_raw_response.insert.call_args + headers = kwargs["request_options"]["additional_headers"] + self.assertNotIn("Authorization", headers) + + +if __name__ == "__main__": + unittest.main() diff --git a/flowvault/tests/vault/data/__init__.py b/flowvault/tests/vault/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/flowvault/tests/vault/data/test_data_classes.py b/flowvault/tests/vault/data/test_data_classes.py new file mode 100644 index 00000000..33b274ca --- /dev/null +++ b/flowvault/tests/vault/data/test_data_classes.py @@ -0,0 +1,57 @@ +import unittest + +from common.vault.data import BaseInsertRequest, BaseInsertResponse +from skyflow_flowvault.utils.enums import UpsertType +from skyflow_flowvault.vault.data import InsertRequest, InsertResponse + + +class TestInsertRequest(unittest.TestCase): + def test_is_a_base_insert_request(self): + request = InsertRequest(values=[{"values": {"a": 1}}], table="t1") + self.assertIsInstance(request, BaseInsertRequest) + self.assertEqual(request.table, "t1") + + def test_records_are_plain_dicts_supporting_per_record_overrides(self): + upsert = {"update_type": UpsertType.REPLACE, "unique_columns": ["a"]} + record = {"values": {"a": 1}, "table": "t2", "upsert": upsert} + request = InsertRequest(values=[record]) + self.assertEqual(request.values[0]["values"], {"a": 1}) + self.assertEqual(request.values[0]["table"], "t2") + self.assertIs(request.values[0]["upsert"], upsert) + + def test_table_and_upsert_are_optional_defaults(self): + request = InsertRequest(values=[{"values": {"a": 1}}]) + self.assertIsNone(request.table) + self.assertIsNone(request.upsert) + + def test_no_v2_only_fields_exist(self): + request = InsertRequest(values=[{"values": {"a": 1}}]) + for legacy_field in ("tokens", "homogeneous", "continue_on_error", "token_mode", "return_tokens"): + self.assertFalse(hasattr(request, legacy_field), f"v3 InsertRequest should not have '{legacy_field}'") + + +class TestInsertResponse(unittest.TestCase): + """Shared shape with PDB's InsertResponse -- inserted_fields/errors, each entry tagged + request_index -- plain dicts/list-of-dicts, not custom classes.""" + + def test_shape(self): + inserted_fields = [{"request_index": 0, "skyflow_id": "id1"}] + response = InsertResponse(inserted_fields=inserted_fields, errors=[]) + + self.assertIs(response.inserted_fields, inserted_fields) + self.assertEqual(response.errors, []) + + def test_is_a_base_insert_response(self): + response = InsertResponse(inserted_fields=[], errors=None) + self.assertIsInstance(response, BaseInsertResponse) + + def test_repr_does_not_raise(self): + response = InsertResponse( + inserted_fields=[], + errors=[{"request_index": 0, "error": "boom", "code": 500, "request_id": None}], + ) + self.assertIn("InsertResponse", repr(response)) + + +if __name__ == "__main__": + unittest.main() diff --git a/ruff.toml b/ruff.toml index aea6cce7..103d80d3 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ # ruff.toml exclude = [ - "skyflow/generated", + "generated", ".git", ".ruff_cache", ".venv", diff --git a/setup.py b/setup.py deleted file mode 100644 index cfed2f0c..00000000 --- a/setup.py +++ /dev/null @@ -1,48 +0,0 @@ -''' - Copyright (c) 2022 Skyflow, Inc. -''' -from setuptools import setup, find_packages -import sys - - -if sys.version_info < (3, 9): - raise RuntimeError("skyflow requires Python 3.9+") -current_version = '2.1.2' - -with open('README.md', 'r', encoding='utf-8') as f: - long_description = f.read() - -setup( - name='skyflow', - version=current_version, - author='Skyflow', - author_email='service-ops@skyflow.com', - packages=find_packages(where='.', exclude=['test*']), - # Ship PEP 561 markers so type checkers (mypy/pyright) see the SDK's types. - package_data={ - 'skyflow': ['py.typed'], - 'skyflow.generated.rest': ['py.typed'], - }, - url='https://github.com/skyflowapi/skyflow-python/', - license='LICENSE', - description='Skyflow SDK for the Python programming language', - long_description=long_description, - long_description_content_type='text/markdown', - install_requires=[ - 'pydantic >= 2.0.0', - 'typing-extensions >= 4.0.0', - 'PyJWT >= 2.12, < 3', - 'requests >= 2.28.0', - 'cryptography >= 44.0.2', - 'httpx >= 0.21.2', - 'python-dotenv >= 1.1.0, < 2', - ], - extras_require={ - 'dev': [ - 'codespell >= 2.4.1', - 'ruff >= 0.9.0', - 'pre-commit >= 4.3.0', - ] - }, - python_requires=">=3.9", -) diff --git a/skyflow/client/skyflow.py b/skyflow/client/skyflow.py deleted file mode 100644 index ebd5ef7d..00000000 --- a/skyflow/client/skyflow.py +++ /dev/null @@ -1,253 +0,0 @@ -from collections import OrderedDict -from skyflow import LogLevel -from skyflow.error import SkyflowError -from skyflow.utils import SkyflowMessages -from skyflow.utils.logger import log_info, log_warn, set_active_log_level, Logger -from skyflow.utils.constants import OptionField -from skyflow.utils.validations import validate_vault_config, validate_connection_config, validate_update_vault_config, \ - validate_update_connection_config, validate_credentials, validate_log_level -from skyflow.vault.client.client import VaultClient -from skyflow.vault.controller import Vault -from skyflow.vault.controller import Connection -from skyflow.vault.controller import Detect - -class Skyflow: - def __init__(self, builder): - self.__builder = builder - log_info(SkyflowMessages.Info.CLIENT_INITIALIZED.value, self.__builder.get_logger()) - - @staticmethod - def builder(): - return Skyflow.Builder() - - def add_vault_config(self, config): - self.__builder._Builder__add_vault_config(config) - return self - - def remove_vault_config(self, vault_id): - self.__builder.remove_vault_config(vault_id) - - def update_vault_config(self,config): - self.__builder.update_vault_config(config) - - def get_vault_config(self, vault_id): - return self.__builder.get_vault_config(vault_id).get(OptionField.VAULT_CLIENT).get_config() - - def add_connection_config(self, config): - self.__builder._Builder__add_connection_config(config) - return self - - def remove_connection_config(self, connection_id): - self.__builder.remove_connection_config(connection_id) - return self - - def update_connection_config(self, config): - self.__builder.update_connection_config(config) - return self - - def get_connection_config(self, connection_id): - return self.__builder.get_connection_config(connection_id).get(OptionField.VAULT_CLIENT).get_config() - - def add_skyflow_credentials(self, credentials): - self.__builder._Builder__add_skyflow_credentials(credentials) - return self - - def update_skyflow_credentials(self, credentials): - self.__builder._Builder__add_skyflow_credentials(credentials) - - def set_log_level(self, log_level): - self.__builder._Builder__set_log_level(log_level) - return self - - def update_log_level(self, log_level): - """.. deprecated:: Use set_log_level() instead. Will be removed in a future release.""" - log_warn(SkyflowMessages.Warning.UPDATE_LOG_LEVEL_DEPRECATED.value) - return self.set_log_level(log_level) - - def get_log_level(self): - return self.__builder._Builder__log_level - - def vault(self, vault_id = None) -> Vault: - vault_config = self.__builder.get_vault_config(vault_id) - return vault_config.get(OptionField.VAULT_CONTROLLER) - - def connection(self, connection_id = None) -> Connection: - connection_config = self.__builder.get_connection_config(connection_id) - return connection_config.get(OptionField.CONTROLLER) - - def detect(self, vault_id = None) -> Detect: - vault_config = self.__builder.get_vault_config(vault_id) - return vault_config.get(OptionField.DETECT_CONTROLLER) - - class Builder: - def __init__(self): - self.__vault_configs = OrderedDict() - self.__vault_list = list() - self.__connection_configs = OrderedDict() - self.__connection_list = list() - self.__skyflow_credentials = None - self.__log_level = LogLevel.ERROR - self.__logger = Logger(LogLevel.ERROR) - - def add_vault_config(self, config): - vault_id = config.get(OptionField.VAULT_ID) - if not isinstance(vault_id, str) or not vault_id: - raise SkyflowError( - SkyflowMessages.Error.INVALID_VAULT_ID.value, - SkyflowMessages.ErrorCodes.INVALID_INPUT.value - ) - if vault_id in [vault.get(OptionField.VAULT_ID) for vault in self.__vault_list]: - log_info(SkyflowMessages.Info.VAULT_CONFIG_EXISTS.value.format(vault_id), self.__logger) - raise SkyflowError( - SkyflowMessages.Error.VAULT_ID_ALREADY_EXISTS.value.format(vault_id), - SkyflowMessages.ErrorCodes.INVALID_INPUT.value - ) - - self.__vault_list.append(config) - return self - - def remove_vault_config(self, vault_id): - if vault_id in self.__vault_configs.keys(): - self.__vault_configs.pop(vault_id) - else: - raise SkyflowError(SkyflowMessages.Error.INVALID_VAULT_ID.value, - SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - - def update_vault_config(self, config): - validate_update_vault_config(self.__logger, config) - vault_id = config.get(OptionField.VAULT_ID) - if vault_id not in self.__vault_configs: - raise SkyflowError(SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format(vault_id), SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - vault_config = self.__vault_configs[vault_id] - vault_config.get(OptionField.VAULT_CLIENT).update_config(config) - - def get_vault_config(self, vault_id): - if vault_id is None: - if self.__vault_configs: - return next(iter(self.__vault_configs.values())) - raise SkyflowError(SkyflowMessages.Error.EMPTY_VAULT_CONFIGS.value, SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - - if vault_id in self.__vault_configs: - return self.__vault_configs.get(vault_id) - log_info(SkyflowMessages.Info.VAULT_CONFIG_DOES_NOT_EXIST.value.format(vault_id), self.__logger) - raise SkyflowError(SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format(vault_id), SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - - - def add_connection_config(self, config): - connection_id = config.get(OptionField.CONNECTION_ID) - if not isinstance(connection_id, str) or not connection_id: - raise SkyflowError( - SkyflowMessages.Error.INVALID_CONNECTION_ID.value, - SkyflowMessages.ErrorCodes.INVALID_INPUT.value - ) - if connection_id in [connection.get(OptionField.CONNECTION_ID) for connection in self.__connection_list]: - log_info(SkyflowMessages.Info.CONNECTION_CONFIG_EXISTS.value.format(connection_id), self.__logger) - raise SkyflowError( - SkyflowMessages.Error.CONNECTION_ID_ALREADY_EXISTS.value.format(connection_id), - SkyflowMessages.ErrorCodes.INVALID_INPUT.value - ) - self.__connection_list.append(config) - return self - - def remove_connection_config(self, connection_id): - if connection_id in self.__connection_configs.keys(): - self.__connection_configs.pop(connection_id) - else: - raise SkyflowError(SkyflowMessages.Error.INVALID_CONNECTION_ID.value, - SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - - def update_connection_config(self, config): - validate_update_connection_config(self.__logger, config) - connection_id = config[OptionField.CONNECTION_ID] - if connection_id not in self.__connection_configs: - raise SkyflowError(SkyflowMessages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format(connection_id), SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - connection_config = self.__connection_configs[connection_id] - connection_config.get(OptionField.VAULT_CLIENT).update_config(config) - - def get_connection_config(self, connection_id): - if connection_id is None: - if self.__connection_configs: - return next(iter(self.__connection_configs.values())) - - raise SkyflowError(SkyflowMessages.Error.EMPTY_CONNECTION_CONFIGS.value, SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - - if connection_id in self.__connection_configs: - return self.__connection_configs.get(connection_id) - log_info(SkyflowMessages.Info.CONNECTION_CONFIG_DOES_NOT_EXIST.value.format(connection_id), self.__logger) - raise SkyflowError(SkyflowMessages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format(connection_id), SkyflowMessages.ErrorCodes.INVALID_INPUT.value) - - - def add_skyflow_credentials(self, credentials): - self.__skyflow_credentials = credentials - return self - - def set_log_level(self, log_level): - self.__log_level = log_level - return self - - def get_logger(self): - return self.__logger - - def __add_vault_config(self, config): - validate_vault_config(self.__logger, config) - vault_id = config.get(OptionField.VAULT_ID) - vault_client = VaultClient(config) - self.__vault_configs[vault_id] = { - OptionField.VAULT_CLIENT: vault_client, - OptionField.VAULT_CONTROLLER: Vault(vault_client), - OptionField.DETECT_CONTROLLER: Detect(vault_client) - } - log_info(SkyflowMessages.Info.VAULT_CONTROLLER_INITIALIZED.value.format(config.get(OptionField.VAULT_ID)), self.__logger) - log_info(SkyflowMessages.Info.DETECT_CONTROLLER_INITIALIZED.value.format(config.get(OptionField.VAULT_ID)), self.__logger) - - def __add_connection_config(self, config): - validate_connection_config(self.__logger, config) - connection_id = config.get(OptionField.CONNECTION_ID) - vault_client = VaultClient(config) - self.__connection_configs[connection_id] = { - OptionField.VAULT_CLIENT: vault_client, - OptionField.CONTROLLER: Connection(vault_client) - } - log_info(SkyflowMessages.Info.CONNECTION_CONTROLLER_INITIALIZED.value.format(config.get(OptionField.CONNECTION_ID)), self.__logger) - - def __update_vault_client_logger(self, log_level, logger): - for vault_id, vault_config in self.__vault_configs.items(): - vault_config.get(OptionField.VAULT_CLIENT).set_logger(log_level,logger) - - for connection_id, connection_config in self.__connection_configs.items(): - connection_config.get(OptionField.VAULT_CLIENT).set_logger(log_level,logger) - - def __set_log_level(self, log_level): - validate_log_level(self.__logger, log_level) - self.__log_level = log_level - self.__logger.set_log_level(log_level) - set_active_log_level(log_level) - self.__update_vault_client_logger(log_level, self.__logger) - log_info(SkyflowMessages.Info.LOGGER_SETUP_DONE.value, self.__logger) - log_info(SkyflowMessages.Info.CURRENT_LOG_LEVEL.value.format(self.__log_level), self.__logger) - - def __add_skyflow_credentials(self, credentials): - if credentials is not None: - self.__skyflow_credentials = credentials - validate_credentials(self.__logger, credentials) - for vault_id, vault_config in self.__vault_configs.items(): - vault_config.get(OptionField.VAULT_CLIENT).set_common_skyflow_credentials(credentials) - - for connection_id, connection_config in self.__connection_configs.items(): - connection_config.get(OptionField.VAULT_CLIENT).set_common_skyflow_credentials(self.__skyflow_credentials) - def build(self): - validate_log_level(self.__logger, self.__log_level) - self.__logger.set_log_level(self.__log_level) - set_active_log_level(self.__log_level) - - for config in self.__vault_list: - self.__add_vault_config(config) - - for config in self.__connection_list: - self.__add_connection_config(config) - - self.__update_vault_client_logger(self.__log_level, self.__logger) - - self.__add_skyflow_credentials(self.__skyflow_credentials) - - return Skyflow(self) diff --git a/skyflow/error/__init__.py b/skyflow/error/__init__.py deleted file mode 100644 index 305c7966..00000000 --- a/skyflow/error/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._skyflow_error import SkyflowError \ No newline at end of file diff --git a/skyflow/vault/client/client.py b/skyflow/vault/client/client.py deleted file mode 100644 index 8023646c..00000000 --- a/skyflow/vault/client/client.py +++ /dev/null @@ -1,119 +0,0 @@ -from skyflow.error import SkyflowError -from skyflow.generated.rest.client import Skyflow -from skyflow.service_account import generate_bearer_token, generate_bearer_token_from_creds, is_expired -from skyflow.utils import get_vault_url, get_credentials, SkyflowMessages -from skyflow.utils.logger import log_info -from skyflow.utils.constants import OptionField, CredentialField, ConfigField - - -class VaultClient: - def __init__(self, config): - self.__config = config - self.__common_skyflow_credentials = None - self.__log_level = None - self.__client_configuration = None - self.__api_client = None - self.__logger = None - self.__is_config_updated = False - self.__bearer_token = None - self.__credentials = None - self.__vault_url = None - self.__is_static_token = None - - def set_common_skyflow_credentials(self, credentials): - self.__common_skyflow_credentials = credentials - - def set_logger(self, log_level, logger): - self.__log_level = log_level - self.__logger = logger - - def initialize_client_configuration(self): - if self.__api_client is not None and not self.__is_config_updated: - if self.__is_static_token: - return - if self.__bearer_token is not None and not is_expired(self.__bearer_token): - return - - needs_reinit = self.__api_client is None or self.__is_config_updated - if needs_reinit: - self.__credentials = get_credentials(self.__config.get(ConfigField.CREDENTIALS), self.__common_skyflow_credentials, logger=self.__logger) - self.__vault_url = get_vault_url(self.__config.get(ConfigField.CLUSTER_ID), - self.__config.get(ConfigField.ENV), - self.__config.get(ConfigField.VAULT_ID), - logger=self.__logger) - self.__is_static_token = CredentialField.TOKEN in self.__credentials or CredentialField.API_KEY in self.__credentials - bearer_token = self.get_bearer_token(self.__credentials) - if needs_reinit: - self.initialize_api_client(self.__vault_url, bearer_token) - - def initialize_api_client(self, vault_url, bearer_token): - token_provider = lambda: self.__bearer_token if self.__bearer_token is not None else bearer_token # noqa: E731 - self.__api_client = Skyflow(base_url=vault_url, token=token_provider) - - def get_records_api(self): - return self.__api_client.records - - def get_tokens_api(self): - return self.__api_client.tokens - - def get_query_api(self): - return self.__api_client.query - - def get_detect_text_api(self): - return self.__api_client.strings - - def get_detect_file_api(self): - return self.__api_client.files - - def get_vault_id(self): - return self.__config.get(ConfigField.VAULT_ID) - - def get_bearer_token(self, credentials): - if CredentialField.API_KEY in credentials: - return credentials.get(CredentialField.API_KEY) - elif CredentialField.TOKEN in credentials: - return credentials.get(CredentialField.TOKEN) - - options = { - OptionField.ROLE_IDS: self.__config.get(OptionField.ROLES), - OptionField.CTX: self.__config.get(OptionField.CTX) - } - if CredentialField.TOKEN_URI_OPTION in credentials and credentials.get(CredentialField.TOKEN_URI_OPTION): - options[CredentialField.TOKEN_URI_OPTION] = credentials.get(CredentialField.TOKEN_URI_OPTION) - - if self.__bearer_token is None or self.__is_config_updated or is_expired(self.__bearer_token): - if CredentialField.PATH in credentials: - self.__bearer_token, _ = generate_bearer_token( - credentials.get(CredentialField.PATH), - options, - self.__logger - ) - else: - credentials_string = credentials.get(CredentialField.CREDENTIALS_STRING) - log_info(SkyflowMessages.Info.GENERATE_BEARER_TOKEN_FROM_CREDENTIALS_STRING_TRIGGERED.value, self.__logger) - self.__bearer_token, _ = generate_bearer_token_from_creds( - credentials_string, - options, - self.__logger - ) - self.__is_config_updated = False - else: - log_info(SkyflowMessages.Info.REUSE_BEARER_TOKEN.value, self.__logger) - - return self.__bearer_token - - def update_config(self, config): - self.__config.update(config) - self.__is_config_updated = True - - def get_config(self): - return self.__config - - def get_common_skyflow_credentials(self): - return self.__common_skyflow_credentials - - def get_log_level(self): - return self.__log_level - - def get_logger(self): - return self.__logger \ No newline at end of file diff --git a/skyflow/vault/controller/__init__.py b/skyflow/vault/controller/__init__.py deleted file mode 100644 index 681153c0..00000000 --- a/skyflow/vault/controller/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from ._vault import Vault -from ._connections import Connection -from ._detect import Detect \ No newline at end of file diff --git a/skyflow/vault/data/_insert_request.py b/skyflow/vault/data/_insert_request.py deleted file mode 100644 index 909edd88..00000000 --- a/skyflow/vault/data/_insert_request.py +++ /dev/null @@ -1,21 +0,0 @@ -from skyflow.utils.enums import TokenMode - -class InsertRequest: - def __init__(self, - table, - values, - tokens = None, - upsert = None, - homogeneous = False, - token_mode = TokenMode.DISABLE, - return_tokens = True, - continue_on_error = False): - self.table = table - self.values = values - self.tokens = tokens - self.upsert = upsert - self.homogeneous = homogeneous - self.token_mode = token_mode - self.return_tokens = return_tokens - self.continue_on_error = continue_on_error - diff --git a/skyflow/vault/data/_insert_response.py b/skyflow/vault/data/_insert_response.py deleted file mode 100644 index 0c7c777f..00000000 --- a/skyflow/vault/data/_insert_response.py +++ /dev/null @@ -1,10 +0,0 @@ -class InsertResponse: - def __init__(self, inserted_fields = None, errors=None): - self.inserted_fields = inserted_fields - self.errors = errors - - def __repr__(self): - return f"InsertResponse(inserted_fields={self.inserted_fields}, errors={self.errors})" - - def __str__(self): - return self.__repr__() diff --git a/tests/contract/__init__.py b/tests/contract/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/contract/_adapter_loader.py b/tests/contract/_adapter_loader.py new file mode 100644 index 00000000..2534a174 --- /dev/null +++ b/tests/contract/_adapter_loader.py @@ -0,0 +1,26 @@ +"""Selects the v2 or v3 (flowvault) contract adapter based on SKYFLOW_TEST_VARIANT. +Plain-Python equivalent of a pytest conftest.py fixture -- this repo's test runner is plain +unittest (see each variant's tests/), so variant selection happens via import rather than a +fixture. + +Usage (run once per variant, in that variant's own installed/PYTHONPATH environment -- v2's +skyflow and flowvault's skyflow_flowvault can never coexist in one process): + + SKYFLOW_TEST_VARIANT=v2 PYTHONPATH=.:v2 python -m unittest discover -s tests/contract -t . + SKYFLOW_TEST_VARIANT=v3 PYTHONPATH=.:flowvault python -m unittest discover -s tests/contract -t . +""" +import os + +_VARIANT = os.environ.get("SKYFLOW_TEST_VARIANT") + +if _VARIANT == "v2": + from tests.contract.adapters import v2_adapter as adapter +elif _VARIANT == "v3": + from tests.contract.adapters import v3_adapter as adapter +else: + raise RuntimeError( + "SKYFLOW_TEST_VARIANT must be set to 'v2' or 'v3' before running tests/contract/ " + "(e.g. SKYFLOW_TEST_VARIANT=v2 PYTHONPATH=.:v2 python -m unittest discover -s tests/contract -t .)" + ) + +VARIANT = _VARIANT diff --git a/tests/contract/adapters/__init__.py b/tests/contract/adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/contract/adapters/v2_adapter.py b/tests/contract/adapters/v2_adapter.py new file mode 100644 index 00000000..ff581328 --- /dev/null +++ b/tests/contract/adapters/v2_adapter.py @@ -0,0 +1,52 @@ +"""Contract adapter for v2. Constructs a Vault with its underlying generated API call mocked +out, so the contract suite can assert on SDK-level behavior (call counts, response shape) +without needing real network access or credentials. Import this module only from a v2-installed +environment (`SKYFLOW_TEST_VARIANT=v2`) -- v2.skyflow and v3.skyflow cannot coexist in one +process.""" +from types import SimpleNamespace +from unittest.mock import MagicMock + +from skyflow.vault.client.client import VaultClient +from skyflow.vault.controller import Vault +from skyflow.vault.data import InsertRequest + +# Uses the public `Vault` alias deliberately (not VaultController) -- this adapter simulates +# an external consumer, and Vault is the name they'd actually import. + + +def build_vault(): + config = { + "vault_id": "contract_vault", + "cluster_id": "contract_cluster", + "env": "PROD", + "credentials": {"token": "contract_static_token"}, + } + vault_client = VaultClient(config) + vault_client.initialize_client_configuration = MagicMock() # skip real credential/URL resolution + records_api = MagicMock() + vault_client.get_records_api = MagicMock(return_value=records_api) + vault = Vault(vault_client) + return vault, records_api + + +def build_insert_request(n): + return InsertRequest(table="contract_table", values=[{"field": f"value{i}"} for i in range(n)]) + + +def call_insert(vault, records_api, request): + fake_records = [SimpleNamespace(skyflow_id=f"id{i}", tokens=None) for i in range(len(request.values))] + fake_response = SimpleNamespace(data=SimpleNamespace(records=fake_records), headers={}) + records_api.with_raw_response.record_service_insert_record.return_value = fake_response + + response = vault.insert(request) + call_count = records_api.with_raw_response.record_service_insert_record.call_count + return response, call_count + + +# v2's InsertResponse is the shared shape (inserted_fields/errors) both variants now use. +def count_successes(response): + return len(response.inserted_fields) + + +def count_errors(response): + return len(response.errors) if response.errors else 0 diff --git a/tests/contract/adapters/v3_adapter.py b/tests/contract/adapters/v3_adapter.py new file mode 100644 index 00000000..d5a7ceb3 --- /dev/null +++ b/tests/contract/adapters/v3_adapter.py @@ -0,0 +1,52 @@ +"""Contract adapter for v3 (flowvault). See v2_adapter.py for the shared design note -- import +this module only from a flowvault-installed environment (`SKYFLOW_TEST_VARIANT=v3`).""" +from types import SimpleNamespace +from unittest.mock import MagicMock + +from skyflow_flowvault.vault.client.client import VaultClient +from skyflow_flowvault.vault.controller import VaultController +from skyflow_flowvault.vault.data import InsertRequest + + +def build_vault(): + config = { + "vault_id": "contract_vault", + "cluster_id": "contract_cluster", + "env": "PROD", + "credentials": {"token": "contract_static_token"}, + } + vault_client = VaultClient(config) + vault_client.initialize_client_configuration = MagicMock() # skip real credential/URL resolution + insert_api = MagicMock() + vault_client.get_insert_api = MagicMock(return_value=insert_api) + vault = VaultController(vault_client) + return vault, insert_api + + +def build_insert_request(n): + return InsertRequest(table="contract_table", values=[dict(values={"field": f"value{i}"}) for i in range(n)]) + + +def call_insert(vault, insert_api, request): + def fake_insert(**kwargs): + records = [ + SimpleNamespace(skyflow_id=f"id{i}", tokens=None, data=None, error=None, http_code=None, table_name=None) + for i in range(len(kwargs["records"])) + ] + return SimpleNamespace(data=SimpleNamespace(records=records), headers={}) + + insert_api.with_raw_response.insert.side_effect = fake_insert + response = vault.insert(request) + call_count = insert_api.with_raw_response.insert.call_count + return response, call_count + + +# v3's InsertResponse now shares the exact same shape as v2's (inserted_fields/errors, each +# entry tagged request_index) -- kept as separate accessor functions per adapter anyway, since +# the contract module intentionally treats each variant's response as opaque. +def count_successes(response): + return len(response.inserted_fields) + + +def count_errors(response): + return len(response.errors) if response.errors else 0 diff --git a/tests/contract/test_insert_contract.py b/tests/contract/test_insert_contract.py new file mode 100644 index 00000000..69406407 --- /dev/null +++ b/tests/contract/test_insert_contract.py @@ -0,0 +1,48 @@ +"""Shared insert() contract, asserted identically against both variants -- authored once, run +twice (see _adapter_loader.py). If this file needs variant-specific branching, that's a signal +the abstraction leaked and the adapter interface needs to grow, not this file. +""" +import unittest + +from tests.contract._adapter_loader import VARIANT, adapter + + +class TestInsertContract(unittest.TestCase): + def test_vault_exposes_insert_with_a_single_request_argument(self): + vault, _ = adapter.build_vault() + self.assertTrue(hasattr(vault, "insert"), f"{VARIANT}'s Vault must expose insert()") + self.assertTrue(callable(vault.insert)) + + def test_insert_response_reports_correct_counts(self): + """v2 and v3 now share the exact same InsertResponse shape (inserted_fields/errors, + each entry tagged request_index); the contract accesses it via the adapter's + count_successes/count_errors functions rather than assuming the shape directly.""" + vault, api = adapter.build_vault() + request = adapter.build_insert_request(1) + + response, _ = adapter.call_insert(vault, api, request) + + self.assertEqual(adapter.count_successes(response), 1) + self.assertEqual(adapter.count_errors(response), 0) + + def test_insert_of_many_records_returns_one_field_per_record(self): + vault, api = adapter.build_vault() + request = adapter.build_insert_request(7) + + response, _ = adapter.call_insert(vault, api, request) + + self.assertEqual(adapter.count_successes(response), 7) + + def test_insert_always_makes_exactly_one_api_call_regardless_of_record_count(self): + """Neither variant batches -- every insert(), no matter how many records, must reach + the underlying API exactly once.""" + vault, api = adapter.build_vault() + request = adapter.build_insert_request(3) + + _, call_count = adapter.call_insert(vault, api, request) + + self.assertEqual(call_count, 1, f"{VARIANT} must always call the underlying API exactly once") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/contract/test_typecheck_contract.py b/tests/contract/test_typecheck_contract.py new file mode 100644 index 00000000..df2cbf92 --- /dev/null +++ b/tests/contract/test_typecheck_contract.py @@ -0,0 +1,54 @@ +"""Only meaningful under SKYFLOW_TEST_VARIANT=v3 -- shells out to mypy (or pyright, if mypy +isn't installed) against the fixture and asserts it fails with diagnostics naming each v2-only +field. Skipped entirely under v2 (those kwargs are valid there). + +NOT independently verified in this environment: neither mypy nor pyright is installed in the +sandbox this was authored in, so this file has been reviewed for correctness but its actual +pass/fail behavior against a real type checker has not been observed. Install one of them (`pip +install mypy` or `pip install pyright`) before relying on this as a passing gate. +""" +import shutil +import subprocess +import sys +import unittest +from pathlib import Path + +from tests.contract._adapter_loader import VARIANT + +FIXTURE = Path(__file__).parent / "typecheck_fixtures" / "v2_only_insert_kwargs_should_fail_under_v3.py" +V2_ONLY_FIELDS = ["homogeneous", "continue_on_error", "token_mode", "return_tokens"] + + +@unittest.skipUnless(VARIANT == "v3", "v2-only insert kwargs are valid under v2; nothing to type-check there") +class TestV3RejectsV2OnlyInsertFields(unittest.TestCase): + def test_v2_only_kwargs_are_flagged_as_type_errors_under_v3(self): + if shutil.which("mypy") or _module_available("mypy"): + result = subprocess.run( + [sys.executable, "-m", "mypy", "--no-error-summary", str(FIXTURE)], + capture_output=True, text=True, + ) + elif shutil.which("pyright") or _module_available("pyright"): + result = subprocess.run( + [sys.executable, "-m", "pyright", str(FIXTURE)], + capture_output=True, text=True, + ) + else: + self.skipTest("neither mypy nor pyright is installed") + return + + diagnostics = result.stdout + result.stderr + self.assertNotEqual(result.returncode, 0, f"expected type errors, got a clean run:\n{diagnostics}") + for field in V2_ONLY_FIELDS: + self.assertIn(field, diagnostics, f"expected a diagnostic mentioning '{field}':\n{diagnostics}") + + +def _module_available(name): + try: + __import__(name) + return True + except ImportError: + return False + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/contract/typecheck_fixtures/v2_only_insert_kwargs_should_fail_under_v3.py b/tests/contract/typecheck_fixtures/v2_only_insert_kwargs_should_fail_under_v3.py new file mode 100644 index 00000000..84c91c69 --- /dev/null +++ b/tests/contract/typecheck_fixtures/v2_only_insert_kwargs_should_fail_under_v3.py @@ -0,0 +1,19 @@ +"""Type-check fixture, not a test file to be executed directly (see test_typecheck_contract.py). + +Every construction below uses a v2-only InsertRequest keyword argument that does not exist on +flowvault's InsertRequest (records/table/upsert only -- see +flowvault/skyflow_flowvault/vault/data/_insert_request.py). Run under mypy/pyright against a +flowvault install, each of these lines must be flagged as a type error. If a future change to +flowvault's InsertRequest ever silently grows one of these fields back, this fixture stops +producing errors and test_typecheck_contract.py's assertion on it fails -- that's the point: +it's a regression trip-wire, not a demonstration. + +No `# type: ignore` anywhere in this file -- the whole point is for the checker to actually emit +diagnostics. +""" +from skyflow_flowvault.vault.data import InsertRequest + +InsertRequest(values=[], table="t1", homogeneous=True) +InsertRequest(values=[], table="t1", continue_on_error=True) +InsertRequest(values=[], table="t1", token_mode="ENABLE") +InsertRequest(values=[], table="t1", return_tokens=False) diff --git a/tests/vault/client/test__client.py b/tests/vault/client/test__client.py deleted file mode 100644 index 4df508c7..00000000 --- a/tests/vault/client/test__client.py +++ /dev/null @@ -1,327 +0,0 @@ -import unittest -from unittest.mock import patch, MagicMock - -from skyflow.error import SkyflowError -from skyflow.utils import SkyflowMessages -from skyflow.vault.client.client import VaultClient - -CONFIG = { - "credentials": "some_credentials", - "cluster_id": "test_cluster_id", - "env": "test_env", - "vault_id": "test_vault_id", - "roles": ["role_id_1", "role_id_2"], - "ctx": "context" -} - -CREDENTIALS_WITH_API_KEY = {"api_key": "dummy_api_key"} -CREDENTIALS_WITH_TOKEN = {"token": "dummy_static_token"} -CREDENTIALS_WITH_PATH = {"path": "/some/path/credentials.json"} -CREDENTIALS_WITH_STRING = {"credentials_string": '{"clientID": "x"}'} - - -class TestVaultClient(unittest.TestCase): - def setUp(self): - self.vault_client = VaultClient(CONFIG) - - # ------------------------------------------------------------------ # - # Basic setters / getters # - # ------------------------------------------------------------------ # - - def test_set_common_skyflow_credentials(self): - credentials = {"api_key": "dummy_api_key"} - self.vault_client.set_common_skyflow_credentials(credentials) - self.assertEqual(self.vault_client.get_common_skyflow_credentials(), credentials) - - def test_set_logger(self): - mock_logger = MagicMock() - self.vault_client.set_logger("INFO", mock_logger) - self.assertEqual(self.vault_client.get_log_level(), "INFO") - self.assertEqual(self.vault_client.get_logger(), mock_logger) - - def test_get_vault_id(self): - self.assertEqual(self.vault_client.get_vault_id(), CONFIG["vault_id"]) - - def test_get_config(self): - self.assertEqual(self.vault_client.get_config(), CONFIG) - - def test_get_common_skyflow_credentials(self): - credentials = {"api_key": "dummy_api_key"} - self.vault_client.set_common_skyflow_credentials(credentials) - self.assertEqual(self.vault_client.get_common_skyflow_credentials(), credentials) - - def test_get_log_level(self): - self.vault_client.set_logger("DEBUG", MagicMock()) - self.assertEqual(self.vault_client.get_log_level(), "DEBUG") - - def test_get_logger(self): - mock_logger = MagicMock() - self.vault_client.set_logger("INFO", mock_logger) - self.assertEqual(self.vault_client.get_logger(), mock_logger) - - # ------------------------------------------------------------------ # - # initialize_client_configuration — first call (slow path) # - # ------------------------------------------------------------------ # - - @patch("skyflow.vault.client.client.get_credentials") - @patch("skyflow.vault.client.client.get_vault_url") - @patch("skyflow.vault.client.client.VaultClient.initialize_api_client") - def test_initialize_client_configuration_first_call( - self, mock_init_api_client, mock_get_vault_url, mock_get_credentials - ): - mock_get_credentials.return_value = CREDENTIALS_WITH_API_KEY - mock_get_vault_url.return_value = "https://test-vault-url.com" - - self.vault_client.initialize_client_configuration() - - mock_get_credentials.assert_called_once_with( - CONFIG["credentials"], None, logger=None - ) - mock_get_vault_url.assert_called_once_with( - CONFIG["cluster_id"], CONFIG["env"], CONFIG["vault_id"], logger=None - ) - mock_init_api_client.assert_called_once() - - # ------------------------------------------------------------------ # - # initialize_client_configuration — fast path (static token) # - # ------------------------------------------------------------------ # - - @patch("skyflow.vault.client.client.get_credentials") - @patch("skyflow.vault.client.client.get_vault_url") - @patch("skyflow.vault.client.client.VaultClient.initialize_api_client") - def test_initialize_client_configuration_fast_path_api_key( - self, mock_init_api_client, mock_get_vault_url, mock_get_credentials - ): - """Once initialized with api_key, subsequent calls skip all work.""" - mock_get_credentials.return_value = CREDENTIALS_WITH_API_KEY - mock_get_vault_url.return_value = "https://test-vault-url.com" - # Side-effect simulates initialize_api_client actually setting __api_client - mock_init_api_client.side_effect = lambda *_: setattr( - self.vault_client, "_VaultClient__api_client", MagicMock() - ) - - self.vault_client.initialize_client_configuration() # first call — slow path - mock_get_credentials.reset_mock() - mock_get_vault_url.reset_mock() - mock_init_api_client.reset_mock() - - self.vault_client.initialize_client_configuration() # second call — fast path - - mock_get_credentials.assert_not_called() - mock_get_vault_url.assert_not_called() - mock_init_api_client.assert_not_called() - - @patch("skyflow.vault.client.client.get_credentials") - @patch("skyflow.vault.client.client.get_vault_url") - @patch("skyflow.vault.client.client.VaultClient.initialize_api_client") - def test_initialize_client_configuration_fast_path_static_token( - self, mock_init_api_client, mock_get_vault_url, mock_get_credentials - ): - """Once initialized with a static token, subsequent calls skip all work.""" - mock_get_credentials.return_value = CREDENTIALS_WITH_TOKEN - mock_get_vault_url.return_value = "https://test-vault-url.com" - mock_init_api_client.side_effect = lambda *_: setattr( - self.vault_client, "_VaultClient__api_client", MagicMock() - ) - - self.vault_client.initialize_client_configuration() - mock_get_credentials.reset_mock() - mock_get_vault_url.reset_mock() - mock_init_api_client.reset_mock() - - self.vault_client.initialize_client_configuration() - - mock_get_credentials.assert_not_called() - mock_get_vault_url.assert_not_called() - mock_init_api_client.assert_not_called() - - # ------------------------------------------------------------------ # - # initialize_client_configuration — fast path (service account) # - # ------------------------------------------------------------------ # - - @patch("skyflow.vault.client.client.is_expired", return_value=False) - @patch("skyflow.vault.client.client.get_credentials") - @patch("skyflow.vault.client.client.get_vault_url") - @patch("skyflow.vault.client.client.VaultClient.initialize_api_client") - def test_initialize_client_configuration_fast_path_valid_sa_token( - self, mock_init_api_client, mock_get_vault_url, mock_get_credentials, mock_is_expired - ): - """Service account with a still-valid token skips get_bearer_token entirely.""" - mock_get_credentials.return_value = CREDENTIALS_WITH_PATH - mock_get_vault_url.return_value = "https://test-vault-url.com" - - # Seed the cached bearer token as if first call already ran - self.vault_client._VaultClient__api_client = MagicMock() - self.vault_client._VaultClient__is_static_token = False - self.vault_client._VaultClient__bearer_token = "cached_sa_token" - self.vault_client._VaultClient__credentials = CREDENTIALS_WITH_PATH - - self.vault_client.initialize_client_configuration() - - mock_get_credentials.assert_not_called() - mock_get_vault_url.assert_not_called() - mock_init_api_client.assert_not_called() - - # ------------------------------------------------------------------ # - # initialize_client_configuration — token expiry (no client reinit) # - # ------------------------------------------------------------------ # - - @patch("skyflow.vault.client.client.generate_bearer_token", return_value=("new_sa_token", None)) - @patch("skyflow.vault.client.client.is_expired", return_value=True) - @patch("skyflow.vault.client.client.get_credentials") - @patch("skyflow.vault.client.client.get_vault_url") - @patch("skyflow.vault.client.client.VaultClient.initialize_api_client") - def test_initialize_client_configuration_expired_token_no_reinit( - self, mock_init_api_client, mock_get_vault_url, mock_get_credentials, - mock_is_expired, mock_generate_bearer_token - ): - """Expired service account token is regenerated in-place; httpx client is NOT recreated.""" - mock_get_credentials.return_value = CREDENTIALS_WITH_PATH - mock_get_vault_url.return_value = "https://test-vault-url.com" - - # Client already initialized — simulate warm state with an expired token - self.vault_client._VaultClient__api_client = MagicMock() - self.vault_client._VaultClient__is_static_token = False - self.vault_client._VaultClient__bearer_token = "expired_sa_token" - self.vault_client._VaultClient__credentials = CREDENTIALS_WITH_PATH - - self.vault_client.initialize_client_configuration() - - # Token was regenerated - mock_generate_bearer_token.assert_called_once() - self.assertEqual( - self.vault_client._VaultClient__bearer_token, "new_sa_token" - ) - # httpx client was NOT recreated - mock_init_api_client.assert_not_called() - - # ------------------------------------------------------------------ # - # initialize_client_configuration — config update forces reinit # - # ------------------------------------------------------------------ # - - @patch("skyflow.vault.client.client.get_credentials") - @patch("skyflow.vault.client.client.get_vault_url") - @patch("skyflow.vault.client.client.VaultClient.initialize_api_client") - def test_initialize_client_configuration_reinit_after_update_config( - self, mock_init_api_client, mock_get_vault_url, mock_get_credentials - ): - """update_config() marks the client stale; next call must recreate it.""" - mock_get_credentials.return_value = CREDENTIALS_WITH_API_KEY - mock_get_vault_url.return_value = "https://test-vault-url.com" - - # Simulate already-initialized client - self.vault_client._VaultClient__api_client = MagicMock() - self.vault_client._VaultClient__is_static_token = True - - self.vault_client.update_config({"cluster_id": "new_cluster"}) - self.vault_client.initialize_client_configuration() - - mock_get_credentials.assert_called_once() - mock_get_vault_url.assert_called_once() - mock_init_api_client.assert_called_once() - - # ------------------------------------------------------------------ # - # initialize_api_client — lambda token provider # - # ------------------------------------------------------------------ # - - @patch("skyflow.vault.client.client.Skyflow") - def test_initialize_api_client_passes_callable_token(self, mock_skyflow): - """initialize_api_client must pass a callable (lambda) as token, not a string.""" - self.vault_client.initialize_api_client("https://test-vault-url.com", "initial_token") - - args, kwargs = mock_skyflow.call_args - self.assertEqual(kwargs["base_url"], "https://test-vault-url.com") - self.assertTrue(callable(kwargs["token"]), "token must be a callable (lambda)") - - @patch("skyflow.vault.client.client.Skyflow") - def test_initialize_api_client_lambda_returns_cached_bearer_token(self, mock_skyflow): - """Lambda returns __bearer_token when it is set (interceptor behaviour).""" - self.vault_client._VaultClient__bearer_token = "refreshed_token" - self.vault_client.initialize_api_client("https://test-vault-url.com", "initial_token") - - _, kwargs = mock_skyflow.call_args - self.assertEqual(kwargs["token"](), "refreshed_token") - - @patch("skyflow.vault.client.client.Skyflow") - def test_initialize_api_client_lambda_falls_back_to_initial_token(self, mock_skyflow): - """Lambda falls back to the initial token when __bearer_token is None.""" - self.vault_client._VaultClient__bearer_token = None - self.vault_client.initialize_api_client("https://test-vault-url.com", "initial_token") - - _, kwargs = mock_skyflow.call_args - self.assertEqual(kwargs["token"](), "initial_token") - - # ------------------------------------------------------------------ # - # get_bearer_token # - # ------------------------------------------------------------------ # - - def test_get_bearer_token_with_api_key(self): - result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_API_KEY) - self.assertEqual(result, "dummy_api_key") - - def test_get_bearer_token_with_static_token(self): - result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_TOKEN) - self.assertEqual(result, "dummy_static_token") - - @patch("skyflow.vault.client.client.generate_bearer_token", return_value=("sa_token", None)) - def test_get_bearer_token_generates_from_path_on_first_call(self, mock_generate): - result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_PATH) - mock_generate.assert_called_once() - self.assertEqual(result, "sa_token") - self.assertEqual(self.vault_client._VaultClient__bearer_token, "sa_token") - - @patch("skyflow.vault.client.client.generate_bearer_token_from_creds", return_value=("sa_token_str", None)) - @patch("skyflow.vault.client.client.log_info") - def test_get_bearer_token_generates_from_credentials_string(self, mock_log, mock_generate): - result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_STRING) - mock_generate.assert_called_once() - self.assertEqual(result, "sa_token_str") - - @patch("skyflow.vault.client.client.generate_bearer_token", return_value=("new_token", None)) - @patch("skyflow.vault.client.client.is_expired", return_value=True) - @patch("skyflow.vault.client.client.log_info") - def test_get_bearer_token_regenerates_on_expiry(self, mock_log, mock_is_expired, mock_generate): - """Expired token is regenerated silently — no exception raised.""" - self.vault_client._VaultClient__bearer_token = "expired_token" - result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_PATH) - mock_generate.assert_called_once() - self.assertEqual(result, "new_token") - - @patch("skyflow.vault.client.client.generate_bearer_token") - @patch("skyflow.vault.client.client.is_expired", return_value=False) - @patch("skyflow.vault.client.client.log_info") - def test_get_bearer_token_reuses_valid_cached_token(self, mock_log, mock_is_expired, mock_generate): - """Valid cached token is reused without calling generate_bearer_token.""" - self.vault_client._VaultClient__bearer_token = "valid_token" - result = self.vault_client.get_bearer_token(CREDENTIALS_WITH_PATH) - mock_generate.assert_not_called() - self.assertEqual(result, "valid_token") - - # ------------------------------------------------------------------ # - # update_config # - # ------------------------------------------------------------------ # - - def test_update_config_sets_flag(self): - self.vault_client.update_config({"credentials": "new_credentials"}) - self.assertTrue(self.vault_client._VaultClient__is_config_updated) - self.assertEqual(self.vault_client.get_config()["credentials"], "new_credentials") - - # ------------------------------------------------------------------ # - # API accessor stubs # - # ------------------------------------------------------------------ # - - def test_get_records_api(self): - self.vault_client._VaultClient__api_client = MagicMock() - self.assertIsNotNone(self.vault_client.get_records_api()) - - def test_get_tokens_api(self): - self.vault_client._VaultClient__api_client = MagicMock() - self.assertIsNotNone(self.vault_client.get_tokens_api()) - - def test_get_query_api(self): - self.vault_client._VaultClient__api_client = MagicMock() - self.assertIsNotNone(self.vault_client.get_query_api()) - - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/requirements.txt b/v2/requirements.txt similarity index 100% rename from requirements.txt rename to v2/requirements.txt diff --git a/v2/setup.py b/v2/setup.py new file mode 100644 index 00000000..4e7f78bc --- /dev/null +++ b/v2/setup.py @@ -0,0 +1,85 @@ +''' + Copyright (c) 2022 Skyflow, Inc. +''' +import os +import shutil +import sys + +from setuptools import setup, find_packages +from setuptools.command.build_py import build_py as _build_py + + +if sys.version_info < (3, 9): + raise RuntimeError("skyflow requires Python 3.9+") +current_version = '2.1.2' + +HERE = os.path.abspath(os.path.dirname(__file__)) +REPO_ROOT = os.path.dirname(HERE) +COMMON_SRC = os.path.join(REPO_ROOT, 'common') + +with open(os.path.join(REPO_ROOT, 'README.md'), 'r', encoding='utf-8') as f: + long_description = f.read() + +# Anything under common/ that must never ride along into a built wheel. +_COMMON_EXCLUDE_DIRS = {'__pycache__', '.pytest_cache', 'tests', '.mypy_cache'} +_COMMON_EXCLUDE_FILES = {'setup.py', 'pyproject.toml', 'requirements.txt', '.gitignore'} +_COMMON_EXCLUDE_SUFFIXES = ('.egg-info',) + + +def _ignore_common_files(_directory, names): + ignored = set() + for name in names: + if name in _COMMON_EXCLUDE_DIRS or name in _COMMON_EXCLUDE_FILES: + ignored.add(name) + elif name.endswith(_COMMON_EXCLUDE_SUFFIXES): + ignored.add(name) + return ignored + + +class CustomBuildPy(_build_py): + """SK-2938 Option C: bundles the sibling common/ source tree into this variant's wheel + (bdist_wheel/build --wheel only -- sdist doesn't invoke this and isn't supported here).""" + + def run(self): + super().run() + dest = os.path.join(self.build_lib, 'common') + if os.path.exists(dest): + shutil.rmtree(dest) + shutil.copytree(COMMON_SRC, dest, ignore=_ignore_common_files) + + +setup( + name='skyflow', + version=current_version, + author='Skyflow', + author_email='service-ops@skyflow.com', + packages=find_packages(where='.', exclude=['test*', 'samples*']), + # Ship PEP 561 markers so type checkers (mypy/pyright) see the SDK's types. + package_data={ + 'skyflow': ['py.typed'], + 'skyflow.generated.rest': ['py.typed'], + }, + cmdclass={'build_py': CustomBuildPy}, + url='https://github.com/skyflowapi/skyflow-python/', + license='LICENSE', + description='Skyflow SDK for the Python programming language', + long_description=long_description, + long_description_content_type='text/markdown', + install_requires=[ + 'pydantic >= 2.0.0', + 'typing-extensions >= 4.0.0', + 'PyJWT >= 2.12, < 3', + 'requests >= 2.28.0', + 'cryptography >= 44.0.2', + 'httpx >= 0.21.2', + 'python-dotenv >= 1.1.0, < 2', + ], + extras_require={ + 'dev': [ + 'codespell >= 2.4.1', + 'ruff >= 0.9.0', + 'pre-commit >= 4.3.0', + ] + }, + python_requires=">=3.9", +) diff --git a/v2/skyflow/__init__.py b/v2/skyflow/__init__.py new file mode 100644 index 00000000..fc02764f --- /dev/null +++ b/v2/skyflow/__init__.py @@ -0,0 +1,2 @@ +from .utils import LogLevel, Env +from .client import Skyflow diff --git a/v2/skyflow/client/__init__.py b/v2/skyflow/client/__init__.py new file mode 100644 index 00000000..246ca2f6 --- /dev/null +++ b/v2/skyflow/client/__init__.py @@ -0,0 +1 @@ +from .skyflow import Skyflow diff --git a/v2/skyflow/client/skyflow.py b/v2/skyflow/client/skyflow.py new file mode 100644 index 00000000..c464b2f7 --- /dev/null +++ b/v2/skyflow/client/skyflow.py @@ -0,0 +1,17 @@ +from common.client.utils import make_skyflow_class +from skyflow.utils import SkyflowMessages +from skyflow.utils.logger import set_active_log_level +from skyflow.utils.validations import validate_connection_config, validate_update_connection_config +from skyflow.vault.client.client import VaultClient +from skyflow.vault.controller import VaultController, Connection, Detect + +Skyflow = make_skyflow_class( + vault_client_cls=VaultClient, + vault_controller_cls=VaultController, + connection_cls=Connection, + detect_cls=Detect, + skyflow_messages=SkyflowMessages, + validate_connection_config=validate_connection_config, + validate_update_connection_config=validate_update_connection_config, + set_active_log_level=set_active_log_level, +) diff --git a/v2/skyflow/error/__init__.py b/v2/skyflow/error/__init__.py new file mode 100644 index 00000000..a02e980c --- /dev/null +++ b/v2/skyflow/error/__init__.py @@ -0,0 +1,3 @@ +from common.errors import SkyflowError + +__all__ = ["SkyflowError"] \ No newline at end of file diff --git a/v2/skyflow/generated/__init__.py b/v2/skyflow/generated/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skyflow/generated/rest/__init__.py b/v2/skyflow/generated/rest/__init__.py similarity index 100% rename from skyflow/generated/rest/__init__.py rename to v2/skyflow/generated/rest/__init__.py diff --git a/skyflow/generated/rest/audit/__init__.py b/v2/skyflow/generated/rest/audit/__init__.py similarity index 100% rename from skyflow/generated/rest/audit/__init__.py rename to v2/skyflow/generated/rest/audit/__init__.py diff --git a/skyflow/generated/rest/audit/client.py b/v2/skyflow/generated/rest/audit/client.py similarity index 100% rename from skyflow/generated/rest/audit/client.py rename to v2/skyflow/generated/rest/audit/client.py diff --git a/skyflow/generated/rest/audit/raw_client.py b/v2/skyflow/generated/rest/audit/raw_client.py similarity index 100% rename from skyflow/generated/rest/audit/raw_client.py rename to v2/skyflow/generated/rest/audit/raw_client.py diff --git a/skyflow/generated/rest/audit/types/__init__.py b/v2/skyflow/generated/rest/audit/types/__init__.py similarity index 100% rename from skyflow/generated/rest/audit/types/__init__.py rename to v2/skyflow/generated/rest/audit/types/__init__.py diff --git a/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_action_type.py b/v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_action_type.py similarity index 100% rename from skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_action_type.py rename to v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_action_type.py diff --git a/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_access_type.py b/v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_access_type.py similarity index 100% rename from skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_access_type.py rename to v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_access_type.py diff --git a/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_actor_type.py b/v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_actor_type.py similarity index 100% rename from skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_actor_type.py rename to v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_actor_type.py diff --git a/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_auth_mode.py b/v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_auth_mode.py similarity index 100% rename from skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_auth_mode.py rename to v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_context_auth_mode.py diff --git a/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_resource_type.py b/v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_resource_type.py similarity index 100% rename from skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_resource_type.py rename to v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_filter_ops_resource_type.py diff --git a/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_sort_ops_order_by.py b/v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_sort_ops_order_by.py similarity index 100% rename from skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_sort_ops_order_by.py rename to v2/skyflow/generated/rest/audit/types/audit_service_list_audit_events_request_sort_ops_order_by.py diff --git a/skyflow/generated/rest/query/__init__.py b/v2/skyflow/generated/rest/authentication/__init__.py similarity index 100% rename from skyflow/generated/rest/query/__init__.py rename to v2/skyflow/generated/rest/authentication/__init__.py diff --git a/skyflow/generated/rest/authentication/client.py b/v2/skyflow/generated/rest/authentication/client.py similarity index 100% rename from skyflow/generated/rest/authentication/client.py rename to v2/skyflow/generated/rest/authentication/client.py diff --git a/skyflow/generated/rest/authentication/raw_client.py b/v2/skyflow/generated/rest/authentication/raw_client.py similarity index 100% rename from skyflow/generated/rest/authentication/raw_client.py rename to v2/skyflow/generated/rest/authentication/raw_client.py diff --git a/skyflow/generated/rest/tokens/__init__.py b/v2/skyflow/generated/rest/bin_lookup/__init__.py similarity index 100% rename from skyflow/generated/rest/tokens/__init__.py rename to v2/skyflow/generated/rest/bin_lookup/__init__.py diff --git a/skyflow/generated/rest/bin_lookup/client.py b/v2/skyflow/generated/rest/bin_lookup/client.py similarity index 100% rename from skyflow/generated/rest/bin_lookup/client.py rename to v2/skyflow/generated/rest/bin_lookup/client.py diff --git a/skyflow/generated/rest/bin_lookup/raw_client.py b/v2/skyflow/generated/rest/bin_lookup/raw_client.py similarity index 100% rename from skyflow/generated/rest/bin_lookup/raw_client.py rename to v2/skyflow/generated/rest/bin_lookup/raw_client.py diff --git a/skyflow/generated/rest/client.py b/v2/skyflow/generated/rest/client.py similarity index 100% rename from skyflow/generated/rest/client.py rename to v2/skyflow/generated/rest/client.py diff --git a/v2/skyflow/generated/rest/core/__init__.py b/v2/skyflow/generated/rest/core/__init__.py new file mode 100644 index 00000000..31bbb818 --- /dev/null +++ b/v2/skyflow/generated/rest/core/__init__.py @@ -0,0 +1,52 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +from .api_error import ApiError +from .client_wrapper import AsyncClientWrapper, BaseClientWrapper, SyncClientWrapper +from .datetime_utils import serialize_datetime +from .file import File, convert_file_dict_to_httpx_tuples, with_content_type +from .http_client import AsyncHttpClient, HttpClient +from .http_response import AsyncHttpResponse, HttpResponse +from .jsonable_encoder import jsonable_encoder +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + UniversalBaseModel, + UniversalRootModel, + parse_obj_as, + universal_field_validator, + universal_root_validator, + update_forward_refs, +) +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from .serialization import FieldMetadata, convert_and_respect_annotation_metadata + +__all__ = [ + "ApiError", + "AsyncClientWrapper", + "AsyncHttpClient", + "AsyncHttpResponse", + "BaseClientWrapper", + "FieldMetadata", + "File", + "HttpClient", + "HttpResponse", + "IS_PYDANTIC_V2", + "RequestOptions", + "SyncClientWrapper", + "UniversalBaseModel", + "UniversalRootModel", + "convert_and_respect_annotation_metadata", + "convert_file_dict_to_httpx_tuples", + "encode_query", + "jsonable_encoder", + "parse_obj_as", + "remove_none_from_dict", + "serialize_datetime", + "universal_field_validator", + "universal_root_validator", + "update_forward_refs", + "with_content_type", +] diff --git a/v2/skyflow/generated/rest/core/api_error.py b/v2/skyflow/generated/rest/core/api_error.py new file mode 100644 index 00000000..6f850a60 --- /dev/null +++ b/v2/skyflow/generated/rest/core/api_error.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Optional + + +class ApiError(Exception): + headers: Optional[Dict[str, str]] + status_code: Optional[int] + body: Any + + def __init__( + self, + *, + headers: Optional[Dict[str, str]] = None, + status_code: Optional[int] = None, + body: Any = None, + ) -> None: + self.headers = headers + self.status_code = status_code + self.body = body + + def __str__(self) -> str: + return f"headers: {self.headers}, status_code: {self.status_code}, body: {self.body}" diff --git a/skyflow/generated/rest/core/client_wrapper.py b/v2/skyflow/generated/rest/core/client_wrapper.py similarity index 100% rename from skyflow/generated/rest/core/client_wrapper.py rename to v2/skyflow/generated/rest/core/client_wrapper.py diff --git a/v2/skyflow/generated/rest/core/datetime_utils.py b/v2/skyflow/generated/rest/core/datetime_utils.py new file mode 100644 index 00000000..7c9864a9 --- /dev/null +++ b/v2/skyflow/generated/rest/core/datetime_utils.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import datetime as dt + + +def serialize_datetime(v: dt.datetime) -> str: + """ + Serialize a datetime including timezone info. + + Uses the timezone info provided if present, otherwise uses the current runtime's timezone info. + + UTC datetimes end in "Z" while all other timezones are represented as offset from UTC, e.g. +05:00. + """ + + def _serialize_zoned_datetime(v: dt.datetime) -> str: + if v.tzinfo is not None and v.tzinfo.tzname(None) == dt.timezone.utc.tzname(None): + # UTC is a special case where we use "Z" at the end instead of "+00:00" + return v.isoformat().replace("+00:00", "Z") + else: + # Delegate to the typical +/- offset format + return v.isoformat() + + if v.tzinfo is not None: + return _serialize_zoned_datetime(v) + else: + local_tz = dt.datetime.now().astimezone().tzinfo + localized_dt = v.replace(tzinfo=local_tz) + return _serialize_zoned_datetime(localized_dt) diff --git a/v2/skyflow/generated/rest/core/file.py b/v2/skyflow/generated/rest/core/file.py new file mode 100644 index 00000000..44b0d27c --- /dev/null +++ b/v2/skyflow/generated/rest/core/file.py @@ -0,0 +1,67 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import IO, Dict, List, Mapping, Optional, Tuple, Union, cast + +# File typing inspired by the flexibility of types within the httpx library +# https://github.com/encode/httpx/blob/master/httpx/_types.py +FileContent = Union[IO[bytes], bytes, str] +File = Union[ + # file (or bytes) + FileContent, + # (filename, file (or bytes)) + Tuple[Optional[str], FileContent], + # (filename, file (or bytes), content_type) + Tuple[Optional[str], FileContent, Optional[str]], + # (filename, file (or bytes), content_type, headers) + Tuple[ + Optional[str], + FileContent, + Optional[str], + Mapping[str, str], + ], +] + + +def convert_file_dict_to_httpx_tuples( + d: Dict[str, Union[File, List[File]]], +) -> List[Tuple[str, File]]: + """ + The format we use is a list of tuples, where the first element is the + name of the file and the second is the file object. Typically HTTPX wants + a dict, but to be able to send lists of files, you have to use the list + approach (which also works for non-lists) + https://github.com/encode/httpx/pull/1032 + """ + + httpx_tuples = [] + for key, file_like in d.items(): + if isinstance(file_like, list): + for file_like_item in file_like: + httpx_tuples.append((key, file_like_item)) + else: + httpx_tuples.append((key, file_like)) + return httpx_tuples + + +def with_content_type(*, file: File, default_content_type: str) -> File: + """ + This function resolves to the file's content type, if provided, and defaults + to the default_content_type value if not. + """ + if isinstance(file, tuple): + if len(file) == 2: + filename, content = cast(Tuple[Optional[str], FileContent], file) # type: ignore + return (filename, content, default_content_type) + elif len(file) == 3: + filename, content, file_content_type = cast(Tuple[Optional[str], FileContent, Optional[str]], file) # type: ignore + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type) + elif len(file) == 4: + filename, content, file_content_type, headers = cast( # type: ignore + Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]], file + ) + out_content_type = file_content_type or default_content_type + return (filename, content, out_content_type, headers) + else: + raise ValueError(f"Unexpected tuple length: {len(file)}") + return (None, file, default_content_type) diff --git a/v2/skyflow/generated/rest/core/force_multipart.py b/v2/skyflow/generated/rest/core/force_multipart.py new file mode 100644 index 00000000..ae24ccff --- /dev/null +++ b/v2/skyflow/generated/rest/core/force_multipart.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + + +class ForceMultipartDict(dict): + """ + A dictionary subclass that always evaluates to True in boolean contexts. + + This is used to force multipart/form-data encoding in HTTP requests even when + the dictionary is empty, which would normally evaluate to False. + """ + + def __bool__(self): + return True + + +FORCE_MULTIPART = ForceMultipartDict() diff --git a/v2/skyflow/generated/rest/core/http_client.py b/v2/skyflow/generated/rest/core/http_client.py new file mode 100644 index 00000000..e4173f99 --- /dev/null +++ b/v2/skyflow/generated/rest/core/http_client.py @@ -0,0 +1,543 @@ +# This file was auto-generated by Fern from our API Definition. + +import asyncio +import email.utils +import re +import time +import typing +import urllib.parse +from contextlib import asynccontextmanager, contextmanager +from random import random + +import httpx +from .file import File, convert_file_dict_to_httpx_tuples +from .force_multipart import FORCE_MULTIPART +from .jsonable_encoder import jsonable_encoder +from .query_encoder import encode_query +from .remove_none_from_dict import remove_none_from_dict +from .request_options import RequestOptions +from httpx._types import RequestFiles + +INITIAL_RETRY_DELAY_SECONDS = 0.5 +MAX_RETRY_DELAY_SECONDS = 10 +MAX_RETRY_DELAY_SECONDS_FROM_HEADER = 30 + + +def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]: + """ + This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait. + + Inspired by the urllib3 retry implementation. + """ + retry_after_ms = response_headers.get("retry-after-ms") + if retry_after_ms is not None: + try: + return int(retry_after_ms) / 1000 if retry_after_ms > 0 else 0 + except Exception: + pass + + retry_after = response_headers.get("retry-after") + if retry_after is None: + return None + + # Attempt to parse the header as an int. + if re.match(r"^\s*[0-9]+\s*$", retry_after): + seconds = float(retry_after) + # Fallback to parsing it as a date. + else: + retry_date_tuple = email.utils.parsedate_tz(retry_after) + if retry_date_tuple is None: + return None + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) + seconds = retry_date - time.time() + + if seconds < 0: + seconds = 0 + + return seconds + + +def _retry_timeout(response: httpx.Response, retries: int) -> float: + """ + Determine the amount of time to wait before retrying a request. + This function begins by trying to parse a retry-after header from the response, and then proceeds to use exponential backoff + with a jitter to determine the number of seconds to wait. + """ + + # If the API asks us to wait a certain amount of time (and it's a reasonable amount), just do what it says. + retry_after = _parse_retry_after(response.headers) + if retry_after is not None and retry_after <= MAX_RETRY_DELAY_SECONDS_FROM_HEADER: + return retry_after + + # Apply exponential backoff, capped at MAX_RETRY_DELAY_SECONDS. + retry_delay = min(INITIAL_RETRY_DELAY_SECONDS * pow(2.0, retries), MAX_RETRY_DELAY_SECONDS) + + # Add a randomness / jitter to the retry delay to avoid overwhelming the server with retries. + timeout = retry_delay * (1 - 0.25 * random()) + return timeout if timeout >= 0 else 0 + + +def _should_retry(response: httpx.Response) -> bool: + retryable_400s = [429, 408, 409] + return response.status_code >= 500 or response.status_code in retryable_400s + + +def remove_omit_from_dict( + original: typing.Dict[str, typing.Optional[typing.Any]], + omit: typing.Optional[typing.Any], +) -> typing.Dict[str, typing.Any]: + if omit is None: + return original + new: typing.Dict[str, typing.Any] = {} + for key, value in original.items(): + if value is not omit: + new[key] = value + return new + + +def maybe_filter_request_body( + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Optional[typing.Any]: + if data is None: + return ( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else None + ) + elif not isinstance(data, typing.Mapping): + data_content = jsonable_encoder(data) + else: + data_content = { + **(jsonable_encoder(remove_omit_from_dict(data, omit))), # type: ignore + **( + jsonable_encoder(request_options.get("additional_body_parameters", {})) or {} + if request_options is not None + else {} + ), + } + return data_content + + +# Abstracted out for testing purposes +def get_request_body( + *, + json: typing.Optional[typing.Any], + data: typing.Optional[typing.Any], + request_options: typing.Optional[RequestOptions], + omit: typing.Optional[typing.Any], +) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]: + json_body = None + data_body = None + if data is not None: + data_body = maybe_filter_request_body(data, request_options, omit) + else: + # If both data and json are None, we send json data in the event extra properties are specified + json_body = maybe_filter_request_body(json, request_options, omit) + + # If you have an empty JSON body, you should just send None + return (json_body if json_body != {} else None), data_body if data_body != {} else None + + +class HttpClient: + def __init__( + self, + *, + httpx_client: httpx.Client, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + response = self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + time.sleep(_retry_timeout(response=response, retries=retries)) + return self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + + return response + + @contextmanager + def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.Iterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream + + +class AsyncHttpClient: + def __init__( + self, + *, + httpx_client: httpx.AsyncClient, + base_timeout: typing.Callable[[], typing.Optional[float]], + base_headers: typing.Callable[[], typing.Dict[str, str]], + base_url: typing.Optional[typing.Callable[[], str]] = None, + ): + self.base_url = base_url + self.base_timeout = base_timeout + self.base_headers = base_headers + self.httpx_client = httpx_client + + def get_base_url(self, maybe_base_url: typing.Optional[str]) -> str: + base_url = maybe_base_url + if self.base_url is not None and base_url is None: + base_url = self.base_url() + + if base_url is None: + raise ValueError("A base_url is required to make this request, please provide one and try again.") + return base_url + + async def request( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> httpx.Response: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + # Add the input to each of these and do None-safety checks + response = await self.httpx_client.request( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) or {} + if request_options is not None + else {} + ), + }, + omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) + + max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response): + if max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + ) + return response + + @asynccontextmanager + async def stream( + self, + path: typing.Optional[str] = None, + *, + method: str, + base_url: typing.Optional[str] = None, + params: typing.Optional[typing.Dict[str, typing.Any]] = None, + json: typing.Optional[typing.Any] = None, + data: typing.Optional[typing.Any] = None, + content: typing.Optional[typing.Union[bytes, typing.Iterator[bytes], typing.AsyncIterator[bytes]]] = None, + files: typing.Optional[ + typing.Union[ + typing.Dict[str, typing.Optional[typing.Union[File, typing.List[File]]]], + typing.List[typing.Tuple[str, File]], + ] + ] = None, + headers: typing.Optional[typing.Dict[str, typing.Any]] = None, + request_options: typing.Optional[RequestOptions] = None, + retries: int = 2, + omit: typing.Optional[typing.Any] = None, + force_multipart: typing.Optional[bool] = None, + ) -> typing.AsyncIterator[httpx.Response]: + base_url = self.get_base_url(base_url) + timeout = ( + request_options.get("timeout_in_seconds") + if request_options is not None and request_options.get("timeout_in_seconds") is not None + else self.base_timeout() + ) + + request_files: typing.Optional[RequestFiles] = ( + convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) + if (files is not None and files is not omit and isinstance(files, dict)) + else None + ) + + if (request_files is None or len(request_files) == 0) and force_multipart: + request_files = FORCE_MULTIPART + + json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) + + async with self.httpx_client.stream( + method=method, + url=urllib.parse.urljoin(f"{base_url}/", path), + headers=jsonable_encoder( + remove_none_from_dict( + { + **self.base_headers(), + **(headers if headers is not None else {}), + **(request_options.get("additional_headers", {}) if request_options is not None else {}), + } + ) + ), + params=encode_query( + jsonable_encoder( + remove_none_from_dict( + remove_omit_from_dict( + { + **(params if params is not None else {}), + **( + request_options.get("additional_query_parameters", {}) + if request_options is not None + else {} + ), + }, + omit=omit, + ) + ) + ) + ), + json=json_body, + data=data_body, + content=content, + files=request_files, + timeout=timeout, + ) as stream: + yield stream diff --git a/v2/skyflow/generated/rest/core/http_response.py b/v2/skyflow/generated/rest/core/http_response.py new file mode 100644 index 00000000..48a1798a --- /dev/null +++ b/v2/skyflow/generated/rest/core/http_response.py @@ -0,0 +1,55 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Dict, Generic, TypeVar + +import httpx + +T = TypeVar("T") +"""Generic to represent the underlying type of the data wrapped by the HTTP response.""" + + +class BaseHttpResponse: + """Minimalist HTTP response wrapper that exposes response headers.""" + + _response: httpx.Response + + def __init__(self, response: httpx.Response): + self._response = response + + @property + def headers(self) -> Dict[str, str]: + return dict(self._response.headers) + + +class HttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + def close(self) -> None: + self._response.close() + + +class AsyncHttpResponse(Generic[T], BaseHttpResponse): + """HTTP response wrapper that exposes response headers and data.""" + + _data: T + + def __init__(self, response: httpx.Response, data: T): + super().__init__(response) + self._data = data + + @property + def data(self) -> T: + return self._data + + async def close(self) -> None: + await self._response.aclose() diff --git a/v2/skyflow/generated/rest/core/jsonable_encoder.py b/v2/skyflow/generated/rest/core/jsonable_encoder.py new file mode 100644 index 00000000..afee3662 --- /dev/null +++ b/v2/skyflow/generated/rest/core/jsonable_encoder.py @@ -0,0 +1,100 @@ +# This file was auto-generated by Fern from our API Definition. + +""" +jsonable_encoder converts a Python object to a JSON-friendly dict +(e.g. datetimes to strings, Pydantic models to dicts). + +Taken from FastAPI, and made a bit simpler +https://github.com/tiangolo/fastapi/blob/master/fastapi/encoders.py +""" + +import base64 +import dataclasses +import datetime as dt +from enum import Enum +from pathlib import PurePath +from types import GeneratorType +from typing import Any, Callable, Dict, List, Optional, Set, Union + +import pydantic +from .datetime_utils import serialize_datetime +from .pydantic_utilities import ( + IS_PYDANTIC_V2, + encode_by_type, + to_jsonable_with_fallback, +) + +SetIntStr = Set[Union[int, str]] +DictIntStrAny = Dict[Union[int, str], Any] + + +def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + custom_encoder = custom_encoder or {} + if custom_encoder: + if type(obj) in custom_encoder: + return custom_encoder[type(obj)](obj) + else: + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + if isinstance(obj, pydantic.BaseModel): + if IS_PYDANTIC_V2: + encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 + else: + encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 + if custom_encoder: + encoder.update(custom_encoder) + obj_dict = obj.dict(by_alias=True) + if "__root__" in obj_dict: + obj_dict = obj_dict["__root__"] + if "root" in obj_dict: + obj_dict = obj_dict["root"] + return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): + obj_dict = dataclasses.asdict(obj) # type: ignore + return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + if isinstance(obj, dict): + encoded_dict = {} + allowed_keys = set(obj.keys()) + for key, value in obj.items(): + if key in allowed_keys: + encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) + encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) + encoded_dict[encoded_key] = encoded_value + return encoded_dict + if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): + encoded_list = [] + for item in obj: + encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) + return encoded_list + + def fallback_serializer(o: Any) -> Any: + attempt_encode = encode_by_type(o) + if attempt_encode is not None: + return attempt_encode + + try: + data = dict(o) + except Exception as e: + errors: List[Exception] = [] + errors.append(e) + try: + data = vars(o) + except Exception as e: + errors.append(e) + raise ValueError(errors) from e + return jsonable_encoder(data, custom_encoder=custom_encoder) + + return to_jsonable_with_fallback(obj, fallback_serializer) diff --git a/v2/skyflow/generated/rest/core/pydantic_utilities.py b/v2/skyflow/generated/rest/core/pydantic_utilities.py new file mode 100644 index 00000000..7db29500 --- /dev/null +++ b/v2/skyflow/generated/rest/core/pydantic_utilities.py @@ -0,0 +1,255 @@ +# This file was auto-generated by Fern from our API Definition. + +# nopycln: file +import datetime as dt +from collections import defaultdict +from typing import Any, Callable, ClassVar, Dict, List, Mapping, Optional, Set, Tuple, Type, TypeVar, Union, cast + +import pydantic + +IS_PYDANTIC_V2 = pydantic.VERSION.startswith("2.") + +if IS_PYDANTIC_V2: + from pydantic.v1.datetime_parse import parse_date as parse_date + from pydantic.v1.datetime_parse import parse_datetime as parse_datetime + from pydantic.v1.fields import ModelField as ModelField + from pydantic.v1.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[attr-defined] + from pydantic.v1.typing import get_args as get_args + from pydantic.v1.typing import get_origin as get_origin + from pydantic.v1.typing import is_literal_type as is_literal_type + from pydantic.v1.typing import is_union as is_union +else: + from pydantic.datetime_parse import parse_date as parse_date # type: ignore[no-redef] + from pydantic.datetime_parse import parse_datetime as parse_datetime # type: ignore[no-redef] + from pydantic.fields import ModelField as ModelField # type: ignore[attr-defined, no-redef] + from pydantic.json import ENCODERS_BY_TYPE as encoders_by_type # type: ignore[no-redef] + from pydantic.typing import get_args as get_args # type: ignore[no-redef] + from pydantic.typing import get_origin as get_origin # type: ignore[no-redef] + from pydantic.typing import is_literal_type as is_literal_type # type: ignore[no-redef] + from pydantic.typing import is_union as is_union # type: ignore[no-redef] + +from .datetime_utils import serialize_datetime +from .serialization import convert_and_respect_annotation_metadata +from typing_extensions import TypeAlias + +T = TypeVar("T") +Model = TypeVar("Model", bound=pydantic.BaseModel) + + +def parse_obj_as(type_: Type[T], object_: Any) -> T: + dealiased_object = convert_and_respect_annotation_metadata(object_=object_, annotation=type_, direction="read") + if IS_PYDANTIC_V2: + adapter = pydantic.TypeAdapter(type_) # type: ignore[attr-defined] + return adapter.validate_python(dealiased_object) + return pydantic.parse_obj_as(type_, dealiased_object) + + +def to_jsonable_with_fallback(obj: Any, fallback_serializer: Callable[[Any], Any]) -> Any: + if IS_PYDANTIC_V2: + from pydantic_core import to_jsonable_python + + return to_jsonable_python(obj, fallback=fallback_serializer) + return fallback_serializer(obj) + + +class UniversalBaseModel(pydantic.BaseModel): + if IS_PYDANTIC_V2: + model_config: ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict( # type: ignore[typeddict-unknown-key] + # Allow fields beginning with `model_` to be used in the model + protected_namespaces=(), + ) + + @pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined] + def serialize_model(self) -> Any: # type: ignore[name-defined] + serialized = self.model_dump() + data = {k: serialize_datetime(v) if isinstance(v, dt.datetime) else v for k, v in serialized.items()} + return data + + else: + + class Config: + smart_union = True + json_encoders = {dt.datetime: serialize_datetime} + + @classmethod + def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + return cls.construct(_fields_set, **dealiased_object) + + @classmethod + def construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model": + dealiased_object = convert_and_respect_annotation_metadata(object_=values, annotation=cls, direction="read") + if IS_PYDANTIC_V2: + return super().model_construct(_fields_set, **dealiased_object) # type: ignore[misc] + return super().construct(_fields_set, **dealiased_object) + + def json(self, **kwargs: Any) -> str: + kwargs_with_defaults = { + "by_alias": True, + "exclude_unset": True, + **kwargs, + } + if IS_PYDANTIC_V2: + return super().model_dump_json(**kwargs_with_defaults) # type: ignore[misc] + return super().json(**kwargs_with_defaults) + + def dict(self, **kwargs: Any) -> Dict[str, Any]: + """ + Override the default dict method to `exclude_unset` by default. This function patches + `exclude_unset` to work include fields within non-None default values. + """ + # Note: the logic here is multiplexed given the levers exposed in Pydantic V1 vs V2 + # Pydantic V1's .dict can be extremely slow, so we do not want to call it twice. + # + # We'd ideally do the same for Pydantic V2, but it shells out to a library to serialize models + # that we have less control over, and this is less intrusive than custom serializers for now. + if IS_PYDANTIC_V2: + kwargs_with_defaults_exclude_unset = { + **kwargs, + "by_alias": True, + "exclude_unset": True, + "exclude_none": False, + } + kwargs_with_defaults_exclude_none = { + **kwargs, + "by_alias": True, + "exclude_none": True, + "exclude_unset": False, + } + dict_dump = deep_union_pydantic_dicts( + super().model_dump(**kwargs_with_defaults_exclude_unset), # type: ignore[misc] + super().model_dump(**kwargs_with_defaults_exclude_none), # type: ignore[misc] + ) + + else: + _fields_set = self.__fields_set__.copy() + + fields = _get_model_fields(self.__class__) + for name, field in fields.items(): + if name not in _fields_set: + default = _get_field_default(field) + + # If the default values are non-null act like they've been set + # This effectively allows exclude_unset to work like exclude_none where + # the latter passes through intentionally set none values. + if default is not None or ("exclude_unset" in kwargs and not kwargs["exclude_unset"]): + _fields_set.add(name) + + if default is not None: + self.__fields_set__.add(name) + + kwargs_with_defaults_exclude_unset_include_fields = { + "by_alias": True, + "exclude_unset": True, + "include": _fields_set, + **kwargs, + } + + dict_dump = super().dict(**kwargs_with_defaults_exclude_unset_include_fields) + + return convert_and_respect_annotation_metadata(object_=dict_dump, annotation=self.__class__, direction="write") + + +def _union_list_of_pydantic_dicts(source: List[Any], destination: List[Any]) -> List[Any]: + converted_list: List[Any] = [] + for i, item in enumerate(source): + destination_value = destination[i] + if isinstance(item, dict): + converted_list.append(deep_union_pydantic_dicts(item, destination_value)) + elif isinstance(item, list): + converted_list.append(_union_list_of_pydantic_dicts(item, destination_value)) + else: + converted_list.append(item) + return converted_list + + +def deep_union_pydantic_dicts(source: Dict[str, Any], destination: Dict[str, Any]) -> Dict[str, Any]: + for key, value in source.items(): + node = destination.setdefault(key, {}) + if isinstance(value, dict): + deep_union_pydantic_dicts(value, node) + # Note: we do not do this same processing for sets given we do not have sets of models + # and given the sets are unordered, the processing of the set and matching objects would + # be non-trivial. + elif isinstance(value, list): + destination[key] = _union_list_of_pydantic_dicts(value, node) + else: + destination[key] = value + + return destination + + +if IS_PYDANTIC_V2: + + class V2RootModel(UniversalBaseModel, pydantic.RootModel): # type: ignore[misc, name-defined, type-arg] + pass + + UniversalRootModel: TypeAlias = V2RootModel # type: ignore[misc] +else: + UniversalRootModel: TypeAlias = UniversalBaseModel # type: ignore[misc, no-redef] + + +def encode_by_type(o: Any) -> Any: + encoders_by_class_tuples: Dict[Callable[[Any], Any], Tuple[Any, ...]] = defaultdict(tuple) + for type_, encoder in encoders_by_type.items(): + encoders_by_class_tuples[encoder] += (type_,) + + if type(o) in encoders_by_type: + return encoders_by_type[type(o)](o) + for encoder, classes_tuple in encoders_by_class_tuples.items(): + if isinstance(o, classes_tuple): + return encoder(o) + + +def update_forward_refs(model: Type["Model"], **localns: Any) -> None: + if IS_PYDANTIC_V2: + model.model_rebuild(raise_errors=False) # type: ignore[attr-defined] + else: + model.update_forward_refs(**localns) + + +# Mirrors Pydantic's internal typing +AnyCallable = Callable[..., Any] + + +def universal_root_validator( + pre: bool = False, +) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.model_validator(mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.root_validator(pre=pre)(func)) # type: ignore[call-overload] + + return decorator + + +def universal_field_validator(field_name: str, pre: bool = False) -> Callable[[AnyCallable], AnyCallable]: + def decorator(func: AnyCallable) -> AnyCallable: + if IS_PYDANTIC_V2: + return cast(AnyCallable, pydantic.field_validator(field_name, mode="before" if pre else "after")(func)) # type: ignore[attr-defined] + return cast(AnyCallable, pydantic.validator(field_name, pre=pre)(func)) + + return decorator + + +PydanticField = Union[ModelField, pydantic.fields.FieldInfo] + + +def _get_model_fields(model: Type["Model"]) -> Mapping[str, PydanticField]: + if IS_PYDANTIC_V2: + return cast(Mapping[str, PydanticField], model.model_fields) # type: ignore[attr-defined] + return cast(Mapping[str, PydanticField], model.__fields__) + + +def _get_field_default(field: PydanticField) -> Any: + try: + value = field.get_default() # type: ignore[union-attr] + except: + value = field.default + if IS_PYDANTIC_V2: + from pydantic_core import PydanticUndefined + + if value == PydanticUndefined: + return None + return value + return value diff --git a/v2/skyflow/generated/rest/core/query_encoder.py b/v2/skyflow/generated/rest/core/query_encoder.py new file mode 100644 index 00000000..3183001d --- /dev/null +++ b/v2/skyflow/generated/rest/core/query_encoder.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, List, Optional, Tuple + +import pydantic + + +# Flattens dicts to be of the form {"key[subkey][subkey2]": value} where value is not a dict +def traverse_query_dict(dict_flat: Dict[str, Any], key_prefix: Optional[str] = None) -> List[Tuple[str, Any]]: + result = [] + for k, v in dict_flat.items(): + key = f"{key_prefix}[{k}]" if key_prefix is not None else k + if isinstance(v, dict): + result.extend(traverse_query_dict(v, key)) + elif isinstance(v, list): + for arr_v in v: + if isinstance(arr_v, dict): + result.extend(traverse_query_dict(arr_v, key)) + else: + result.append((key, arr_v)) + else: + result.append((key, v)) + return result + + +def single_query_encoder(query_key: str, query_value: Any) -> List[Tuple[str, Any]]: + if isinstance(query_value, pydantic.BaseModel) or isinstance(query_value, dict): + if isinstance(query_value, pydantic.BaseModel): + obj_dict = query_value.dict(by_alias=True) + else: + obj_dict = query_value + return traverse_query_dict(obj_dict, query_key) + elif isinstance(query_value, list): + encoded_values: List[Tuple[str, Any]] = [] + for value in query_value: + if isinstance(value, pydantic.BaseModel) or isinstance(value, dict): + if isinstance(value, pydantic.BaseModel): + obj_dict = value.dict(by_alias=True) + elif isinstance(value, dict): + obj_dict = value + + encoded_values.extend(single_query_encoder(query_key, obj_dict)) + else: + encoded_values.append((query_key, value)) + + return encoded_values + + return [(query_key, query_value)] + + +def encode_query(query: Optional[Dict[str, Any]]) -> Optional[List[Tuple[str, Any]]]: + if query is None: + return None + + encoded_query = [] + for k, v in query.items(): + encoded_query.extend(single_query_encoder(k, v)) + return encoded_query diff --git a/v2/skyflow/generated/rest/core/remove_none_from_dict.py b/v2/skyflow/generated/rest/core/remove_none_from_dict.py new file mode 100644 index 00000000..c2298143 --- /dev/null +++ b/v2/skyflow/generated/rest/core/remove_none_from_dict.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +from typing import Any, Dict, Mapping, Optional + + +def remove_none_from_dict(original: Mapping[str, Optional[Any]]) -> Dict[str, Any]: + new: Dict[str, Any] = {} + for key, value in original.items(): + if value is not None: + new[key] = value + return new diff --git a/v2/skyflow/generated/rest/core/request_options.py b/v2/skyflow/generated/rest/core/request_options.py new file mode 100644 index 00000000..1b388044 --- /dev/null +++ b/v2/skyflow/generated/rest/core/request_options.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +try: + from typing import NotRequired # type: ignore +except ImportError: + from typing_extensions import NotRequired + + +class RequestOptions(typing.TypedDict, total=False): + """ + Additional options for request-specific configuration when calling APIs via the SDK. + This is used primarily as an optional final parameter for service functions. + + Attributes: + - timeout_in_seconds: int. The number of seconds to await an API call before timing out. + + - max_retries: int. The max number of retries to attempt if the API call fails. + + - additional_headers: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's header dict + + - additional_query_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's query parameters dict + + - additional_body_parameters: typing.Dict[str, typing.Any]. A dictionary containing additional parameters to spread into the request's body parameters dict + + - chunk_size: int. The size, in bytes, to process each chunk of data being streamed back within the response. This equates to leveraging `chunk_size` within `requests` or `httpx`, and is only leveraged for file downloads. + """ + + timeout_in_seconds: NotRequired[int] + max_retries: NotRequired[int] + additional_headers: NotRequired[typing.Dict[str, typing.Any]] + additional_query_parameters: NotRequired[typing.Dict[str, typing.Any]] + additional_body_parameters: NotRequired[typing.Dict[str, typing.Any]] + chunk_size: NotRequired[int] diff --git a/v2/skyflow/generated/rest/core/serialization.py b/v2/skyflow/generated/rest/core/serialization.py new file mode 100644 index 00000000..c36e865c --- /dev/null +++ b/v2/skyflow/generated/rest/core/serialization.py @@ -0,0 +1,276 @@ +# This file was auto-generated by Fern from our API Definition. + +import collections +import inspect +import typing + +import pydantic +import typing_extensions + + +class FieldMetadata: + """ + Metadata class used to annotate fields to provide additional information. + + Example: + class MyDict(TypedDict): + field: typing.Annotated[str, FieldMetadata(alias="field_name")] + + Will serialize: `{"field": "value"}` + To: `{"field_name": "value"}` + """ + + alias: str + + def __init__(self, *, alias: str) -> None: + self.alias = alias + + +def convert_and_respect_annotation_metadata( + *, + object_: typing.Any, + annotation: typing.Any, + inner_type: typing.Optional[typing.Any] = None, + direction: typing.Literal["read", "write"], +) -> typing.Any: + """ + Respect the metadata annotations on a field, such as aliasing. This function effectively + manipulates the dict-form of an object to respect the metadata annotations. This is primarily used for + TypedDicts, which cannot support aliasing out of the box, and can be extended for additional + utilities, such as defaults. + + Parameters + ---------- + object_ : typing.Any + + annotation : type + The type we're looking to apply typing annotations from + + inner_type : typing.Optional[type] + + Returns + ------- + typing.Any + """ + + if object_ is None: + return None + if inner_type is None: + inner_type = annotation + + clean_type = _remove_annotations(inner_type) + # Pydantic models + if ( + inspect.isclass(clean_type) + and issubclass(clean_type, pydantic.BaseModel) + and isinstance(object_, typing.Mapping) + ): + return _convert_mapping(object_, clean_type, direction) + # TypedDicts + if typing_extensions.is_typeddict(clean_type) and isinstance(object_, typing.Mapping): + return _convert_mapping(object_, clean_type, direction) + + if ( + typing_extensions.get_origin(clean_type) == typing.Dict + or typing_extensions.get_origin(clean_type) == dict + or clean_type == typing.Dict + ) and isinstance(object_, typing.Dict): + key_type = typing_extensions.get_args(clean_type)[0] + value_type = typing_extensions.get_args(clean_type)[1] + + return { + key: convert_and_respect_annotation_metadata( + object_=value, + annotation=annotation, + inner_type=value_type, + direction=direction, + ) + for key, value in object_.items() + } + + # If you're iterating on a string, do not bother to coerce it to a sequence. + if not isinstance(object_, str): + if ( + typing_extensions.get_origin(clean_type) == typing.Set + or typing_extensions.get_origin(clean_type) == set + or clean_type == typing.Set + ) and isinstance(object_, typing.Set): + inner_type = typing_extensions.get_args(clean_type)[0] + return { + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + } + elif ( + ( + typing_extensions.get_origin(clean_type) == typing.List + or typing_extensions.get_origin(clean_type) == list + or clean_type == typing.List + ) + and isinstance(object_, typing.List) + ) or ( + ( + typing_extensions.get_origin(clean_type) == typing.Sequence + or typing_extensions.get_origin(clean_type) == collections.abc.Sequence + or clean_type == typing.Sequence + ) + and isinstance(object_, typing.Sequence) + ): + inner_type = typing_extensions.get_args(clean_type)[0] + return [ + convert_and_respect_annotation_metadata( + object_=item, + annotation=annotation, + inner_type=inner_type, + direction=direction, + ) + for item in object_ + ] + + if typing_extensions.get_origin(clean_type) == typing.Union: + # We should be able to ~relatively~ safely try to convert keys against all + # member types in the union, the edge case here is if one member aliases a field + # of the same name to a different name from another member + # Or if another member aliases a field of the same name that another member does not. + for member in typing_extensions.get_args(clean_type): + object_ = convert_and_respect_annotation_metadata( + object_=object_, + annotation=annotation, + inner_type=member, + direction=direction, + ) + return object_ + + annotated_type = _get_annotation(annotation) + if annotated_type is None: + return object_ + + # If the object is not a TypedDict, a Union, or other container (list, set, sequence, etc.) + # Then we can safely call it on the recursive conversion. + return object_ + + +def _convert_mapping( + object_: typing.Mapping[str, object], + expected_type: typing.Any, + direction: typing.Literal["read", "write"], +) -> typing.Mapping[str, object]: + converted_object: typing.Dict[str, object] = {} + try: + annotations = typing_extensions.get_type_hints(expected_type, include_extras=True) + except NameError: + # The TypedDict contains a circular reference, so + # we use the __annotations__ attribute directly. + annotations = getattr(expected_type, "__annotations__", {}) + aliases_to_field_names = _get_alias_to_field_name(annotations) + for key, value in object_.items(): + if direction == "read" and key in aliases_to_field_names: + dealiased_key = aliases_to_field_names.get(key) + if dealiased_key is not None: + type_ = annotations.get(dealiased_key) + else: + type_ = annotations.get(key) + # Note you can't get the annotation by the field name if you're in read mode, so you must check the aliases map + # + # So this is effectively saying if we're in write mode, and we don't have a type, or if we're in read mode and we don't have an alias + # then we can just pass the value through as is + if type_ is None: + converted_object[key] = value + elif direction == "read" and key not in aliases_to_field_names: + converted_object[key] = convert_and_respect_annotation_metadata( + object_=value, annotation=type_, direction=direction + ) + else: + converted_object[_alias_key(key, type_, direction, aliases_to_field_names)] = ( + convert_and_respect_annotation_metadata(object_=value, annotation=type_, direction=direction) + ) + return converted_object + + +def _get_annotation(type_: typing.Any) -> typing.Optional[typing.Any]: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return None + + if maybe_annotated_type == typing_extensions.NotRequired: + type_ = typing_extensions.get_args(type_)[0] + maybe_annotated_type = typing_extensions.get_origin(type_) + + if maybe_annotated_type == typing_extensions.Annotated: + return type_ + + return None + + +def _remove_annotations(type_: typing.Any) -> typing.Any: + maybe_annotated_type = typing_extensions.get_origin(type_) + if maybe_annotated_type is None: + return type_ + + if maybe_annotated_type == typing_extensions.NotRequired: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + if maybe_annotated_type == typing_extensions.Annotated: + return _remove_annotations(typing_extensions.get_args(type_)[0]) + + return type_ + + +def get_alias_to_field_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_alias_to_field_name(annotations) + + +def get_field_to_alias_mapping(type_: typing.Any) -> typing.Dict[str, str]: + annotations = typing_extensions.get_type_hints(type_, include_extras=True) + return _get_field_to_alias_name(annotations) + + +def _get_alias_to_field_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[maybe_alias] = field + return aliases + + +def _get_field_to_alias_name( + field_to_hint: typing.Dict[str, typing.Any], +) -> typing.Dict[str, str]: + aliases = {} + for field, hint in field_to_hint.items(): + maybe_alias = _get_alias_from_type(hint) + if maybe_alias is not None: + aliases[field] = maybe_alias + return aliases + + +def _get_alias_from_type(type_: typing.Any) -> typing.Optional[str]: + maybe_annotated_type = _get_annotation(type_) + + if maybe_annotated_type is not None: + # The actual annotations are 1 onward, the first is the annotated type + annotations = typing_extensions.get_args(maybe_annotated_type)[1:] + + for annotation in annotations: + if isinstance(annotation, FieldMetadata) and annotation.alias is not None: + return annotation.alias + return None + + +def _alias_key( + key: str, + type_: typing.Any, + direction: typing.Literal["read", "write"], + aliases_to_field_names: typing.Dict[str, str], +) -> str: + if direction == "read": + return aliases_to_field_names.get(key, key) + return _get_alias_from_type(type_=type_) or key diff --git a/skyflow/generated/rest/environment.py b/v2/skyflow/generated/rest/environment.py similarity index 100% rename from skyflow/generated/rest/environment.py rename to v2/skyflow/generated/rest/environment.py diff --git a/skyflow/generated/rest/errors/__init__.py b/v2/skyflow/generated/rest/errors/__init__.py similarity index 100% rename from skyflow/generated/rest/errors/__init__.py rename to v2/skyflow/generated/rest/errors/__init__.py diff --git a/skyflow/generated/rest/errors/bad_request_error.py b/v2/skyflow/generated/rest/errors/bad_request_error.py similarity index 100% rename from skyflow/generated/rest/errors/bad_request_error.py rename to v2/skyflow/generated/rest/errors/bad_request_error.py diff --git a/skyflow/generated/rest/errors/internal_server_error.py b/v2/skyflow/generated/rest/errors/internal_server_error.py similarity index 100% rename from skyflow/generated/rest/errors/internal_server_error.py rename to v2/skyflow/generated/rest/errors/internal_server_error.py diff --git a/skyflow/generated/rest/errors/not_found_error.py b/v2/skyflow/generated/rest/errors/not_found_error.py similarity index 100% rename from skyflow/generated/rest/errors/not_found_error.py rename to v2/skyflow/generated/rest/errors/not_found_error.py diff --git a/skyflow/generated/rest/errors/unauthorized_error.py b/v2/skyflow/generated/rest/errors/unauthorized_error.py similarity index 100% rename from skyflow/generated/rest/errors/unauthorized_error.py rename to v2/skyflow/generated/rest/errors/unauthorized_error.py diff --git a/skyflow/generated/rest/files/__init__.py b/v2/skyflow/generated/rest/files/__init__.py similarity index 100% rename from skyflow/generated/rest/files/__init__.py rename to v2/skyflow/generated/rest/files/__init__.py diff --git a/skyflow/generated/rest/files/client.py b/v2/skyflow/generated/rest/files/client.py similarity index 100% rename from skyflow/generated/rest/files/client.py rename to v2/skyflow/generated/rest/files/client.py diff --git a/skyflow/generated/rest/files/raw_client.py b/v2/skyflow/generated/rest/files/raw_client.py similarity index 100% rename from skyflow/generated/rest/files/raw_client.py rename to v2/skyflow/generated/rest/files/raw_client.py diff --git a/skyflow/generated/rest/files/types/__init__.py b/v2/skyflow/generated/rest/files/types/__init__.py similarity index 100% rename from skyflow/generated/rest/files/types/__init__.py rename to v2/skyflow/generated/rest/files/types/__init__.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_output_transcription.py b/v2/skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_output_transcription.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_output_transcription.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_audio_request_deidentify_audio_output_transcription.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_document_pdf_request_deidentify_pdf_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_document_pdf_request_deidentify_pdf_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_document_pdf_request_deidentify_pdf_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_document_pdf_request_deidentify_pdf_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_masking_method.py b/v2/skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_masking_method.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_masking_method.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_image_request_deidentify_image_masking_method.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_document_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_document_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_request_deidentify_document_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_document_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_presentation_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_presentation_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_request_deidentify_presentation_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_presentation_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_spreadsheet_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_spreadsheet_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_request_deidentify_spreadsheet_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_spreadsheet_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_structured_text_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_structured_text_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_request_deidentify_structured_text_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_structured_text_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_text_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_text_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_request_deidentify_text_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_request_deidentify_text_entity_types_item.py diff --git a/skyflow/generated/rest/files/types/deidentify_file_request_entity_types_item.py b/v2/skyflow/generated/rest/files/types/deidentify_file_request_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/files/types/deidentify_file_request_entity_types_item.py rename to v2/skyflow/generated/rest/files/types/deidentify_file_request_entity_types_item.py diff --git a/v2/skyflow/generated/rest/guardrails/__init__.py b/v2/skyflow/generated/rest/guardrails/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/v2/skyflow/generated/rest/guardrails/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/skyflow/generated/rest/guardrails/client.py b/v2/skyflow/generated/rest/guardrails/client.py similarity index 100% rename from skyflow/generated/rest/guardrails/client.py rename to v2/skyflow/generated/rest/guardrails/client.py diff --git a/skyflow/generated/rest/guardrails/raw_client.py b/v2/skyflow/generated/rest/guardrails/raw_client.py similarity index 100% rename from skyflow/generated/rest/guardrails/raw_client.py rename to v2/skyflow/generated/rest/guardrails/raw_client.py diff --git a/v2/skyflow/generated/rest/py.typed b/v2/skyflow/generated/rest/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/v2/skyflow/generated/rest/query/__init__.py b/v2/skyflow/generated/rest/query/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/v2/skyflow/generated/rest/query/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/skyflow/generated/rest/query/client.py b/v2/skyflow/generated/rest/query/client.py similarity index 100% rename from skyflow/generated/rest/query/client.py rename to v2/skyflow/generated/rest/query/client.py diff --git a/skyflow/generated/rest/query/raw_client.py b/v2/skyflow/generated/rest/query/raw_client.py similarity index 100% rename from skyflow/generated/rest/query/raw_client.py rename to v2/skyflow/generated/rest/query/raw_client.py diff --git a/skyflow/generated/rest/records/__init__.py b/v2/skyflow/generated/rest/records/__init__.py similarity index 100% rename from skyflow/generated/rest/records/__init__.py rename to v2/skyflow/generated/rest/records/__init__.py diff --git a/skyflow/generated/rest/records/client.py b/v2/skyflow/generated/rest/records/client.py similarity index 100% rename from skyflow/generated/rest/records/client.py rename to v2/skyflow/generated/rest/records/client.py diff --git a/skyflow/generated/rest/records/raw_client.py b/v2/skyflow/generated/rest/records/raw_client.py similarity index 100% rename from skyflow/generated/rest/records/raw_client.py rename to v2/skyflow/generated/rest/records/raw_client.py diff --git a/skyflow/generated/rest/records/types/__init__.py b/v2/skyflow/generated/rest/records/types/__init__.py similarity index 100% rename from skyflow/generated/rest/records/types/__init__.py rename to v2/skyflow/generated/rest/records/types/__init__.py diff --git a/skyflow/generated/rest/records/types/record_service_bulk_get_record_request_order_by.py b/v2/skyflow/generated/rest/records/types/record_service_bulk_get_record_request_order_by.py similarity index 100% rename from skyflow/generated/rest/records/types/record_service_bulk_get_record_request_order_by.py rename to v2/skyflow/generated/rest/records/types/record_service_bulk_get_record_request_order_by.py diff --git a/skyflow/generated/rest/records/types/record_service_bulk_get_record_request_redaction.py b/v2/skyflow/generated/rest/records/types/record_service_bulk_get_record_request_redaction.py similarity index 100% rename from skyflow/generated/rest/records/types/record_service_bulk_get_record_request_redaction.py rename to v2/skyflow/generated/rest/records/types/record_service_bulk_get_record_request_redaction.py diff --git a/skyflow/generated/rest/records/types/record_service_get_record_request_redaction.py b/v2/skyflow/generated/rest/records/types/record_service_get_record_request_redaction.py similarity index 100% rename from skyflow/generated/rest/records/types/record_service_get_record_request_redaction.py rename to v2/skyflow/generated/rest/records/types/record_service_get_record_request_redaction.py diff --git a/skyflow/generated/rest/strings/__init__.py b/v2/skyflow/generated/rest/strings/__init__.py similarity index 100% rename from skyflow/generated/rest/strings/__init__.py rename to v2/skyflow/generated/rest/strings/__init__.py diff --git a/skyflow/generated/rest/strings/client.py b/v2/skyflow/generated/rest/strings/client.py similarity index 100% rename from skyflow/generated/rest/strings/client.py rename to v2/skyflow/generated/rest/strings/client.py diff --git a/skyflow/generated/rest/strings/raw_client.py b/v2/skyflow/generated/rest/strings/raw_client.py similarity index 100% rename from skyflow/generated/rest/strings/raw_client.py rename to v2/skyflow/generated/rest/strings/raw_client.py diff --git a/skyflow/generated/rest/strings/types/__init__.py b/v2/skyflow/generated/rest/strings/types/__init__.py similarity index 100% rename from skyflow/generated/rest/strings/types/__init__.py rename to v2/skyflow/generated/rest/strings/types/__init__.py diff --git a/skyflow/generated/rest/strings/types/deidentify_string_request_entity_types_item.py b/v2/skyflow/generated/rest/strings/types/deidentify_string_request_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/strings/types/deidentify_string_request_entity_types_item.py rename to v2/skyflow/generated/rest/strings/types/deidentify_string_request_entity_types_item.py diff --git a/v2/skyflow/generated/rest/tokens/__init__.py b/v2/skyflow/generated/rest/tokens/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/v2/skyflow/generated/rest/tokens/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/skyflow/generated/rest/tokens/client.py b/v2/skyflow/generated/rest/tokens/client.py similarity index 100% rename from skyflow/generated/rest/tokens/client.py rename to v2/skyflow/generated/rest/tokens/client.py diff --git a/skyflow/generated/rest/tokens/raw_client.py b/v2/skyflow/generated/rest/tokens/raw_client.py similarity index 100% rename from skyflow/generated/rest/tokens/raw_client.py rename to v2/skyflow/generated/rest/tokens/raw_client.py diff --git a/skyflow/generated/rest/types/__init__.py b/v2/skyflow/generated/rest/types/__init__.py similarity index 100% rename from skyflow/generated/rest/types/__init__.py rename to v2/skyflow/generated/rest/types/__init__.py diff --git a/skyflow/generated/rest/types/audit_event_audit_resource_type.py b/v2/skyflow/generated/rest/types/audit_event_audit_resource_type.py similarity index 100% rename from skyflow/generated/rest/types/audit_event_audit_resource_type.py rename to v2/skyflow/generated/rest/types/audit_event_audit_resource_type.py diff --git a/skyflow/generated/rest/types/audit_event_context.py b/v2/skyflow/generated/rest/types/audit_event_context.py similarity index 100% rename from skyflow/generated/rest/types/audit_event_context.py rename to v2/skyflow/generated/rest/types/audit_event_context.py diff --git a/skyflow/generated/rest/types/audit_event_data.py b/v2/skyflow/generated/rest/types/audit_event_data.py similarity index 100% rename from skyflow/generated/rest/types/audit_event_data.py rename to v2/skyflow/generated/rest/types/audit_event_data.py diff --git a/skyflow/generated/rest/types/audit_event_http_info.py b/v2/skyflow/generated/rest/types/audit_event_http_info.py similarity index 100% rename from skyflow/generated/rest/types/audit_event_http_info.py rename to v2/skyflow/generated/rest/types/audit_event_http_info.py diff --git a/skyflow/generated/rest/types/batch_record_method.py b/v2/skyflow/generated/rest/types/batch_record_method.py similarity index 100% rename from skyflow/generated/rest/types/batch_record_method.py rename to v2/skyflow/generated/rest/types/batch_record_method.py diff --git a/skyflow/generated/rest/types/context_access_type.py b/v2/skyflow/generated/rest/types/context_access_type.py similarity index 100% rename from skyflow/generated/rest/types/context_access_type.py rename to v2/skyflow/generated/rest/types/context_access_type.py diff --git a/skyflow/generated/rest/types/context_auth_mode.py b/v2/skyflow/generated/rest/types/context_auth_mode.py similarity index 100% rename from skyflow/generated/rest/types/context_auth_mode.py rename to v2/skyflow/generated/rest/types/context_auth_mode.py diff --git a/skyflow/generated/rest/types/deidentified_file_output.py b/v2/skyflow/generated/rest/types/deidentified_file_output.py similarity index 100% rename from skyflow/generated/rest/types/deidentified_file_output.py rename to v2/skyflow/generated/rest/types/deidentified_file_output.py diff --git a/skyflow/generated/rest/types/deidentified_file_output_processed_file_extension.py b/v2/skyflow/generated/rest/types/deidentified_file_output_processed_file_extension.py similarity index 100% rename from skyflow/generated/rest/types/deidentified_file_output_processed_file_extension.py rename to v2/skyflow/generated/rest/types/deidentified_file_output_processed_file_extension.py diff --git a/skyflow/generated/rest/types/deidentified_file_output_processed_file_type.py b/v2/skyflow/generated/rest/types/deidentified_file_output_processed_file_type.py similarity index 100% rename from skyflow/generated/rest/types/deidentified_file_output_processed_file_type.py rename to v2/skyflow/generated/rest/types/deidentified_file_output_processed_file_type.py diff --git a/skyflow/generated/rest/types/deidentify_file_response.py b/v2/skyflow/generated/rest/types/deidentify_file_response.py similarity index 100% rename from skyflow/generated/rest/types/deidentify_file_response.py rename to v2/skyflow/generated/rest/types/deidentify_file_response.py diff --git a/skyflow/generated/rest/types/deidentify_string_response.py b/v2/skyflow/generated/rest/types/deidentify_string_response.py similarity index 100% rename from skyflow/generated/rest/types/deidentify_string_response.py rename to v2/skyflow/generated/rest/types/deidentify_string_response.py diff --git a/skyflow/generated/rest/types/detect_guardrails_response.py b/v2/skyflow/generated/rest/types/detect_guardrails_response.py similarity index 100% rename from skyflow/generated/rest/types/detect_guardrails_response.py rename to v2/skyflow/generated/rest/types/detect_guardrails_response.py diff --git a/skyflow/generated/rest/types/detect_guardrails_response_validation.py b/v2/skyflow/generated/rest/types/detect_guardrails_response_validation.py similarity index 100% rename from skyflow/generated/rest/types/detect_guardrails_response_validation.py rename to v2/skyflow/generated/rest/types/detect_guardrails_response_validation.py diff --git a/skyflow/generated/rest/types/detect_runs_response.py b/v2/skyflow/generated/rest/types/detect_runs_response.py similarity index 100% rename from skyflow/generated/rest/types/detect_runs_response.py rename to v2/skyflow/generated/rest/types/detect_runs_response.py diff --git a/skyflow/generated/rest/types/detect_runs_response_output_type.py b/v2/skyflow/generated/rest/types/detect_runs_response_output_type.py similarity index 100% rename from skyflow/generated/rest/types/detect_runs_response_output_type.py rename to v2/skyflow/generated/rest/types/detect_runs_response_output_type.py diff --git a/skyflow/generated/rest/types/detect_runs_response_status.py b/v2/skyflow/generated/rest/types/detect_runs_response_status.py similarity index 100% rename from skyflow/generated/rest/types/detect_runs_response_status.py rename to v2/skyflow/generated/rest/types/detect_runs_response_status.py diff --git a/skyflow/generated/rest/types/detokenize_record_response_value_type.py b/v2/skyflow/generated/rest/types/detokenize_record_response_value_type.py similarity index 100% rename from skyflow/generated/rest/types/detokenize_record_response_value_type.py rename to v2/skyflow/generated/rest/types/detokenize_record_response_value_type.py diff --git a/skyflow/generated/rest/types/error_response.py b/v2/skyflow/generated/rest/types/error_response.py similarity index 100% rename from skyflow/generated/rest/types/error_response.py rename to v2/skyflow/generated/rest/types/error_response.py diff --git a/skyflow/generated/rest/types/error_response_error.py b/v2/skyflow/generated/rest/types/error_response_error.py similarity index 100% rename from skyflow/generated/rest/types/error_response_error.py rename to v2/skyflow/generated/rest/types/error_response_error.py diff --git a/skyflow/generated/rest/types/file_data.py b/v2/skyflow/generated/rest/types/file_data.py similarity index 100% rename from skyflow/generated/rest/types/file_data.py rename to v2/skyflow/generated/rest/types/file_data.py diff --git a/skyflow/generated/rest/types/file_data_data_format.py b/v2/skyflow/generated/rest/types/file_data_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_data_format.py rename to v2/skyflow/generated/rest/types/file_data_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_audio.py b/v2/skyflow/generated/rest/types/file_data_deidentify_audio.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_audio.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_audio.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_audio_data_format.py b/v2/skyflow/generated/rest/types/file_data_deidentify_audio_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_audio_data_format.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_audio_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_document.py b/v2/skyflow/generated/rest/types/file_data_deidentify_document.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_document.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_document.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_document_data_format.py b/v2/skyflow/generated/rest/types/file_data_deidentify_document_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_document_data_format.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_document_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_image.py b/v2/skyflow/generated/rest/types/file_data_deidentify_image.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_image.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_image.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_image_data_format.py b/v2/skyflow/generated/rest/types/file_data_deidentify_image_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_image_data_format.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_image_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_pdf.py b/v2/skyflow/generated/rest/types/file_data_deidentify_pdf.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_pdf.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_pdf.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_presentation.py b/v2/skyflow/generated/rest/types/file_data_deidentify_presentation.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_presentation.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_presentation.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_presentation_data_format.py b/v2/skyflow/generated/rest/types/file_data_deidentify_presentation_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_presentation_data_format.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_presentation_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_spreadsheet.py b/v2/skyflow/generated/rest/types/file_data_deidentify_spreadsheet.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_spreadsheet.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_spreadsheet.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_spreadsheet_data_format.py b/v2/skyflow/generated/rest/types/file_data_deidentify_spreadsheet_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_spreadsheet_data_format.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_spreadsheet_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_structured_text.py b/v2/skyflow/generated/rest/types/file_data_deidentify_structured_text.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_structured_text.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_structured_text.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_structured_text_data_format.py b/v2/skyflow/generated/rest/types/file_data_deidentify_structured_text_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_structured_text_data_format.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_structured_text_data_format.py diff --git a/skyflow/generated/rest/types/file_data_deidentify_text.py b/v2/skyflow/generated/rest/types/file_data_deidentify_text.py similarity index 100% rename from skyflow/generated/rest/types/file_data_deidentify_text.py rename to v2/skyflow/generated/rest/types/file_data_deidentify_text.py diff --git a/skyflow/generated/rest/types/file_data_reidentify_file.py b/v2/skyflow/generated/rest/types/file_data_reidentify_file.py similarity index 100% rename from skyflow/generated/rest/types/file_data_reidentify_file.py rename to v2/skyflow/generated/rest/types/file_data_reidentify_file.py diff --git a/skyflow/generated/rest/types/file_data_reidentify_file_data_format.py b/v2/skyflow/generated/rest/types/file_data_reidentify_file_data_format.py similarity index 100% rename from skyflow/generated/rest/types/file_data_reidentify_file_data_format.py rename to v2/skyflow/generated/rest/types/file_data_reidentify_file_data_format.py diff --git a/skyflow/generated/rest/types/format.py b/v2/skyflow/generated/rest/types/format.py similarity index 100% rename from skyflow/generated/rest/types/format.py rename to v2/skyflow/generated/rest/types/format.py diff --git a/skyflow/generated/rest/types/format_masked_item.py b/v2/skyflow/generated/rest/types/format_masked_item.py similarity index 100% rename from skyflow/generated/rest/types/format_masked_item.py rename to v2/skyflow/generated/rest/types/format_masked_item.py diff --git a/skyflow/generated/rest/types/format_plaintext_item.py b/v2/skyflow/generated/rest/types/format_plaintext_item.py similarity index 100% rename from skyflow/generated/rest/types/format_plaintext_item.py rename to v2/skyflow/generated/rest/types/format_plaintext_item.py diff --git a/skyflow/generated/rest/types/format_redacted_item.py b/v2/skyflow/generated/rest/types/format_redacted_item.py similarity index 100% rename from skyflow/generated/rest/types/format_redacted_item.py rename to v2/skyflow/generated/rest/types/format_redacted_item.py diff --git a/v2/skyflow/generated/rest/types/googlerpc_status.py b/v2/skyflow/generated/rest/types/googlerpc_status.py new file mode 100644 index 00000000..f0a885b4 --- /dev/null +++ b/v2/skyflow/generated/rest/types/googlerpc_status.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .protobuf_any import ProtobufAny + + +class GooglerpcStatus(UniversalBaseModel): + code: typing.Optional[int] = None + message: typing.Optional[str] = None + details: typing.Optional[typing.List[ProtobufAny]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/skyflow/generated/rest/types/http_code.py b/v2/skyflow/generated/rest/types/http_code.py similarity index 100% rename from skyflow/generated/rest/types/http_code.py rename to v2/skyflow/generated/rest/types/http_code.py diff --git a/skyflow/generated/rest/types/identify_response.py b/v2/skyflow/generated/rest/types/identify_response.py similarity index 100% rename from skyflow/generated/rest/types/identify_response.py rename to v2/skyflow/generated/rest/types/identify_response.py diff --git a/skyflow/generated/rest/types/locations.py b/v2/skyflow/generated/rest/types/locations.py similarity index 100% rename from skyflow/generated/rest/types/locations.py rename to v2/skyflow/generated/rest/types/locations.py diff --git a/v2/skyflow/generated/rest/types/protobuf_any.py b/v2/skyflow/generated/rest/types/protobuf_any.py new file mode 100644 index 00000000..9062870c --- /dev/null +++ b/v2/skyflow/generated/rest/types/protobuf_any.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class ProtobufAny(UniversalBaseModel): + type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="@type")] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/skyflow/generated/rest/types/redaction_enum_redaction.py b/v2/skyflow/generated/rest/types/redaction_enum_redaction.py similarity index 100% rename from skyflow/generated/rest/types/redaction_enum_redaction.py rename to v2/skyflow/generated/rest/types/redaction_enum_redaction.py diff --git a/skyflow/generated/rest/types/reidentified_file_output.py b/v2/skyflow/generated/rest/types/reidentified_file_output.py similarity index 100% rename from skyflow/generated/rest/types/reidentified_file_output.py rename to v2/skyflow/generated/rest/types/reidentified_file_output.py diff --git a/skyflow/generated/rest/types/reidentified_file_output_processed_file_extension.py b/v2/skyflow/generated/rest/types/reidentified_file_output_processed_file_extension.py similarity index 100% rename from skyflow/generated/rest/types/reidentified_file_output_processed_file_extension.py rename to v2/skyflow/generated/rest/types/reidentified_file_output_processed_file_extension.py diff --git a/skyflow/generated/rest/types/reidentify_file_response.py b/v2/skyflow/generated/rest/types/reidentify_file_response.py similarity index 100% rename from skyflow/generated/rest/types/reidentify_file_response.py rename to v2/skyflow/generated/rest/types/reidentify_file_response.py diff --git a/skyflow/generated/rest/types/reidentify_file_response_output_type.py b/v2/skyflow/generated/rest/types/reidentify_file_response_output_type.py similarity index 100% rename from skyflow/generated/rest/types/reidentify_file_response_output_type.py rename to v2/skyflow/generated/rest/types/reidentify_file_response_output_type.py diff --git a/skyflow/generated/rest/types/reidentify_file_response_status.py b/v2/skyflow/generated/rest/types/reidentify_file_response_status.py similarity index 100% rename from skyflow/generated/rest/types/reidentify_file_response_status.py rename to v2/skyflow/generated/rest/types/reidentify_file_response_status.py diff --git a/skyflow/generated/rest/types/request_action_type.py b/v2/skyflow/generated/rest/types/request_action_type.py similarity index 100% rename from skyflow/generated/rest/types/request_action_type.py rename to v2/skyflow/generated/rest/types/request_action_type.py diff --git a/skyflow/generated/rest/types/resource_id.py b/v2/skyflow/generated/rest/types/resource_id.py similarity index 100% rename from skyflow/generated/rest/types/resource_id.py rename to v2/skyflow/generated/rest/types/resource_id.py diff --git a/skyflow/generated/rest/types/shift_dates.py b/v2/skyflow/generated/rest/types/shift_dates.py similarity index 100% rename from skyflow/generated/rest/types/shift_dates.py rename to v2/skyflow/generated/rest/types/shift_dates.py diff --git a/skyflow/generated/rest/types/shift_dates_entity_types_item.py b/v2/skyflow/generated/rest/types/shift_dates_entity_types_item.py similarity index 100% rename from skyflow/generated/rest/types/shift_dates_entity_types_item.py rename to v2/skyflow/generated/rest/types/shift_dates_entity_types_item.py diff --git a/skyflow/generated/rest/types/string_response_entities.py b/v2/skyflow/generated/rest/types/string_response_entities.py similarity index 100% rename from skyflow/generated/rest/types/string_response_entities.py rename to v2/skyflow/generated/rest/types/string_response_entities.py diff --git a/skyflow/generated/rest/types/token_type_mapping.py b/v2/skyflow/generated/rest/types/token_type_mapping.py similarity index 100% rename from skyflow/generated/rest/types/token_type_mapping.py rename to v2/skyflow/generated/rest/types/token_type_mapping.py diff --git a/skyflow/generated/rest/types/token_type_mapping_default.py b/v2/skyflow/generated/rest/types/token_type_mapping_default.py similarity index 100% rename from skyflow/generated/rest/types/token_type_mapping_default.py rename to v2/skyflow/generated/rest/types/token_type_mapping_default.py diff --git a/skyflow/generated/rest/types/token_type_mapping_entity_only_item.py b/v2/skyflow/generated/rest/types/token_type_mapping_entity_only_item.py similarity index 100% rename from skyflow/generated/rest/types/token_type_mapping_entity_only_item.py rename to v2/skyflow/generated/rest/types/token_type_mapping_entity_only_item.py diff --git a/skyflow/generated/rest/types/token_type_mapping_entity_unq_counter_item.py b/v2/skyflow/generated/rest/types/token_type_mapping_entity_unq_counter_item.py similarity index 100% rename from skyflow/generated/rest/types/token_type_mapping_entity_unq_counter_item.py rename to v2/skyflow/generated/rest/types/token_type_mapping_entity_unq_counter_item.py diff --git a/skyflow/generated/rest/types/token_type_mapping_vault_token_item.py b/v2/skyflow/generated/rest/types/token_type_mapping_vault_token_item.py similarity index 100% rename from skyflow/generated/rest/types/token_type_mapping_vault_token_item.py rename to v2/skyflow/generated/rest/types/token_type_mapping_vault_token_item.py diff --git a/skyflow/generated/rest/types/transformations.py b/v2/skyflow/generated/rest/types/transformations.py similarity index 100% rename from skyflow/generated/rest/types/transformations.py rename to v2/skyflow/generated/rest/types/transformations.py diff --git a/skyflow/generated/rest/types/upload_file_v_2_response.py b/v2/skyflow/generated/rest/types/upload_file_v_2_response.py similarity index 100% rename from skyflow/generated/rest/types/upload_file_v_2_response.py rename to v2/skyflow/generated/rest/types/upload_file_v_2_response.py diff --git a/skyflow/generated/rest/types/uuid_.py b/v2/skyflow/generated/rest/types/uuid_.py similarity index 100% rename from skyflow/generated/rest/types/uuid_.py rename to v2/skyflow/generated/rest/types/uuid_.py diff --git a/skyflow/generated/rest/types/v_1_audit_after_options.py b/v2/skyflow/generated/rest/types/v_1_audit_after_options.py similarity index 100% rename from skyflow/generated/rest/types/v_1_audit_after_options.py rename to v2/skyflow/generated/rest/types/v_1_audit_after_options.py diff --git a/skyflow/generated/rest/types/v_1_audit_event_response.py b/v2/skyflow/generated/rest/types/v_1_audit_event_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_audit_event_response.py rename to v2/skyflow/generated/rest/types/v_1_audit_event_response.py diff --git a/skyflow/generated/rest/types/v_1_audit_response.py b/v2/skyflow/generated/rest/types/v_1_audit_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_audit_response.py rename to v2/skyflow/generated/rest/types/v_1_audit_response.py diff --git a/skyflow/generated/rest/types/v_1_audit_response_event.py b/v2/skyflow/generated/rest/types/v_1_audit_response_event.py similarity index 100% rename from skyflow/generated/rest/types/v_1_audit_response_event.py rename to v2/skyflow/generated/rest/types/v_1_audit_response_event.py diff --git a/skyflow/generated/rest/types/v_1_audit_response_event_request.py b/v2/skyflow/generated/rest/types/v_1_audit_response_event_request.py similarity index 100% rename from skyflow/generated/rest/types/v_1_audit_response_event_request.py rename to v2/skyflow/generated/rest/types/v_1_audit_response_event_request.py diff --git a/skyflow/generated/rest/types/v_1_batch_operation_response.py b/v2/skyflow/generated/rest/types/v_1_batch_operation_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_batch_operation_response.py rename to v2/skyflow/generated/rest/types/v_1_batch_operation_response.py diff --git a/skyflow/generated/rest/types/v_1_batch_record.py b/v2/skyflow/generated/rest/types/v_1_batch_record.py similarity index 100% rename from skyflow/generated/rest/types/v_1_batch_record.py rename to v2/skyflow/generated/rest/types/v_1_batch_record.py diff --git a/skyflow/generated/rest/types/v_1_bin_list_response.py b/v2/skyflow/generated/rest/types/v_1_bin_list_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_bin_list_response.py rename to v2/skyflow/generated/rest/types/v_1_bin_list_response.py diff --git a/skyflow/generated/rest/types/v_1_bulk_delete_record_response.py b/v2/skyflow/generated/rest/types/v_1_bulk_delete_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_bulk_delete_record_response.py rename to v2/skyflow/generated/rest/types/v_1_bulk_delete_record_response.py diff --git a/skyflow/generated/rest/types/v_1_bulk_get_record_response.py b/v2/skyflow/generated/rest/types/v_1_bulk_get_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_bulk_get_record_response.py rename to v2/skyflow/generated/rest/types/v_1_bulk_get_record_response.py diff --git a/skyflow/generated/rest/types/v_1_byot.py b/v2/skyflow/generated/rest/types/v_1_byot.py similarity index 100% rename from skyflow/generated/rest/types/v_1_byot.py rename to v2/skyflow/generated/rest/types/v_1_byot.py diff --git a/skyflow/generated/rest/types/v_1_card.py b/v2/skyflow/generated/rest/types/v_1_card.py similarity index 100% rename from skyflow/generated/rest/types/v_1_card.py rename to v2/skyflow/generated/rest/types/v_1_card.py diff --git a/skyflow/generated/rest/types/v_1_delete_file_response.py b/v2/skyflow/generated/rest/types/v_1_delete_file_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_delete_file_response.py rename to v2/skyflow/generated/rest/types/v_1_delete_file_response.py diff --git a/skyflow/generated/rest/types/v_1_delete_record_response.py b/v2/skyflow/generated/rest/types/v_1_delete_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_delete_record_response.py rename to v2/skyflow/generated/rest/types/v_1_delete_record_response.py diff --git a/skyflow/generated/rest/types/v_1_detokenize_record_request.py b/v2/skyflow/generated/rest/types/v_1_detokenize_record_request.py similarity index 100% rename from skyflow/generated/rest/types/v_1_detokenize_record_request.py rename to v2/skyflow/generated/rest/types/v_1_detokenize_record_request.py diff --git a/skyflow/generated/rest/types/v_1_detokenize_record_response.py b/v2/skyflow/generated/rest/types/v_1_detokenize_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_detokenize_record_response.py rename to v2/skyflow/generated/rest/types/v_1_detokenize_record_response.py diff --git a/skyflow/generated/rest/types/v_1_detokenize_response.py b/v2/skyflow/generated/rest/types/v_1_detokenize_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_detokenize_response.py rename to v2/skyflow/generated/rest/types/v_1_detokenize_response.py diff --git a/skyflow/generated/rest/types/v_1_field_records.py b/v2/skyflow/generated/rest/types/v_1_field_records.py similarity index 100% rename from skyflow/generated/rest/types/v_1_field_records.py rename to v2/skyflow/generated/rest/types/v_1_field_records.py diff --git a/skyflow/generated/rest/types/v_1_file_av_scan_status.py b/v2/skyflow/generated/rest/types/v_1_file_av_scan_status.py similarity index 100% rename from skyflow/generated/rest/types/v_1_file_av_scan_status.py rename to v2/skyflow/generated/rest/types/v_1_file_av_scan_status.py diff --git a/v2/skyflow/generated/rest/types/v_1_get_auth_token_response.py b/v2/skyflow/generated/rest/types/v_1_get_auth_token_response.py new file mode 100644 index 00000000..c4db65a0 --- /dev/null +++ b/v2/skyflow/generated/rest/types/v_1_get_auth_token_response.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from ..core.serialization import FieldMetadata + + +class V1GetAuthTokenResponse(UniversalBaseModel): + access_token: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="accessToken")] = ( + pydantic.Field(default=None) + ) + """ + AccessToken. + """ + + token_type: typing_extensions.Annotated[typing.Optional[str], FieldMetadata(alias="tokenType")] = pydantic.Field( + default=None + ) + """ + TokenType : Bearer. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/skyflow/generated/rest/types/v_1_get_file_scan_status_response.py b/v2/skyflow/generated/rest/types/v_1_get_file_scan_status_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_get_file_scan_status_response.py rename to v2/skyflow/generated/rest/types/v_1_get_file_scan_status_response.py diff --git a/skyflow/generated/rest/types/v_1_get_query_response.py b/v2/skyflow/generated/rest/types/v_1_get_query_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_get_query_response.py rename to v2/skyflow/generated/rest/types/v_1_get_query_response.py diff --git a/skyflow/generated/rest/types/v_1_insert_record_response.py b/v2/skyflow/generated/rest/types/v_1_insert_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_insert_record_response.py rename to v2/skyflow/generated/rest/types/v_1_insert_record_response.py diff --git a/skyflow/generated/rest/types/v_1_member_type.py b/v2/skyflow/generated/rest/types/v_1_member_type.py similarity index 100% rename from skyflow/generated/rest/types/v_1_member_type.py rename to v2/skyflow/generated/rest/types/v_1_member_type.py diff --git a/skyflow/generated/rest/types/v_1_record_meta_properties.py b/v2/skyflow/generated/rest/types/v_1_record_meta_properties.py similarity index 100% rename from skyflow/generated/rest/types/v_1_record_meta_properties.py rename to v2/skyflow/generated/rest/types/v_1_record_meta_properties.py diff --git a/skyflow/generated/rest/types/v_1_tokenize_record_request.py b/v2/skyflow/generated/rest/types/v_1_tokenize_record_request.py similarity index 100% rename from skyflow/generated/rest/types/v_1_tokenize_record_request.py rename to v2/skyflow/generated/rest/types/v_1_tokenize_record_request.py diff --git a/skyflow/generated/rest/types/v_1_tokenize_record_response.py b/v2/skyflow/generated/rest/types/v_1_tokenize_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_tokenize_record_response.py rename to v2/skyflow/generated/rest/types/v_1_tokenize_record_response.py diff --git a/skyflow/generated/rest/types/v_1_tokenize_response.py b/v2/skyflow/generated/rest/types/v_1_tokenize_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_tokenize_response.py rename to v2/skyflow/generated/rest/types/v_1_tokenize_response.py diff --git a/skyflow/generated/rest/types/v_1_update_record_response.py b/v2/skyflow/generated/rest/types/v_1_update_record_response.py similarity index 100% rename from skyflow/generated/rest/types/v_1_update_record_response.py rename to v2/skyflow/generated/rest/types/v_1_update_record_response.py diff --git a/skyflow/generated/rest/types/v_1_vault_field_mapping.py b/v2/skyflow/generated/rest/types/v_1_vault_field_mapping.py similarity index 100% rename from skyflow/generated/rest/types/v_1_vault_field_mapping.py rename to v2/skyflow/generated/rest/types/v_1_vault_field_mapping.py diff --git a/skyflow/generated/rest/types/v_1_vault_schema_config.py b/v2/skyflow/generated/rest/types/v_1_vault_schema_config.py similarity index 100% rename from skyflow/generated/rest/types/v_1_vault_schema_config.py rename to v2/skyflow/generated/rest/types/v_1_vault_schema_config.py diff --git a/skyflow/generated/rest/types/word_character_count.py b/v2/skyflow/generated/rest/types/word_character_count.py similarity index 100% rename from skyflow/generated/rest/types/word_character_count.py rename to v2/skyflow/generated/rest/types/word_character_count.py diff --git a/skyflow/generated/rest/version.py b/v2/skyflow/generated/rest/version.py similarity index 100% rename from skyflow/generated/rest/version.py rename to v2/skyflow/generated/rest/version.py diff --git a/v2/skyflow/py.typed b/v2/skyflow/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/skyflow/service_account/__init__.py b/v2/skyflow/service_account/__init__.py similarity index 100% rename from skyflow/service_account/__init__.py rename to v2/skyflow/service_account/__init__.py diff --git a/skyflow/service_account/_utils.py b/v2/skyflow/service_account/_utils.py similarity index 100% rename from skyflow/service_account/_utils.py rename to v2/skyflow/service_account/_utils.py diff --git a/v2/skyflow/service_account/client/__init__.py b/v2/skyflow/service_account/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/skyflow/service_account/client/auth_client.py b/v2/skyflow/service_account/client/auth_client.py similarity index 100% rename from skyflow/service_account/client/auth_client.py rename to v2/skyflow/service_account/client/auth_client.py diff --git a/skyflow/utils/__init__.py b/v2/skyflow/utils/__init__.py similarity index 100% rename from skyflow/utils/__init__.py rename to v2/skyflow/utils/__init__.py diff --git a/v2/skyflow/utils/_helpers.py b/v2/skyflow/utils/_helpers.py new file mode 100644 index 00000000..12ff1257 --- /dev/null +++ b/v2/skyflow/utils/_helpers.py @@ -0,0 +1,18 @@ +from urllib.parse import urlparse + +def get_base_url(url): + parsed_url = urlparse(url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}" + return base_url + +def format_scope(scopes): + if not scopes: + return None + return " ".join([f"role:{scope}" for scope in scopes]) + +def is_valid_url(url): + try: + result = urlparse(url) + return all([result.scheme == "https", result.netloc]) + except Exception: + return False \ No newline at end of file diff --git a/skyflow/utils/_skyflow_messages.py b/v2/skyflow/utils/_skyflow_messages.py similarity index 99% rename from skyflow/utils/_skyflow_messages.py rename to v2/skyflow/utils/_skyflow_messages.py index 232bd8b0..9149e1d4 100644 --- a/skyflow/utils/_skyflow_messages.py +++ b/v2/skyflow/utils/_skyflow_messages.py @@ -91,6 +91,9 @@ class Error(Enum): INVALID_TABLE_NAME_IN_INSERT = f"{error_prefix} Validation error. Invalid table name in insert request. Specify a valid table name." INVALID_TYPE_OF_DATA_IN_INSERT = f"{error_prefix} Validation error. Invalid type of data in insert request. Specify data as a object array." EMPTY_DATA_IN_INSERT = f"{error_prefix} Validation error. Data array cannot be empty. Specify data in insert request." + INVALID_RECORD_DATA_IN_INSERT = f"{error_prefix} Validation error. Each record's field values must be a non-empty dict." + EMPTY_KEY_IN_INSERT_DATA = f"{error_prefix} Validation error. A record must not contain a null or empty key." + EMPTY_VALUE_IN_INSERT_DATA = f"{error_prefix} Validation error. A record must not contain a null or empty value." INVALID_UPSERT_OPTIONS_TYPE = f"{error_prefix} Validation error. Invalid 'upsert' value in options. Specify 'upsert' as a non-empty string containing the column name." INVALID_HOMOGENEOUS_TYPE = f"{error_prefix} Validation error. Invalid type of homogeneous. Specify homogeneous as a string." INVALID_TOKEN_MODE_TYPE = f"{error_prefix} Validation error. Invalid type of token mode. Specify token mode as a TokenMode enum." diff --git a/skyflow/utils/_utils.py b/v2/skyflow/utils/_utils.py similarity index 100% rename from skyflow/utils/_utils.py rename to v2/skyflow/utils/_utils.py diff --git a/skyflow/utils/_version.py b/v2/skyflow/utils/_version.py similarity index 100% rename from skyflow/utils/_version.py rename to v2/skyflow/utils/_version.py diff --git a/v2/skyflow/utils/constants.py b/v2/skyflow/utils/constants.py new file mode 100644 index 00000000..05d28380 --- /dev/null +++ b/v2/skyflow/utils/constants.py @@ -0,0 +1,291 @@ +OPTIONAL_TOKEN='token' +PROTOCOL='https' +SKY_META_DATA_HEADER='sky-metadata' +CTX_KEY_REGEX=r'^[a-zA-Z0-9_]+$' + +class SKYFLOW: + SKYFLOW_ID = 'skyflowId' + X_SKYFLOW_AUTHORIZATION = 'x-skyflow-authorization' + + +class HttpHeader: + CONTENT_TYPE = 'Content-Type' + CONTENT_TYPE_LOWERCASE = 'content-type' + X_REQUEST_ID = 'x-request-id' + ERROR_FROM_CLIENT = 'error-from-client' + AUTHORIZATION = 'Authorization' + X_SKYFLOW_AUTHORIZATION_HEADER = 'X-Skyflow-Authorization' + + +class HttpStatusCode: + OK = 200 + BAD_REQUEST = 400 + UNAUTHORIZED = 401 + INTERNAL_SERVER_ERROR = 500 + + +class ContentType: + APPLICATION_JSON = 'application/json' + APPLICATION_X_WWW_FORM_URLENCODED = 'application/x-www-form-urlencoded' + TEXT_PLAIN = 'text/plain' + + +class DetectStatus: + IN_PROGRESS = 'IN_PROGRESS' + SUCCESS = 'SUCCESS' + FAILED = 'FAILED' + UNKNOWN = 'UNKNOWN' + +class Detect: + WAIT_TIME = 64 + +class FileExtension: + JSON = 'json' + MP3 = 'mp3' + WAV = 'wav' + PDF = 'pdf' + TXT = 'txt' + DOC = 'doc' + DOCX = 'docx' + JPG = 'jpg' + JPEG = 'jpeg' + PNG = 'png' + BMP = 'bmp' + TIF = 'tif' + TIFF = 'tiff' + PPT = 'ppt' + PPTX = 'pptx' + CSV = 'csv' + XLS = 'xls' + XLSX = 'xlsx' + XML = 'xml' + + +class FileProcessing: + PROCESSED_PREFIX = 'processed-' + DEIDENTIFIED_PREFIX = 'deidentified.' + ENTITIES = 'entities' + + +class EncodingType: + UTF8 = 'utf8' + UTF_8 = 'utf-8' + BASE64 = 'base64' + BINARY = 'binary' + + +class JWT: + ALGORITHM_RS256 = 'RS256' + GRANT_TYPE_JWT_BEARER = 'urn:ietf:params:oauth:grant-type:jwt-bearer' + ISSUER_SDK = 'sdk' + SIGNED_TOKEN_PREFIX = 'signed_token_' + ROLE_PREFIX = 'role:' + + +class ApiKey: + SKY_PREFIX = 'sky-' + LENGTH = 42 + + +class UrlProtocol: + HTTPS = 'https' + HTTP = 'http' + + +class BooleanString: + TRUE = 'true' + FALSE = 'false' + + +class ResponseField: + STATUS = 'Status' + BODY = 'Body' + RECORDS = 'records' + TOKENS = 'tokens' + ERROR = 'error' + SKYFLOW_ID = 'skyflow_id' + REQUEST_INDEX = 'request_index' + REQUEST_ID = 'request_id' + HTTP_CODE = 'http_code' + HTTP_STATUS = 'http_status' + GRPC_CODE = 'grpc_code' + DETAILS = 'details' + MESSAGE = 'message' + ERROR_FROM_CLIENT = 'error_from_client' + TOKEN = 'token' + VALUE = 'value' + TYPE = 'type' + TOKENIZED_DATA = 'tokenized_data' + SIGNED_TOKEN = 'signed_token' + RESPONSES = 'responses' + + +class CredentialField: + PRIVATE_KEY = 'privateKey' + CLIENT_ID = 'clientID' + KEY_ID = 'keyID' + TOKEN_URI = 'tokenURI' + TOKEN_URI_OPTION = 'token_uri' + CLIENT_NAME = 'clientName' + CREDENTIALS_STRING = 'credentials_string' + API_KEY = 'api_key' + TOKEN = 'token' + PATH = 'path' + CONTEXT = 'context' + ROLES = 'roles' + + +class JwtField: + ISS = 'iss' + KEY = 'key' + AUD = 'aud' + SUB = 'sub' + EXP = 'exp' + CTX = 'ctx' + TOK = 'tok' + IAT = 'iat' + + +class OptionField: + ROLE_IDS = 'role_ids' + DATA_TOKENS = 'data_tokens' + TIME_TO_LIVE = 'time_to_live' + ROLES = 'roles' + CTX = 'ctx' + VAULT_ID = 'vault_id' + CONNECTION_ID = 'connection_id' + CONNECTION_URL = 'connection_url' + VAULT_CLIENT = 'vault_client' + VAULT_CONTROLLER = 'vault_controller' + DETECT_CONTROLLER = 'detect_controller' + CONTROLLER = 'controller' + VERIFY_SIGNATURE = 'verify_signature' + VERIFY_AUD = 'verify_aud' + + +class ConfigField: + CREDENTIALS = 'credentials' + CLUSTER_ID = 'cluster_id' + ENV = 'env' + VAULT_ID = 'vault_id' + + +class RequestParameter: + VALUE = 'value' + COLUMN_GROUP = 'column_group' + REDACTION = 'redaction' + REDACTION_TYPE = 'redaction_type' + + +class FileUploadField: + TABLE = 'table' + SKYFLOW_ID = 'skyflow_id' + COLUMN_NAME = 'column_name' + FILE_PATH = 'file_path' + BASE64 = 'base64' + FILE_OBJECT = 'file_object' + FILE_NAME = 'file_name' + FILE = 'file' + NAME = 'name' + + +class DeidentifyFileRequestField: + ENTITIES = 'entities' + ALLOW_REGEX_LIST = 'allow_regex_list' + RESTRICT_REGEX_LIST = 'restrict_regex_list' + OUTPUT_PROCESSED_IMAGE = 'output_processed_image' + OUTPUT_OCR_TEXT = 'output_ocr_text' + MASKING_METHOD = 'masking_method' + PIXEL_DENSITY = 'pixel_density' + DENSITY = 'density' + MAX_RESOLUTION = 'max_resolution' + OUTPUT_PROCESSED_AUDIO = 'output_processed_audio' + OUTPUT_TRANSCRIPTION = 'output_transcription' + BLEEP = 'bleep' + OUTPUT_DIRECTORY = 'output_directory' + WAIT_TIME = 'wait_time' + + +class DeidentifyField: + TEXT = 'text' + ENTITY_TYPES = 'entity_types' + TOKEN_TYPE = 'token_type' + ALLOW_REGEX = 'allow_regex' + RESTRICT_REGEX = 'restrict_regex' + TRANSFORMATIONS = 'transformations' + FORMAT = 'format' + OUTPUT = 'output' + STATUS = 'status' + RUN_ID = 'run_id' + WORD_CHARACTER_COUNT = 'word_character_count' + WORD_COUNT = 'word_count' + CHARACTER_COUNT = 'character_count' + SIZE = 'size' + DURATION = 'duration' + PAGES = 'pages' + SLIDES = 'slides' + PROCESSED_FILE = 'processed_file' + PROCESSED_FILE_TYPE = 'processed_file_type' + PROCESSED_FILE_EXTENSION = 'processed_file_extension' + REDACTED_FILE = 'redacted_file' + SHIFT_DATES = 'shift_dates' + DEFAULT = 'default' + ENTITY_UNQ_COUNTER = 'entity_unq_counter' + ENTITY_UNIQUE_COUNTER = 'entity_unique_counter' + ENTITY_ONLY = 'entity_only' + VAULT_TOKEN = 'vault_token' + ENTITIES = 'entities' + MAX_DAYS = 'max_days' + MIN_DAYS = 'min_days' + MAX = 'max' + MIN = 'min' + FILE = 'file' + TYPE = 'type' + EXTENSION = 'extension' + IN_PROGRESS = 'IN_PROGRESS' + REQUEST_OPTIONS = 'request_options' + BLEEP_GAIN = 'bleep_gain' + BLEEP_FREQUENCY = 'bleep_frequency' + BLEEP_START_PADDING = 'bleep_start_padding' + BLEEP_STOP_PADDING = 'bleep_stop_padding' + DENSITY = 'density' + TOKEN_FORMAT = 'token_format' + PROCESSED_FILE_RESPONSE_KEY = 'processedFile' + PROCESSED_FILE_TYPE_RESPONSE_KEY = 'processedFileType' + PROCESSED_FILE_EXTENSION_RESPONSE_KEY = 'processedFileExtension' + + +class RequestOperation: + INSERT = 'INSERT' + DELETE = 'DELETE' + GET = 'GET' + UPDATE = 'UPDATE' + QUERY = 'QUERY' + TOKENIZE = 'TOKENIZE' + DETOKENIZE = 'DETOKENIZE' + FILE_UPLOAD = 'FILE_UPLOAD' + + +class ConfigType: + VAULT = 'vault' + CONNECTION = 'connection' + + +class SqlCommand: + SELECT = 'SELECT' + + +class SdkPrefix: + SKYFLOW_PYTHON = 'skyflow-python@' + PYTHON_RUNTIME = 'Python ' + + +class SdkMetricsKey: + SDK_NAME_VERSION = 'sdk_name_version' + SDK_CLIENT_DEVICE_MODEL = 'sdk_client_device_model' + SDK_CLIENT_OS_DETAILS = 'sdk_client_os_details' + SDK_RUNTIME_DETAILS = 'sdk_runtime_details' + + +class ErrorDefaults: + UNKNOWN_REQUEST_ID = 'unknown-request-id' diff --git a/v2/skyflow/utils/enums/__init__.py b/v2/skyflow/utils/enums/__init__.py new file mode 100644 index 00000000..af293ce2 --- /dev/null +++ b/v2/skyflow/utils/enums/__init__.py @@ -0,0 +1,12 @@ +from .env import Env, EnvUrls +from .log_level import LogLevel +from .content_types import ContentType +from .detect_entities import DetectEntities +from .token_mode import TokenMode +from .token_type import TokenType +from .request_method import RequestMethod +from .redaction_type import RedactionType +from .detect_entities import DetectEntities +from .detect_output_transcriptions import DetectOutputTranscriptions +from .masking_method import MaskingMethod +from .token_type import TokenType \ No newline at end of file diff --git a/v2/skyflow/utils/enums/content_types.py b/v2/skyflow/utils/enums/content_types.py new file mode 100644 index 00000000..f2db5b92 --- /dev/null +++ b/v2/skyflow/utils/enums/content_types.py @@ -0,0 +1,9 @@ +from enum import Enum + +class ContentType(Enum): + JSON = 'application/json' + PLAINTEXT = 'text/plain' + XML = 'text/xml' + URLENCODED = 'application/x-www-form-urlencoded' + FORMDATA = 'multipart/form-data' + HTML = 'text/html' \ No newline at end of file diff --git a/v2/skyflow/utils/enums/detect_entities.py b/v2/skyflow/utils/enums/detect_entities.py new file mode 100644 index 00000000..91d5e1a2 --- /dev/null +++ b/v2/skyflow/utils/enums/detect_entities.py @@ -0,0 +1,73 @@ +from enum import Enum + +class DetectEntities(Enum): + ACCOUNT_NUMBER = "account_number" + AGE = "age" + ALL = "all" + BANK_ACCOUNT = "bank_account" + BLOOD_TYPE = "blood_type" + CONDITION = "condition" + CORPORATE_ACTION = "corporate_action" + CREDIT_CARD = "credit_card" + CREDIT_CARD_EXPIRATION = "credit_card_expiration" + CVV = "cvv" + DATE = "date" + DAY = "day" + DATE_INTERVAL = "date_interval" + DOB = "dob" + DOSE = "dose" + DRIVER_LICENSE = "driver_license" + DRUG = "drug" + DURATION = "duration" + EFFECT = "effect" + EMAIL_ADDRESS = "email_address" + EVENT = "event" + FILENAME = "filename" + FINANCIAL_METRIC = "financial_metric" + GENDER = "gender" + HEALTHCARE_NUMBER = "healthcare_number" + INJURY = "injury" + IP_ADDRESS = "ip_address" + LANGUAGE = "language" + LOCATION = "location" + LOCATION_ADDRESS = "location_address" + LOCATION_ADDRESS_STREET = "location_address_street" + LOCATION_CITY = "location_city" + LOCATION_COORDINATE = "location_coordinate" + LOCATION_COUNTRY = "location_country" + LOCATION_STATE = "location_state" + LOCATION_ZIP = "location_zip" + MARITAL_STATUS = "marital_status" + MEDICAL_CODE = "medical_code" + MEDICAL_PROCESS = "medical_process" + MONEY = "money" + MONTH = "month" + NAME = "name" + NAME_FAMILY = "name_family" + NAME_GIVEN = "name_given" + NAME_MEDICAL_PROFESSIONAL = "name_medical_professional" + NUMERICAL_PII = "numerical_pii" + OCCUPATION = "occupation" + ORGANIZATION = "organization" + ORGANIZATION_ID = "organization_id" + ORGANIZATION_MEDICAL_FACILITY = "organization_medical_facility" + ORIGIN = "origin" + PASSPORT_NUMBER = "passport_number" + PASSWORD = "password" + PHONE_NUMBER = "phone_number" + PROJECT = "project" + PHYSICAL_ATTRIBUTE = "physical_attribute" + POLITICAL_AFFILIATION = "political_affiliation" + PRODUCT = "product" + RELIGION = "religion" + ROUTING_NUMBER = "routing_number" + SEXUALITY = "sexuality" + SSN = "ssn" + STATISTICS = "statistics" + TIME = "time" + TREND = "trend" + URL = "url" + USERNAME = "username" + VEHICLE_ID = "vehicle_id" + YEAR = "year" + ZODIAC_SIGN = "zodiac_sign" \ No newline at end of file diff --git a/v2/skyflow/utils/enums/detect_output_transcriptions.py b/v2/skyflow/utils/enums/detect_output_transcriptions.py new file mode 100644 index 00000000..a398a3d8 --- /dev/null +++ b/v2/skyflow/utils/enums/detect_output_transcriptions.py @@ -0,0 +1,8 @@ +from enum import Enum + +class DetectOutputTranscriptions(Enum): + DIARIZED_TRANSCRIPTION = "diarized_transcription" + MEDICAL_DIARIZED_TRANSCRIPTION = "medical_diarized_transcription" + MEDICAL_TRANSCRIPTION = "medical_transcription" + TRANSCRIPTION = "transcription" + PLAINTEXT_TRANSCRIPTION = "plaintext_transcription" \ No newline at end of file diff --git a/v2/skyflow/utils/enums/env.py b/v2/skyflow/utils/enums/env.py new file mode 100644 index 00000000..512812b8 --- /dev/null +++ b/v2/skyflow/utils/enums/env.py @@ -0,0 +1,16 @@ +# Re-exports common's Env/EnvUrls rather than defining a separate duplicate class. +# +# Why this matters (found via a live run, not caught by tests): VaultClient.initialize_client_configuration() +# is inherited from common.vault.base_vault_client.BaseVaultClient, which calls common.utils.get_vault_url(). +# That function validates its `env` argument with `if env not in Env` against *common's* Env class. If this +# module defined its own separate (even if identically-shaped) Env class, a value built from `skyflow.Env` +# would never satisfy that check -- plain Enum classes never compare equal across distinct class objects, +# even with matching member names/values. Re-exporting the same class object avoids that entirely. +# +# This is safe for v2 compatibility: values, names, and `.value`/`.name` behavior are unchanged -- +# `skyflow.Env.PROD` still is `Env.PROD` with the same string value. Only the class's identity is now +# shared instead of duplicated, which is exactly what a config value needs to survive a round trip through +# shared validation code in common/. +from common.utils.enums import Env, EnvUrls + +__all__ = ["Env", "EnvUrls"] diff --git a/v2/skyflow/utils/enums/log_level.py b/v2/skyflow/utils/enums/log_level.py new file mode 100644 index 00000000..42aa1dfe --- /dev/null +++ b/v2/skyflow/utils/enums/log_level.py @@ -0,0 +1 @@ +from common.utils.enums.log_level import LogLevel diff --git a/v2/skyflow/utils/enums/masking_method.py b/v2/skyflow/utils/enums/masking_method.py new file mode 100644 index 00000000..a322f35f --- /dev/null +++ b/v2/skyflow/utils/enums/masking_method.py @@ -0,0 +1,5 @@ +from enum import Enum + +class MaskingMethod(Enum): + BLACKBOX= "blackbox" + BLUR= "blur" \ No newline at end of file diff --git a/v2/skyflow/utils/enums/redaction_type.py b/v2/skyflow/utils/enums/redaction_type.py new file mode 100644 index 00000000..1780e820 --- /dev/null +++ b/v2/skyflow/utils/enums/redaction_type.py @@ -0,0 +1,7 @@ +from enum import Enum + +class RedactionType(Enum): + PLAIN_TEXT = 'PLAIN_TEXT' + MASKED = 'MASKED' + DEFAULT = 'DEFAULT' + REDACTED = 'REDACTED' diff --git a/v2/skyflow/utils/enums/request_method.py b/v2/skyflow/utils/enums/request_method.py new file mode 100644 index 00000000..61efef3d --- /dev/null +++ b/v2/skyflow/utils/enums/request_method.py @@ -0,0 +1,8 @@ +from enum import Enum + +class RequestMethod(Enum): + GET = "GET" + POST = "POST" + PUT = "PUT" + DELETE = "DELETE" + NONE = "NONE" \ No newline at end of file diff --git a/v2/skyflow/utils/enums/token_mode.py b/v2/skyflow/utils/enums/token_mode.py new file mode 100644 index 00000000..a073b125 --- /dev/null +++ b/v2/skyflow/utils/enums/token_mode.py @@ -0,0 +1,6 @@ +from enum import Enum + +class TokenMode(Enum): + DISABLE = "DISABLE" + ENABLE = "ENABLE" + ENABLE_STRICT = "ENABLE_STRICT" \ No newline at end of file diff --git a/v2/skyflow/utils/enums/token_type.py b/v2/skyflow/utils/enums/token_type.py new file mode 100644 index 00000000..9e9e5fcf --- /dev/null +++ b/v2/skyflow/utils/enums/token_type.py @@ -0,0 +1,6 @@ +from enum import Enum + +class TokenType(Enum): + VAULT_TOKEN = "vault_token" + ENTITY_UNIQUE_COUNTER = "entity_unq_counter" + ENTITY_ONLY = "entity_only" diff --git a/v2/skyflow/utils/logger/__init__.py b/v2/skyflow/utils/logger/__init__.py new file mode 100644 index 00000000..bce55608 --- /dev/null +++ b/v2/skyflow/utils/logger/__init__.py @@ -0,0 +1,2 @@ +from ._logger import Logger +from ._log_helpers import log_error, log_info, log_warn, log_error_log, set_active_log_level \ No newline at end of file diff --git a/v2/skyflow/utils/logger/_log_helpers.py b/v2/skyflow/utils/logger/_log_helpers.py new file mode 100644 index 00000000..4fdff077 --- /dev/null +++ b/v2/skyflow/utils/logger/_log_helpers.py @@ -0,0 +1 @@ +from common.utils.logger._log_helpers import log_error, log_info, log_warn, log_error_log, set_active_log_level diff --git a/v2/skyflow/utils/logger/_logger.py b/v2/skyflow/utils/logger/_logger.py new file mode 100644 index 00000000..423df426 --- /dev/null +++ b/v2/skyflow/utils/logger/_logger.py @@ -0,0 +1 @@ +from common.utils.logger._logger import Logger diff --git a/skyflow/utils/validations/__init__.py b/v2/skyflow/utils/validations/__init__.py similarity index 100% rename from skyflow/utils/validations/__init__.py rename to v2/skyflow/utils/validations/__init__.py diff --git a/skyflow/utils/validations/_validations.py b/v2/skyflow/utils/validations/_validations.py similarity index 82% rename from skyflow/utils/validations/_validations.py rename to v2/skyflow/utils/validations/_validations.py index 42abe188..954b897b 100644 --- a/skyflow/utils/validations/_validations.py +++ b/v2/skyflow/utils/validations/_validations.py @@ -1,8 +1,14 @@ import base64 import json import os +from common.utils.validations import ( + validate_credentials as _common_validate_credentials, + validate_log_level as _common_validate_log_level, + validate_vault_config as _common_validate_vault_config, + validate_update_vault_config as _common_validate_update_vault_config, +) from skyflow.service_account import is_expired -from skyflow.utils.enums import LogLevel, Env, RedactionType, TokenMode, DetectEntities, DetectOutputTranscriptions, \ +from skyflow.utils.enums import RedactionType, TokenMode, DetectEntities, DetectOutputTranscriptions, \ MaskingMethod from skyflow.error import SkyflowError from skyflow.utils import SkyflowMessages @@ -17,12 +23,6 @@ from skyflow.vault.detect._file_input import FileInput from skyflow.utils._helpers import is_valid_url -valid_vault_config_keys = [ - ConfigField.VAULT_ID, - ConfigField.CLUSTER_ID, - ConfigField.CREDENTIALS, - ConfigField.ENV -] valid_connection_config_keys = [ OptionField.CONNECTION_ID, OptionField.CONNECTION_URL, @@ -82,101 +82,14 @@ def validate_api_key(api_key: str, logger = None) -> bool: return True def validate_credentials(logger, credentials, config_id_type=None, config_id=None): - key_present = [k for k in [CredentialField.PATH, CredentialField.TOKEN, CredentialField.CREDENTIALS_STRING, CredentialField.API_KEY] if credentials.get(k)] - - if len(key_present) == 0: - error_message = ( - SkyflowMessages.Error.INVALID_CREDENTIALS_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else - SkyflowMessages.Error.INVALID_CREDENTIALS.value - ) - log_error_log(error_message, logger) - raise SkyflowError(error_message, invalid_input_error_code) - elif len(key_present) > 1: - error_message = ( - SkyflowMessages.Error.MULTIPLE_CREDENTIALS_PASSED_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else - SkyflowMessages.Error.MULTIPLE_CREDENTIALS_PASSED.value - ) - log_error_log(error_message, logger) - raise SkyflowError(error_message, invalid_input_error_code) - - if CredentialField.ROLES in credentials: - validate_required_field( - logger, credentials, CredentialField.ROLES, list, - SkyflowMessages.Error.INVALID_ROLES_KEY_TYPE_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_ROLES_KEY_TYPE.value, - SkyflowMessages.Error.EMPTY_ROLES_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.EMPTY_ROLES.value - ) - - if CredentialField.CONTEXT in credentials: - validate_required_field( - logger, credentials, CredentialField.CONTEXT, str, - SkyflowMessages.Error.EMPTY_CONTEXT_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.EMPTY_CONTEXT.value, - SkyflowMessages.Error.INVALID_CONTEXT_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_CONTEXT.value - ) - - if CredentialField.CREDENTIALS_STRING in credentials: - validate_required_field( - logger, credentials, CredentialField.CREDENTIALS_STRING, str, - SkyflowMessages.Error.EMPTY_CREDENTIALS_STRING_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.EMPTY_CREDENTIALS_STRING.value, - SkyflowMessages.Error.INVALID_CREDENTIALS_STRING_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_CREDENTIALS_STRING.value - ) - elif CredentialField.PATH in credentials: - validate_required_field( - logger, credentials, CredentialField.PATH, str, - SkyflowMessages.Error.EMPTY_CREDENTIAL_FILE_PATH_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.EMPTY_CREDENTIAL_FILE_PATH.value, - SkyflowMessages.Error.INVALID_CREDENTIAL_FILE_PATH_IN_CONFIG.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_CREDENTIAL_FILE_PATH.value - ) - elif CredentialField.TOKEN in credentials: - validate_required_field( - logger, credentials, CredentialField.TOKEN, str, - SkyflowMessages.Error.EMPTY_CREDENTIALS_TOKEN.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.EMPTY_CREDENTIALS_TOKEN.value, - SkyflowMessages.Error.INVALID_CREDENTIALS_TOKEN.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_CREDENTIALS_TOKEN.value - ) - if is_expired(credentials.get(CredentialField.TOKEN), logger): - log_error_log(SkyflowMessages.ErrorLogs.INVALID_BEARER_TOKEN.value, logger) - raise SkyflowError( - SkyflowMessages.Error.EXPIRED_BEARER_TOKEN.value - if config_id_type and config_id else SkyflowMessages.Error.EXPIRED_BEARER_TOKEN.value, - invalid_input_error_code - ) - elif CredentialField.API_KEY in credentials: - validate_required_field( - logger, credentials, CredentialField.API_KEY, str, - SkyflowMessages.Error.EMPTY_API_KEY.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.EMPTY_API_KEY.value, - SkyflowMessages.Error.INVALID_API_KEY.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_API_KEY.value - ) - if not validate_api_key(credentials.get(CredentialField.API_KEY), logger): - raise SkyflowError(SkyflowMessages.Error.INVALID_API_KEY.value.format(config_id_type, config_id) - if config_id_type and config_id else SkyflowMessages.Error.INVALID_API_KEY.value, - invalid_input_error_code) - - if CredentialField.TOKEN_URI_OPTION in credentials: - token_uri = credentials.get(CredentialField.TOKEN_URI_OPTION) - if ( - token_uri is None - or not isinstance(token_uri, str) - or not is_valid_url(token_uri) - ): - log_error_log(SkyflowMessages.ErrorLogs.INVALID_TOKEN_URI.value, logger) - raise SkyflowError(SkyflowMessages.Error.INVALID_TOKEN_URI.value, invalid_input_error_code) + """Delegates to common.utils.validations.validate_credentials -- identical logic, v2's own + SkyflowMessages passed through so raised error text keeps showing v2's SDK version. Kept + under this name/signature since validate_connection_config/validate_update_connection_config + (v2-only, not shared) still call it directly.""" + return _common_validate_credentials(logger, credentials, config_id_type, config_id, messages=SkyflowMessages) def validate_log_level(logger, log_level): - if not isinstance(log_level, LogLevel): - log_error_log(SkyflowMessages.ErrorLogs.INVALID_LOG_LEVEL.value, logger) - raise SkyflowError(SkyflowMessages.Error.INVALID_LOG_LEVEL.value, invalid_input_error_code) + return _common_validate_log_level(logger, log_level, messages=SkyflowMessages) def validate_keys(logger, config, config_keys): for key in config.keys(): @@ -185,62 +98,12 @@ def validate_keys(logger, config, config_keys): raise SkyflowError(SkyflowMessages.Error.INVALID_KEY.value.format(key), invalid_input_error_code) def validate_vault_config(logger, config): - log_info(SkyflowMessages.Info.VALIDATING_VAULT_CONFIG.value, logger) - validate_keys(logger, config, valid_vault_config_keys) - - # Validate vault_id (string, not empty) - validate_required_field( - logger, config, ConfigField.VAULT_ID, str, - SkyflowMessages.Error.EMPTY_VAULT_ID.value, - SkyflowMessages.Error.INVALID_VAULT_ID.value - ) - vault_id = config.get(ConfigField.VAULT_ID) - # Validate cluster_id (string, not empty) - validate_required_field( - logger, config, ConfigField.CLUSTER_ID, str, - SkyflowMessages.Error.EMPTY_CLUSTER_ID.value.format(vault_id), - SkyflowMessages.Error.INVALID_CLUSTER_ID.value.format(vault_id) - ) - - # Validate credentials (dict, not empty) - if ConfigField.CREDENTIALS in config and not config.get(ConfigField.CREDENTIALS): - raise SkyflowError(SkyflowMessages.Error.EMPTY_CREDENTIALS.value.format(ConfigType.VAULT, vault_id), invalid_input_error_code) - - if ConfigField.CREDENTIALS in config and config.get(ConfigField.CREDENTIALS): - validate_credentials(logger, config.get(ConfigField.CREDENTIALS), ConfigType.VAULT, vault_id) - - # Validate env (optional, should be one of LogLevel values) - if ConfigField.ENV in config and config.get(ConfigField.ENV) not in Env: - log_error_log(SkyflowMessages.ErrorLogs.ENV_IS_REQUIRED.value, logger) - raise SkyflowError(SkyflowMessages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code) - - return True + """Delegates to common.utils.validations.validate_vault_config -- identical logic to + flowvault's own validate_vault_config, confirmed field-for-field.""" + return _common_validate_vault_config(logger, config, messages=SkyflowMessages) def validate_update_vault_config(logger, config): - - validate_keys(logger, config, valid_vault_config_keys) - - # Validate vault_id (string, not empty) - validate_required_field( - logger, config, ConfigField.VAULT_ID, str, - SkyflowMessages.Error.EMPTY_VAULT_ID.value, - SkyflowMessages.Error.INVALID_VAULT_ID.value - ) - - vault_id = config.get(ConfigField.VAULT_ID) - - if ConfigField.CLUSTER_ID in config and not config.get(ConfigField.CLUSTER_ID): - raise SkyflowError(SkyflowMessages.Error.INVALID_CLUSTER_ID.value.format(vault_id), invalid_input_error_code) - - if ConfigField.ENV in config and config.get(ConfigField.ENV) not in Env: - raise SkyflowError(SkyflowMessages.Error.INVALID_ENV.value.format(vault_id), invalid_input_error_code) - - if ConfigField.CREDENTIALS not in config: - raise SkyflowError(SkyflowMessages.Error.EMPTY_CREDENTIALS.value.format(ConfigType.VAULT, vault_id), invalid_input_error_code) - - validate_credentials(logger, config.get(ConfigField.CREDENTIALS), ConfigType.VAULT, vault_id) - - return True + return _common_validate_update_vault_config(logger, config, messages=SkyflowMessages) def validate_connection_config(logger, config): log_info(SkyflowMessages.Info.VALIDATING_CONNECTION_CONFIG.value, logger) @@ -469,10 +332,8 @@ def validate_insert_request(logger, request): log_error_log(SkyflowMessages.ErrorLogs.EMPTY_VALUES.value.format(RequestOperation.INSERT), logger=logger) raise SkyflowError(SkyflowMessages.Error.EMPTY_DATA_IN_INSERT.value, invalid_input_error_code) - for i, item in enumerate(request.values, start=1): - for key, value in item.items(): - if key is None or key == "": - log_error_log(SkyflowMessages.ErrorLogs.EMPTY_OR_NULL_KEY_IN_VALUES.value.format(RequestOperation.INSERT), logger = logger) + # Per-record key/value emptiness is validated by the controller via the shared + # BaseVaultController._validate_field_values() -- not here, to avoid duplicating that logic. if request.upsert is not None and (not isinstance(request.upsert, str) or not request.upsert.strip()): log_error_log(SkyflowMessages.ErrorLogs.EMPTY_UPSERT.value.format(RequestOperation.INSERT), logger=logger) diff --git a/v2/skyflow/vault/__init__.py b/v2/skyflow/vault/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/v2/skyflow/vault/client/__init__.py b/v2/skyflow/vault/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/v2/skyflow/vault/client/client.py b/v2/skyflow/vault/client/client.py new file mode 100644 index 00000000..7ddb8996 --- /dev/null +++ b/v2/skyflow/vault/client/client.py @@ -0,0 +1,27 @@ +from common.utils import get_vault_url +from common.vault.base_vault_client import BaseVaultClient +from skyflow.generated.rest.client import Skyflow + + +class VaultClient(BaseVaultClient): + def resolve_vault_url(self, cluster_id, env, vault_id, logger=None): + return get_vault_url(cluster_id, env, vault_id, logger=logger) + + def initialize_api_client(self, vault_url, bearer_token): + token_provider = lambda: self._bearer_token if self._bearer_token is not None else bearer_token # noqa: E731 + self._api_client = Skyflow(base_url=vault_url, token=token_provider) + + def get_records_api(self): + return self._api_client.records + + def get_tokens_api(self): + return self._api_client.tokens + + def get_query_api(self): + return self._api_client.query + + def get_detect_text_api(self): + return self._api_client.strings + + def get_detect_file_api(self): + return self._api_client.files diff --git a/skyflow/vault/connection/__init__.py b/v2/skyflow/vault/connection/__init__.py similarity index 100% rename from skyflow/vault/connection/__init__.py rename to v2/skyflow/vault/connection/__init__.py diff --git a/skyflow/vault/connection/_invoke_connection_request.py b/v2/skyflow/vault/connection/_invoke_connection_request.py similarity index 100% rename from skyflow/vault/connection/_invoke_connection_request.py rename to v2/skyflow/vault/connection/_invoke_connection_request.py diff --git a/skyflow/vault/connection/_invoke_connection_response.py b/v2/skyflow/vault/connection/_invoke_connection_response.py similarity index 100% rename from skyflow/vault/connection/_invoke_connection_response.py rename to v2/skyflow/vault/connection/_invoke_connection_response.py diff --git a/v2/skyflow/vault/controller/__init__.py b/v2/skyflow/vault/controller/__init__.py new file mode 100644 index 00000000..2840034e --- /dev/null +++ b/v2/skyflow/vault/controller/__init__.py @@ -0,0 +1,8 @@ +from ._vault import VaultController +from ._connections import Connection +from ._detect import Detect + +# Public backward-compatible name -- existing consumers do `from skyflow.vault.controller import +# Vault`; VaultController is the canonical internal name (extends common.vault.base_vault_controller's +# BaseVaultController), but the old public name must keep resolving to the exact same class. +Vault = VaultController diff --git a/skyflow/vault/controller/_audit.py b/v2/skyflow/vault/controller/_audit.py similarity index 100% rename from skyflow/vault/controller/_audit.py rename to v2/skyflow/vault/controller/_audit.py diff --git a/skyflow/vault/controller/_bin_look_up.py b/v2/skyflow/vault/controller/_bin_look_up.py similarity index 100% rename from skyflow/vault/controller/_bin_look_up.py rename to v2/skyflow/vault/controller/_bin_look_up.py diff --git a/skyflow/vault/controller/_connections.py b/v2/skyflow/vault/controller/_connections.py similarity index 100% rename from skyflow/vault/controller/_connections.py rename to v2/skyflow/vault/controller/_connections.py diff --git a/skyflow/vault/controller/_detect.py b/v2/skyflow/vault/controller/_detect.py similarity index 100% rename from skyflow/vault/controller/_detect.py rename to v2/skyflow/vault/controller/_detect.py diff --git a/skyflow/vault/controller/_vault.py b/v2/skyflow/vault/controller/_vault.py similarity index 93% rename from skyflow/vault/controller/_vault.py rename to v2/skyflow/vault/controller/_vault.py index 6c47fe3e..16e6aeac 100644 --- a/skyflow/vault/controller/_vault.py +++ b/v2/skyflow/vault/controller/_vault.py @@ -2,6 +2,7 @@ import json import os from typing import Optional +from common.vault.base_vault_controller import BaseVaultController from skyflow.generated.rest import V1FieldRecords, V1BatchRecord, V1TokenizeRecordRequest, \ V1DetokenizeRecordRequest from skyflow.generated.rest.core.file import File @@ -14,10 +15,12 @@ from skyflow.utils.logger import log_info, log_error_log from skyflow.utils.validations import validate_insert_request, validate_delete_request, validate_query_request, \ validate_get_request, validate_update_request, validate_detokenize_request, validate_tokenize_request, validate_file_upload_request -from skyflow.vault.data import InsertRequest, UpdateRequest, DeleteRequest, GetRequest, QueryRequest, FileUploadRequest, FileUploadResponse -from skyflow.vault.tokens import DetokenizeRequest, TokenizeRequest +from skyflow.vault.data import InsertRequest, InsertResponse, UpdateRequest, UpdateResponse, DeleteRequest, DeleteResponse, GetRequest, GetResponse, QueryRequest, QueryResponse, FileUploadRequest, FileUploadResponse +from skyflow.vault.tokens import DetokenizeRequest, DetokenizeResponse, TokenizeRequest, TokenizeResponse + +class VaultController(BaseVaultController): + _skyflow_messages = SkyflowMessages -class Vault: def __init__(self, vault_client): self.__vault_client = vault_client @@ -91,9 +94,11 @@ def __get_file_for_file_upload(self, request: FileUploadRequest) -> Optional[Fil def __get_headers(self): return {SKY_META_DATA_HEADER: json.dumps(get_metrics())} - def insert(self, request: InsertRequest): + def insert(self, request: InsertRequest) -> InsertResponse: log_info(SkyflowMessages.Info.VALIDATE_INSERT_REQUEST.value, self.__vault_client.get_logger()) validate_insert_request(self.__vault_client.get_logger(), request) + for item in request.values: + self._validate_field_values(item) log_info(SkyflowMessages.Info.INSERT_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) self.__initialize() records_api = self.__vault_client.get_records_api().with_raw_response @@ -117,7 +122,7 @@ def insert(self, request: InsertRequest): log_error_log(SkyflowMessages.ErrorLogs.INSERT_RECORDS_REJECTED.value, self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def update(self, request: UpdateRequest): + def update(self, request: UpdateRequest) -> UpdateResponse: log_info(SkyflowMessages.Info.VALIDATE_UPDATE_REQUEST.value, self.__vault_client.get_logger()) validate_update_request(self.__vault_client.get_logger(), request) log_info(SkyflowMessages.Info.UPDATE_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) @@ -144,7 +149,7 @@ def update(self, request: UpdateRequest): log_error_log(SkyflowMessages.ErrorLogs.UPDATE_REQUEST_REJECTED.value, logger = self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def delete(self, request: DeleteRequest): + def delete(self, request: DeleteRequest) -> DeleteResponse: log_info(SkyflowMessages.Info.VALIDATING_DELETE_REQUEST.value, self.__vault_client.get_logger()) validate_delete_request(self.__vault_client.get_logger(), request) log_info(SkyflowMessages.Info.DELETE_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) @@ -165,7 +170,7 @@ def delete(self, request: DeleteRequest): log_error_log(SkyflowMessages.ErrorLogs.DELETE_REQUEST_REJECTED.value, logger = self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def get(self, request: GetRequest): + def get(self, request: GetRequest) -> GetResponse: log_info(SkyflowMessages.Info.VALIDATE_GET_REQUEST.value, self.__vault_client.get_logger()) validate_get_request(self.__vault_client.get_logger(), request) log_info(SkyflowMessages.Info.GET_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) @@ -195,7 +200,7 @@ def get(self, request: GetRequest): log_error_log(SkyflowMessages.ErrorLogs.GET_REQUEST_REJECTED.value, self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def query(self, request: QueryRequest): + def query(self, request: QueryRequest) -> QueryResponse: log_info(SkyflowMessages.Info.VALIDATING_QUERY_REQUEST.value, self.__vault_client.get_logger()) validate_query_request(self.__vault_client.get_logger(), request) log_info(SkyflowMessages.Info.QUERY_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) @@ -215,7 +220,7 @@ def query(self, request: QueryRequest): log_error_log(SkyflowMessages.ErrorLogs.QUERY_REQUEST_REJECTED.value, self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def detokenize(self, request: DetokenizeRequest): + def detokenize(self, request: DetokenizeRequest) -> DetokenizeResponse: log_info(SkyflowMessages.Info.VALIDATE_DETOKENIZE_REQUEST.value, self.__vault_client.get_logger()) validate_detokenize_request(self.__vault_client.get_logger(), request) log_info(SkyflowMessages.Info.DETOKENIZE_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) @@ -243,7 +248,7 @@ def detokenize(self, request: DetokenizeRequest): log_error_log(SkyflowMessages.ErrorLogs.DETOKENIZE_REQUEST_REJECTED.value, logger = self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def tokenize(self, request: TokenizeRequest): + def tokenize(self, request: TokenizeRequest) -> TokenizeResponse: log_info(SkyflowMessages.Info.VALIDATING_TOKENIZE_REQUEST.value, self.__vault_client.get_logger()) validate_tokenize_request(self.__vault_client.get_logger(), request) log_info(SkyflowMessages.Info.TOKENIZE_REQUEST_RESOLVED.value, self.__vault_client.get_logger()) @@ -268,7 +273,7 @@ def tokenize(self, request: TokenizeRequest): log_error_log(SkyflowMessages.ErrorLogs.TOKENIZE_REQUEST_REJECTED.value, logger = self.__vault_client.get_logger()) handle_exception(e, self.__vault_client.get_logger()) - def upload_file(self, request: FileUploadRequest): + def upload_file(self, request: FileUploadRequest) -> FileUploadResponse: log_info(SkyflowMessages.Info.FILE_UPLOAD_TRIGGERED.value, self.__vault_client.get_logger()) log_info(SkyflowMessages.Info.VALIDATING_FILE_UPLOAD_REQUEST.value, self.__vault_client.get_logger()) validate_file_upload_request(self.__vault_client.get_logger(), request) diff --git a/skyflow/vault/data/__init__.py b/v2/skyflow/vault/data/__init__.py similarity index 100% rename from skyflow/vault/data/__init__.py rename to v2/skyflow/vault/data/__init__.py diff --git a/skyflow/vault/data/_delete_request.py b/v2/skyflow/vault/data/_delete_request.py similarity index 100% rename from skyflow/vault/data/_delete_request.py rename to v2/skyflow/vault/data/_delete_request.py diff --git a/skyflow/vault/data/_delete_response.py b/v2/skyflow/vault/data/_delete_response.py similarity index 100% rename from skyflow/vault/data/_delete_response.py rename to v2/skyflow/vault/data/_delete_response.py diff --git a/skyflow/vault/data/_file_upload_request.py b/v2/skyflow/vault/data/_file_upload_request.py similarity index 100% rename from skyflow/vault/data/_file_upload_request.py rename to v2/skyflow/vault/data/_file_upload_request.py diff --git a/skyflow/vault/data/_file_upload_response.py b/v2/skyflow/vault/data/_file_upload_response.py similarity index 100% rename from skyflow/vault/data/_file_upload_response.py rename to v2/skyflow/vault/data/_file_upload_response.py diff --git a/skyflow/vault/data/_get_request.py b/v2/skyflow/vault/data/_get_request.py similarity index 100% rename from skyflow/vault/data/_get_request.py rename to v2/skyflow/vault/data/_get_request.py diff --git a/skyflow/vault/data/_get_response.py b/v2/skyflow/vault/data/_get_response.py similarity index 100% rename from skyflow/vault/data/_get_response.py rename to v2/skyflow/vault/data/_get_response.py diff --git a/v2/skyflow/vault/data/_insert_request.py b/v2/skyflow/vault/data/_insert_request.py new file mode 100644 index 00000000..2c6c2dcc --- /dev/null +++ b/v2/skyflow/vault/data/_insert_request.py @@ -0,0 +1,20 @@ +from common.vault.data import BaseInsertRequest +from skyflow.utils.enums import TokenMode + +class InsertRequest(BaseInsertRequest): + def __init__(self, + table: str, + values: list, + tokens: list = None, + upsert: str = None, + homogeneous: bool = False, + token_mode: TokenMode = TokenMode.DISABLE, + return_tokens: bool = True, + continue_on_error: bool = False): + super().__init__(table, values, upsert=upsert) + self.tokens = tokens + self.homogeneous = homogeneous + self.token_mode = token_mode + self.return_tokens = return_tokens + self.continue_on_error = continue_on_error + diff --git a/v2/skyflow/vault/data/_insert_response.py b/v2/skyflow/vault/data/_insert_response.py new file mode 100644 index 00000000..0cf73343 --- /dev/null +++ b/v2/skyflow/vault/data/_insert_response.py @@ -0,0 +1,6 @@ +from common.vault.data import BaseInsertResponse + + +class InsertResponse(BaseInsertResponse): + """PDB's own insert() response class -- currently identical to the shared base, kept as its + own subclass so PDB-specific fields can be added later without touching flowvault.""" diff --git a/skyflow/vault/data/_query_request.py b/v2/skyflow/vault/data/_query_request.py similarity index 100% rename from skyflow/vault/data/_query_request.py rename to v2/skyflow/vault/data/_query_request.py diff --git a/skyflow/vault/data/_query_response.py b/v2/skyflow/vault/data/_query_response.py similarity index 100% rename from skyflow/vault/data/_query_response.py rename to v2/skyflow/vault/data/_query_response.py diff --git a/skyflow/vault/data/_update_request.py b/v2/skyflow/vault/data/_update_request.py similarity index 100% rename from skyflow/vault/data/_update_request.py rename to v2/skyflow/vault/data/_update_request.py diff --git a/skyflow/vault/data/_update_response.py b/v2/skyflow/vault/data/_update_response.py similarity index 100% rename from skyflow/vault/data/_update_response.py rename to v2/skyflow/vault/data/_update_response.py diff --git a/skyflow/vault/data/_upload_file_request.py b/v2/skyflow/vault/data/_upload_file_request.py similarity index 100% rename from skyflow/vault/data/_upload_file_request.py rename to v2/skyflow/vault/data/_upload_file_request.py diff --git a/skyflow/vault/detect/__init__.py b/v2/skyflow/vault/detect/__init__.py similarity index 100% rename from skyflow/vault/detect/__init__.py rename to v2/skyflow/vault/detect/__init__.py diff --git a/skyflow/vault/detect/_audio_bleep.py b/v2/skyflow/vault/detect/_audio_bleep.py similarity index 100% rename from skyflow/vault/detect/_audio_bleep.py rename to v2/skyflow/vault/detect/_audio_bleep.py diff --git a/skyflow/vault/detect/_date_transformation.py b/v2/skyflow/vault/detect/_date_transformation.py similarity index 100% rename from skyflow/vault/detect/_date_transformation.py rename to v2/skyflow/vault/detect/_date_transformation.py diff --git a/skyflow/vault/detect/_deidentify_file_request.py b/v2/skyflow/vault/detect/_deidentify_file_request.py similarity index 100% rename from skyflow/vault/detect/_deidentify_file_request.py rename to v2/skyflow/vault/detect/_deidentify_file_request.py diff --git a/skyflow/vault/detect/_deidentify_file_response.py b/v2/skyflow/vault/detect/_deidentify_file_response.py similarity index 100% rename from skyflow/vault/detect/_deidentify_file_response.py rename to v2/skyflow/vault/detect/_deidentify_file_response.py diff --git a/skyflow/vault/detect/_deidentify_text_request.py b/v2/skyflow/vault/detect/_deidentify_text_request.py similarity index 100% rename from skyflow/vault/detect/_deidentify_text_request.py rename to v2/skyflow/vault/detect/_deidentify_text_request.py diff --git a/skyflow/vault/detect/_deidentify_text_response.py b/v2/skyflow/vault/detect/_deidentify_text_response.py similarity index 100% rename from skyflow/vault/detect/_deidentify_text_response.py rename to v2/skyflow/vault/detect/_deidentify_text_response.py diff --git a/skyflow/vault/detect/_entity_info.py b/v2/skyflow/vault/detect/_entity_info.py similarity index 100% rename from skyflow/vault/detect/_entity_info.py rename to v2/skyflow/vault/detect/_entity_info.py diff --git a/skyflow/vault/detect/_file.py b/v2/skyflow/vault/detect/_file.py similarity index 100% rename from skyflow/vault/detect/_file.py rename to v2/skyflow/vault/detect/_file.py diff --git a/skyflow/vault/detect/_file_input.py b/v2/skyflow/vault/detect/_file_input.py similarity index 100% rename from skyflow/vault/detect/_file_input.py rename to v2/skyflow/vault/detect/_file_input.py diff --git a/skyflow/vault/detect/_get_detect_run_request.py b/v2/skyflow/vault/detect/_get_detect_run_request.py similarity index 100% rename from skyflow/vault/detect/_get_detect_run_request.py rename to v2/skyflow/vault/detect/_get_detect_run_request.py diff --git a/skyflow/vault/detect/_reidentify_text_request.py b/v2/skyflow/vault/detect/_reidentify_text_request.py similarity index 100% rename from skyflow/vault/detect/_reidentify_text_request.py rename to v2/skyflow/vault/detect/_reidentify_text_request.py diff --git a/skyflow/vault/detect/_reidentify_text_response.py b/v2/skyflow/vault/detect/_reidentify_text_response.py similarity index 100% rename from skyflow/vault/detect/_reidentify_text_response.py rename to v2/skyflow/vault/detect/_reidentify_text_response.py diff --git a/skyflow/vault/detect/_text_index.py b/v2/skyflow/vault/detect/_text_index.py similarity index 100% rename from skyflow/vault/detect/_text_index.py rename to v2/skyflow/vault/detect/_text_index.py diff --git a/skyflow/vault/detect/_token_format.py b/v2/skyflow/vault/detect/_token_format.py similarity index 100% rename from skyflow/vault/detect/_token_format.py rename to v2/skyflow/vault/detect/_token_format.py diff --git a/skyflow/vault/detect/_transformations.py b/v2/skyflow/vault/detect/_transformations.py similarity index 100% rename from skyflow/vault/detect/_transformations.py rename to v2/skyflow/vault/detect/_transformations.py diff --git a/skyflow/vault/tokens/__init__.py b/v2/skyflow/vault/tokens/__init__.py similarity index 100% rename from skyflow/vault/tokens/__init__.py rename to v2/skyflow/vault/tokens/__init__.py diff --git a/skyflow/vault/tokens/_detokenize_request.py b/v2/skyflow/vault/tokens/_detokenize_request.py similarity index 100% rename from skyflow/vault/tokens/_detokenize_request.py rename to v2/skyflow/vault/tokens/_detokenize_request.py diff --git a/skyflow/vault/tokens/_detokenize_response.py b/v2/skyflow/vault/tokens/_detokenize_response.py similarity index 100% rename from skyflow/vault/tokens/_detokenize_response.py rename to v2/skyflow/vault/tokens/_detokenize_response.py diff --git a/skyflow/vault/tokens/_tokenize_request.py b/v2/skyflow/vault/tokens/_tokenize_request.py similarity index 100% rename from skyflow/vault/tokens/_tokenize_request.py rename to v2/skyflow/vault/tokens/_tokenize_request.py diff --git a/skyflow/vault/tokens/_tokenize_response.py b/v2/skyflow/vault/tokens/_tokenize_response.py similarity index 100% rename from skyflow/vault/tokens/_tokenize_response.py rename to v2/skyflow/vault/tokens/_tokenize_response.py diff --git a/v2/tests/__init__.py b/v2/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/v2/tests/client/__init__.py b/v2/tests/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/client/test_skyflow.py b/v2/tests/client/test_skyflow.py similarity index 91% rename from tests/client/test_skyflow.py rename to v2/tests/client/test_skyflow.py index 5b7ea675..00d360fc 100644 --- a/tests/client/test_skyflow.py +++ b/v2/tests/client/test_skyflow.py @@ -124,7 +124,7 @@ def test_get_vault_with_invalid_vault_id_and_non_empty_list_raises_error(self): SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format("invalid_vault_id"), ) - @patch("skyflow.client.skyflow.validate_vault_config") + @patch("skyflow.client.skyflow.Skyflow.Builder._validate_vault_config") def test_build_calls_validate_vault_config(self, mock_validate_vault_config): self.builder.add_vault_config(VALID_VAULT_CONFIG) self.builder.build() @@ -223,7 +223,7 @@ def test_get_connection_with_invalid_connection_id_and_empty_list_raises_Error(s self.assertEqual(context.exception.message, SkyflowMessages.Error.EMPTY_CONNECTION_CONFIGS.value) - @patch("skyflow.client.skyflow.validate_connection_config") + @patch("skyflow.client.skyflow.Skyflow.Builder._validate_connection_config") def test_build_calls_validate_connection_config(self, mock_validate): self.builder.add_connection_config(VALID_CONNECTION_CONFIG) self.builder.build() @@ -246,16 +246,16 @@ def test_invalid_credentials(self): self.assertEqual(VALID_CREDENTIALS, self.builder._Builder__skyflow_credentials) self.assertEqual(builder, self.builder) - @patch("skyflow.client.skyflow.validate_vault_config") + @patch("skyflow.client.skyflow.Skyflow.Builder._validate_vault_config") def test_skyflow_client_add_remove_vault_config(self, mock_validate_vault_config): skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() new_config = VALID_VAULT_CONFIG.copy() - new_config["vault_id"] = "VAULT_ID" + new_config["vault_id"] = "VAULT_ID_2" skyflow_client.add_vault_config(new_config) self.assertEqual(mock_validate_vault_config.call_count, 2) - self.assertEqual("VAULT_ID", skyflow_client.get_vault_config(new_config["vault_id"]).get("vault_id")) + self.assertEqual("VAULT_ID_2", skyflow_client.get_vault_config(new_config["vault_id"]).get("vault_id")) skyflow_client.remove_vault_config(new_config["vault_id"]) with self.assertRaises(SkyflowError) as context: @@ -266,6 +266,16 @@ def test_skyflow_client_add_remove_vault_config(self, mock_validate_vault_config SkyflowMessages.Error.VAULT_ID_NOT_IN_CONFIG_LIST.value.format(new_config["vault_id"]), ) + def test_skyflow_client_add_vault_config_duplicate_id_raises(self): + skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() + with self.assertRaises(SkyflowError) as context: + skyflow_client.add_vault_config(VALID_VAULT_CONFIG) + + self.assertEqual( + context.exception.message, + SkyflowMessages.Error.VAULT_ID_ALREADY_EXISTS.value.format(VALID_VAULT_CONFIG["vault_id"]), + ) + @patch("skyflow.vault.client.client.VaultClient.update_config") def test_skyflow_client_update_and_get_vault_config(self, mock_update_config): skyflow_client = self.builder.add_vault_config(VALID_VAULT_CONFIG).build() @@ -278,19 +288,19 @@ def test_skyflow_client_update_and_get_vault_config(self, mock_update_config): self.assertEqual(VALID_VAULT_CONFIG.get("vault_id"), vault.get("vault_id")) - @patch("skyflow.client.skyflow.validate_connection_config") + @patch("skyflow.client.skyflow.Skyflow.Builder._validate_connection_config") def test_skyflow_client_add_remove_connection_config(self, mock_validate_connection_config): skyflow_client = self.builder.add_connection_config(VALID_CONNECTION_CONFIG).build() new_config = VALID_CONNECTION_CONFIG.copy() - new_config["connection_id"] = "CONNECTION_ID" + new_config["connection_id"] = "CONNECTION_ID_2" skyflow_client.add_connection_config(new_config) self.assertEqual(mock_validate_connection_config.call_count, 2) self.assertEqual( - "CONNECTION_ID", skyflow_client.get_connection_config(new_config["connection_id"]).get("connection_id") + "CONNECTION_ID_2", skyflow_client.get_connection_config(new_config["connection_id"]).get("connection_id") ) - skyflow_client.remove_connection_config("CONNECTION_ID") + skyflow_client.remove_connection_config("CONNECTION_ID_2") with self.assertRaises(SkyflowError) as context: skyflow_client.get_connection_config(new_config["connection_id"]).get("connection_id") @@ -299,6 +309,16 @@ def test_skyflow_client_add_remove_connection_config(self, mock_validate_connect SkyflowMessages.Error.CONNECTION_ID_NOT_IN_CONFIG_LIST.value.format(new_config["connection_id"]), ) + def test_skyflow_client_add_connection_config_duplicate_id_raises(self): + skyflow_client = self.builder.add_connection_config(VALID_CONNECTION_CONFIG).build() + with self.assertRaises(SkyflowError) as context: + skyflow_client.add_connection_config(VALID_CONNECTION_CONFIG) + + self.assertEqual( + context.exception.message, + SkyflowMessages.Error.CONNECTION_ID_ALREADY_EXISTS.value.format(VALID_CONNECTION_CONFIG["connection_id"]), + ) + @patch("skyflow.vault.client.client.VaultClient.update_config") def test_skyflow_client_update_and_get_connection_config(self, mock_update_config): builder = self.builder @@ -392,21 +412,21 @@ def test_update_connection_config_with_invalid_connection_id_raises_error(self, class TestVaultClient(unittest.TestCase): def _make_client(self): client = VaultClient({"vault_id": "test_vault"}) - client._VaultClient__api_client = Mock() + client._api_client = Mock() return client def test_get_detect_text_api_returns_strings(self): client = self._make_client() result = client.get_detect_text_api() - self.assertEqual(result, client._VaultClient__api_client.strings) + self.assertEqual(result, client._api_client.strings) def test_get_detect_file_api_returns_files(self): client = self._make_client() result = client.get_detect_file_api() - self.assertEqual(result, client._VaultClient__api_client.files) + self.assertEqual(result, client._api_client.files) - @patch("skyflow.vault.client.client.generate_bearer_token_from_creds") - @patch("skyflow.vault.client.client.is_expired", return_value=True) + @patch("common.vault.base_vault_client.generate_bearer_token_from_creds") + @patch("common.vault.base_vault_client.is_expired", return_value=True) def test_get_bearer_token_passes_token_uri_option(self, _mock_expired, mock_gen): mock_gen.return_value = ("test_token", "bearer") client = VaultClient({"vault_id": "test_vault"}) @@ -426,7 +446,7 @@ def _build_client(self): def test_update_log_level_emits_deprecation_warning(self): client = self._build_client() - with patch('skyflow.client.skyflow.log_warn') as mock_warn: + with patch('common.client.base_skyflow.log_warn') as mock_warn: client.update_log_level(LogLevel.INFO) mock_warn.assert_called_once() self.assertIn("set_log_level", mock_warn.call_args[0][0]) diff --git a/v2/tests/service_account/__init__.py b/v2/tests/service_account/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/service_account/invalid_creds.json b/v2/tests/service_account/invalid_creds.json similarity index 100% rename from tests/service_account/invalid_creds.json rename to v2/tests/service_account/invalid_creds.json diff --git a/tests/service_account/test__utils.py b/v2/tests/service_account/test__utils.py similarity index 100% rename from tests/service_account/test__utils.py rename to v2/tests/service_account/test__utils.py diff --git a/v2/tests/utils/__init__.py b/v2/tests/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/v2/tests/utils/logger/__init__.py b/v2/tests/utils/logger/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/logger/test__log_helpers.py b/v2/tests/utils/logger/test__log_helpers.py similarity index 87% rename from tests/utils/logger/test__log_helpers.py rename to v2/tests/utils/logger/test__log_helpers.py index 1ea50d45..7cf2e9db 100644 --- a/tests/utils/logger/test__log_helpers.py +++ b/v2/tests/utils/logger/test__log_helpers.py @@ -7,7 +7,7 @@ class TestLoggingFunctions(unittest.TestCase): - @patch('skyflow.utils.logger._log_helpers.Logger') + @patch('common.utils.logger._log_helpers.Logger') def test_log_info_with_logger(self, MockLogger): mock_logger = MockLogger() message = "Info message" @@ -17,14 +17,14 @@ def test_log_info_with_logger(self, MockLogger): mock_logger.info.assert_called_once_with(f"{message}") - @patch('skyflow.utils.logger._log_helpers.Logger') + @patch('common.utils.logger._log_helpers.Logger') def test_log_info_without_logger(self, MockLogger): try: log_info("Message", None) except AttributeError: self.fail("log_info raised AttributeError unexpectedly!") - @patch('skyflow.utils.logger._log_helpers.Logger') + @patch('common.utils.logger._log_helpers.Logger') def test_log_error_with_all_fields(self, MockLogger): mock_logger = MockLogger() message = "Error message" @@ -47,7 +47,7 @@ def test_log_error_with_all_fields(self, MockLogger): mock_logger.error.assert_called_once_with(expected_log_data) - @patch('skyflow.utils.logger._log_helpers.Logger') + @patch('common.utils.logger._log_helpers.Logger') def test_log_error_with_minimal_fields(self, MockLogger): mock_logger = MockLogger() message = "Minimal error" @@ -62,7 +62,7 @@ def test_log_error_with_minimal_fields(self, MockLogger): mock_logger.error.assert_called_once_with(expected_log_data) - @patch('skyflow.utils.logger._log_helpers.Logger') + @patch('common.utils.logger._log_helpers.Logger') def test_log_error_creates_logger_if_none(self, MockLogger): message = "Auto-created logger error" http_code = 500 @@ -71,7 +71,7 @@ def test_log_error_creates_logger_if_none(self, MockLogger): MockLogger.assert_called_once_with(LogLevel.ERROR) - @patch('skyflow.utils.logger._log_helpers.Logger') + @patch('common.utils.logger._log_helpers.Logger') def test_log_error_handles_missing_optional_fields(self, MockLogger): mock_logger = MockLogger() message = "Test missing optional fields" diff --git a/tests/utils/logger/test__logger.py b/v2/tests/utils/logger/test__logger.py similarity index 100% rename from tests/utils/logger/test__logger.py rename to v2/tests/utils/logger/test__logger.py diff --git a/tests/utils/test__helpers.py b/v2/tests/utils/test__helpers.py similarity index 100% rename from tests/utils/test__helpers.py rename to v2/tests/utils/test__helpers.py diff --git a/tests/utils/test__utils.py b/v2/tests/utils/test__utils.py similarity index 100% rename from tests/utils/test__utils.py rename to v2/tests/utils/test__utils.py diff --git a/v2/tests/utils/validations/__init__.py b/v2/tests/utils/validations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/utils/validations/test__validations.py b/v2/tests/utils/validations/test__validations.py similarity index 100% rename from tests/utils/validations/test__validations.py rename to v2/tests/utils/validations/test__validations.py diff --git a/v2/tests/vault/__init__.py b/v2/tests/vault/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/v2/tests/vault/client/__init__.py b/v2/tests/vault/client/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/v2/tests/vault/client/test__client.py b/v2/tests/vault/client/test__client.py new file mode 100644 index 00000000..a0894567 --- /dev/null +++ b/v2/tests/vault/client/test__client.py @@ -0,0 +1,127 @@ +import unittest +from unittest.mock import patch, MagicMock + +from common.utils.enums import Env +from skyflow.error import SkyflowError +from skyflow.utils import SkyflowMessages +from skyflow.vault.client.client import VaultClient + +CONFIG = { + "credentials": "some_credentials", + "cluster_id": "test_cluster_id", + "env": "test_env", + "vault_id": "test_vault_id", + "roles": ["role_id_1", "role_id_2"], + "ctx": "context" +} + +CREDENTIALS_WITH_API_KEY = {"api_key": "dummy_api_key"} +CREDENTIALS_WITH_TOKEN = {"token": "dummy_static_token"} +CREDENTIALS_WITH_PATH = {"path": "/some/path/credentials.json"} +CREDENTIALS_WITH_STRING = {"credentials_string": '{"clientID": "x"}'} + + +class TestVaultClient(unittest.TestCase): + """v2-specific VaultClient coverage. + + Shared logic (initialize_client_configuration, get_bearer_token, credential-resolution + fast paths, update_config) moved to common.vault.base_vault_client.BaseVaultClient and is + covered by common/tests/vault/test_base_vault_client.py instead -- those scenarios used to + live here, patching names at this module's path, but the code they exercise no longer runs + from this module. What remains here is what's genuinely still v2-specific: the + initialize_api_client() lambda/Skyflow-construction override, and the resource accessors. + """ + + def setUp(self): + self.vault_client = VaultClient(CONFIG) + + # ------------------------------------------------------------------ # + # Basic setters / getters (inherited from BaseVaultClient, but exercised + # here too as a cheap smoke test that the subclass wiring works) + # ------------------------------------------------------------------ # + + def test_set_common_skyflow_credentials(self): + credentials = {"api_key": "dummy_api_key"} + self.vault_client.set_common_skyflow_credentials(credentials) + self.assertEqual(self.vault_client.get_common_skyflow_credentials(), credentials) + + def test_set_logger(self): + mock_logger = MagicMock() + self.vault_client.set_logger("INFO", mock_logger) + self.assertEqual(self.vault_client.get_log_level(), "INFO") + self.assertEqual(self.vault_client.get_logger(), mock_logger) + + def test_get_vault_id(self): + self.assertEqual(self.vault_client.get_vault_id(), CONFIG["vault_id"]) + + def test_get_config(self): + self.assertEqual(self.vault_client.get_config(), CONFIG) + + # ------------------------------------------------------------------ # + # resolve_vault_url — v2's own domain (vault.skyflowapis.*), the OTHER + # hook v2 overrides. Regression-pins the exact host v2 must keep hitting. + # ------------------------------------------------------------------ # + + def test_resolve_vault_url_uses_v2_domain(self): + url = self.vault_client.resolve_vault_url("mycluster", Env.PROD, "myvault") + self.assertEqual(url, "https://mycluster.vault.skyflowapis.com") + + # ------------------------------------------------------------------ # + # initialize_api_client — lambda token provider (v2-specific: this is + # the one hook v2 actually overrides) + # ------------------------------------------------------------------ # + + @patch("skyflow.vault.client.client.Skyflow") + def test_initialize_api_client_passes_callable_token(self, mock_skyflow): + """initialize_api_client must pass a callable (lambda) as token, not a string.""" + self.vault_client.initialize_api_client("https://test-vault-url.com", "initial_token") + + args, kwargs = mock_skyflow.call_args + self.assertEqual(kwargs["base_url"], "https://test-vault-url.com") + self.assertTrue(callable(kwargs["token"]), "token must be a callable (lambda)") + + @patch("skyflow.vault.client.client.Skyflow") + def test_initialize_api_client_lambda_returns_cached_bearer_token(self, mock_skyflow): + """Lambda returns _bearer_token when it is set (interceptor behaviour).""" + self.vault_client._bearer_token = "refreshed_token" + self.vault_client.initialize_api_client("https://test-vault-url.com", "initial_token") + + _, kwargs = mock_skyflow.call_args + self.assertEqual(kwargs["token"](), "refreshed_token") + + @patch("skyflow.vault.client.client.Skyflow") + def test_initialize_api_client_lambda_falls_back_to_initial_token(self, mock_skyflow): + """Lambda falls back to the initial token when _bearer_token is None.""" + self.vault_client._bearer_token = None + self.vault_client.initialize_api_client("https://test-vault-url.com", "initial_token") + + _, kwargs = mock_skyflow.call_args + self.assertEqual(kwargs["token"](), "initial_token") + + # ------------------------------------------------------------------ # + # API accessor stubs (v2-specific: v3 doesn't have these) + # ------------------------------------------------------------------ # + + def test_get_records_api(self): + self.vault_client._api_client = MagicMock() + self.assertIsNotNone(self.vault_client.get_records_api()) + + def test_get_tokens_api(self): + self.vault_client._api_client = MagicMock() + self.assertIsNotNone(self.vault_client.get_tokens_api()) + + def test_get_query_api(self): + self.vault_client._api_client = MagicMock() + self.assertIsNotNone(self.vault_client.get_query_api()) + + def test_get_detect_text_api(self): + self.vault_client._api_client = MagicMock() + self.assertIsNotNone(self.vault_client.get_detect_text_api()) + + def test_get_detect_file_api(self): + self.vault_client._api_client = MagicMock() + self.assertIsNotNone(self.vault_client.get_detect_file_api()) + + +if __name__ == "__main__": + unittest.main() diff --git a/v2/tests/vault/connection/__init__.py b/v2/tests/vault/connection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/vault/connection/test_responses.py b/v2/tests/vault/connection/test_responses.py similarity index 100% rename from tests/vault/connection/test_responses.py rename to v2/tests/vault/connection/test_responses.py diff --git a/v2/tests/vault/controller/__init__.py b/v2/tests/vault/controller/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/vault/controller/test__audit_binlookup.py b/v2/tests/vault/controller/test__audit_binlookup.py similarity index 100% rename from tests/vault/controller/test__audit_binlookup.py rename to v2/tests/vault/controller/test__audit_binlookup.py diff --git a/tests/vault/controller/test__connection.py b/v2/tests/vault/controller/test__connection.py similarity index 100% rename from tests/vault/controller/test__connection.py rename to v2/tests/vault/controller/test__connection.py diff --git a/tests/vault/controller/test__detect.py b/v2/tests/vault/controller/test__detect.py similarity index 100% rename from tests/vault/controller/test__detect.py rename to v2/tests/vault/controller/test__detect.py diff --git a/tests/vault/controller/test__vault.py b/v2/tests/vault/controller/test__vault.py similarity index 96% rename from tests/vault/controller/test__vault.py rename to v2/tests/vault/controller/test__vault.py index 5acdf779..f3c9124c 100644 --- a/tests/vault/controller/test__vault.py +++ b/v2/tests/vault/controller/test__vault.py @@ -140,6 +140,31 @@ def test_insert_handles_generic_error(self, mock_validate): records_api.with_raw_response.record_service_insert_record.assert_called_once() + @patch("skyflow.vault.controller._vault.validate_insert_request") + def test_insert_raises_on_empty_key(self, mock_validate): + """Shared BaseVaultController._validate_field_values() -- an empty/null key in insert + data now raises for v2 too (previously only logged a warning and let the insert through).""" + request = InsertRequest(table="test_table", values=[{"": "value"}]) + + with self.assertRaises(SkyflowError): + self.vault.insert(request) + + self.vault_client.get_records_api.return_value.with_raw_response.record_service_insert_record.assert_not_called() + + @patch("skyflow.vault.controller._vault.validate_insert_request") + @patch("skyflow.vault.controller._vault.parse_insert_response") + def test_insert_allows_empty_value(self, mock_parse_response, mock_validate): + request = InsertRequest(table="test_table", values=[{"column_name": ""}]) + + mock_api_response = Mock() + mock_parse_response.return_value = InsertResponse(inserted_fields=[{"skyflow_id": "id1"}]) + records_api = self.vault_client.get_records_api.return_value + records_api.with_raw_response.record_service_insert_record.return_value = mock_api_response + + self.vault.insert(request) + + records_api.with_raw_response.record_service_insert_record.assert_called_once() + @patch("skyflow.vault.controller._vault.validate_insert_request") @patch("skyflow.vault.controller._vault.parse_insert_response") def test_insert_with_continue_on_error_false_when_tokens_are_not_none(self, mock_parse_response, mock_validate): diff --git a/v2/tests/vault/data/__init__.py b/v2/tests/vault/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/vault/data/test_responses.py b/v2/tests/vault/data/test_responses.py similarity index 93% rename from tests/vault/data/test_responses.py rename to v2/tests/vault/data/test_responses.py index ea9f2be1..67616fbd 100644 --- a/tests/vault/data/test_responses.py +++ b/v2/tests/vault/data/test_responses.py @@ -1,4 +1,5 @@ import unittest +from common.vault.data import BaseInsertResponse from skyflow.vault.data._delete_response import DeleteResponse from skyflow.vault.data._file_upload_response import FileUploadResponse from skyflow.vault.data._get_response import GetResponse @@ -54,6 +55,10 @@ def test_empty_data_not_replaced(self): class TestInsertResponse(unittest.TestCase): + def test_is_a_base_insert_response(self): + r = InsertResponse(inserted_fields=[{"skyflow_id": "id1"}], errors=None) + self.assertIsInstance(r, BaseInsertResponse) + def test_repr(self): r = InsertResponse(inserted_fields=[{"skyflow_id": "id1"}], errors=None) self.assertIn("InsertResponse", repr(r)) diff --git a/v2/tests/vault/detect/__init__.py b/v2/tests/vault/detect/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/vault/detect/test_models.py b/v2/tests/vault/detect/test_models.py similarity index 100% rename from tests/vault/detect/test_models.py rename to v2/tests/vault/detect/test_models.py diff --git a/v2/tests/vault/tokens/__init__.py b/v2/tests/vault/tokens/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/vault/tokens/test_responses.py b/v2/tests/vault/tokens/test_responses.py similarity index 100% rename from tests/vault/tokens/test_responses.py rename to v2/tests/vault/tokens/test_responses.py