diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..28dd52d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run pre-commit hooks + run: | + pre-commit run --all-files + + - name: Run tests + run: | + pytest --cov=omni_python_sdk --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run Black + run: black --check . + + - name: Run isort + run: isort --check-only . + + - name: Run flake8 + run: flake8 + + - name: Run mypy + run: mypy omni_python_sdk/ + + build: + runs-on: ubuntu-latest + needs: [test, lint] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: dist/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index df4abbd..0c5d2b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,186 @@ +# Byte-compiled / optimized / DLL files __pycache__/ -*.pyc -*.pyo -*.pyd -*.swp -*.swo +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# .python-version + +# pipenv +#Pipfile.lock + +# poetry +#poetry.lock + +# pdm +#pdm.lock +.pdm.toml + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env .venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ .envrc + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm .idea/ + +# VS Code .vscode/ + +# macOS .DS_Store -build/ -dist/ -omni_python_sdk.egg-info/ \ No newline at end of file +.AppleDouble +.LSOverride +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.tmp +*.temp +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk + +# Editor files +*.swp +*.swo +*.pyc +*.pyo +*.pyd \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f149640 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,36 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-merge-conflict + - id: debug-statements + - id: check-json + + - repo: https://github.com/psf/black + rev: 23.12.1 + hooks: + - id: black + language_version: python3 + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + args: ["--profile", "black"] + + - repo: https://github.com/pycqa/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + additional_dependencies: [flake8-docstrings] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.8.0 + hooks: + - id: mypy + additional_dependencies: [types-requests] + args: [--strict] \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5a9a8cd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,169 @@ +# Claude AI Assistant Instructions + +This file contains instructions for AI assistants working on the omni-python-sdk project. + +## Project Overview + +This is a Python SDK for interacting with the Omni API. The SDK provides a comprehensive interface for: +- Running queries and retrieving data +- User and group management via SCIM API +- Document import/export operations +- Model and topic management +- Embed URL generation + +## Development Environment Setup + +### Prerequisites +- Python 3.9+ +- pip for package management + +### Installation +```bash +# Install development dependencies +pip install -e ".[dev]" + +# Install pre-commit hooks +pre-commit install +``` + +## Code Quality Tools + +### Linting and Formatting +- **Black**: Code formatting (line length: 88) +- **isort**: Import sorting with black profile +- **flake8**: Linting with E203, W503 ignored +- **mypy**: Type checking (strict mode enabled) + +### Running Quality Checks +```bash +# Format code +black . + +# Sort imports +isort . + +# Run linting +flake8 + +# Type checking +mypy omni_python_sdk/ + +# Run all pre-commit hooks +pre-commit run --all-files +``` + +### Testing +- **pytest**: Testing framework +- **pytest-cov**: Coverage reporting + +```bash +# Run all tests +make test + +# Run tests with coverage +make test-cov + +# Run example tests only +make test-examples + +# Run integration tests +make test-integration + +# Run fast tests (excluding integration) +make test-fast + +# Run specific test file +pytest tests/test_api.py -v +``` + +## Project Structure + +``` +omni-python-sdk/ +├── omni_python_sdk/ # Main package +│ ├── __init__.py +│ └── api.py # Core API client +├── tests/ # Unit tests +│ ├── __init__.py +│ ├── conftest.py # Test fixtures +│ ├── test_api.py # API tests +│ ├── test_examples.py # Example functionality tests +│ ├── data/ # Test data files +│ └── README.md # Test documentation +├── examples/ # Usage examples +├── pyproject.toml # Project configuration +├── .pre-commit-config.yaml # Pre-commit hooks +└── CLAUDE.md # This file +``` + +## Key Components + +### OmniAPI Class +The main API client class located in `omni_python_sdk/api.py`. Key methods include: +- `run_query_blocking()`: Execute queries and return PyArrow tables +- `create_user()`, `update_user()`, `find_user_by_email()`: User management +- `document_export()`, `document_import()`: Document operations +- `create_topic()`, `update_topic()`, `get_topic()`: Topic management + +### Authentication +The SDK supports multiple authentication methods: +- Direct API key and base URL parameters +- Environment variables (OMNI_API_KEY, OMNI_BASE_URL) +- .env file loading + +## Development Guidelines + +### Code Style +- Follow PEP 8 with Black formatting +- Use type hints for all function parameters and return values +- Add docstrings for all public methods +- Keep line length to 88 characters + +### Testing +- Write unit tests for all new functionality +- Use mocks for external API calls +- Maintain high test coverage (aim for >90%) +- Test both success and error scenarios + +### Error Handling +- Use the `@requests_error_handler` decorator for API calls +- Provide meaningful error messages +- Handle network timeouts and connection errors gracefully + +## Common Tasks + +### Adding New API Endpoints +1. Add method to OmniAPI class with proper type hints +2. Use `@requests_error_handler` decorator +3. Follow existing URL building patterns +4. Add comprehensive docstring +5. Write unit tests + +### Updating Dependencies +1. Update pyproject.toml +2. Test with new versions +3. Update any breaking changes in code +4. Run full test suite + +### Release Process +1. Update version in pyproject.toml +2. Run all quality checks +3. Ensure all tests pass +4. Update changelog/documentation +5. Create release via GitHub + +## Troubleshooting + +### Common Issues +- Import errors: Check Python path and installation +- API authentication: Verify API key and base URL +- Type checking errors: Ensure all imports have proper type stubs + +### Debug Mode +Set environment variable `DEBUG=1` for verbose logging during development. + +## Resources + +- [Omni API Documentation](https://docs.omni.co) +- [PyArrow Documentation](https://arrow.apache.org/docs/python/) +- [Python Type Hints](https://docs.python.org/3/library/typing.html) \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d5014ff --- /dev/null +++ b/Makefile @@ -0,0 +1,76 @@ +.PHONY: help install install-dev test test-cov lint format type-check clean build upload docs pre-commit + +help: ## Show this help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +install: ## Install package + pip install -e . + +install-dev: ## Install package with development dependencies + pip install -e ".[dev]" + +test: ## Run tests + pytest + +test-cov: ## Run tests with coverage + pytest --cov=omni_python_sdk --cov-report=term-missing --cov-report=html + +test-examples: ## Run example tests only + pytest tests/test_examples.py -v + +test-integration: ## Run integration tests + pytest -m integration -v + +test-fast: ## Run tests excluding integration tests + pytest -m "not integration" + +lint: ## Run linting + flake8 omni_python_sdk/ tests/ + +format: ## Format code + black . + isort . + +type-check: ## Run type checking + mypy omni_python_sdk/ + +quality: ## Run all quality checks + black --check . + isort --check-only . + flake8 omni_python_sdk/ tests/ + mypy omni_python_sdk/ + +pre-commit: ## Install pre-commit hooks + pre-commit install + +pre-commit-run: ## Run pre-commit hooks on all files + pre-commit run --all-files + +clean: ## Clean build artifacts + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info/ + rm -rf .pytest_cache/ + rm -rf .coverage + rm -rf htmlcov/ + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + +build: clean ## Build package + python -m build + +upload-test: build ## Upload to Test PyPI + twine check dist/* + twine upload --repository testpypi dist/* + +upload: build ## Upload to PyPI + twine check dist/* + twine upload dist/* + +docs: ## Generate documentation (placeholder) + @echo "Documentation generation not configured yet" + +dev-setup: install-dev pre-commit ## Set up development environment + @echo "Development environment setup complete!" + +.DEFAULT_GOAL := help \ No newline at end of file diff --git a/README.md b/README.md index 4d4285a..ef426d5 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,53 @@ -# omni-python-sdk +# Omni Python SDK -Python SDK for interacting with the Omni API +[![CI](https://github.com/exploreomni/omni-python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/exploreomni/omni-python-sdk/actions/workflows/ci.yml) +[![PyPI version](https://badge.fury.io/py/omni-python-sdk.svg)](https://badge.fury.io/py/omni-python-sdk) +[![Python Support](https://img.shields.io/pypi/pyversions/omni-python-sdk.svg)](https://pypi.org/project/omni-python-sdk/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +A comprehensive Python SDK for interacting with the Omni API. This library provides a simple and intuitive interface for querying data, managing users and groups, handling documents, and working with Omni's analytics platform. + +## Features + +- 🔍 **Query Execution**: Run queries and retrieve data as PyArrow tables or Pandas DataFrames +- 👥 **User Management**: Create, update, and manage users via SCIM API +- 📁 **Document Operations**: Import and export Omni documents +- 🏗️ **Model Management**: Create and manage data models and topics +- 🔗 **Embed URLs**: Generate secure embed URLs for dashboards +- 🛡️ **Type Safety**: Full type hints for better development experience +- ⚡ **Async Support**: Built with performance in mind ## Installation +Install from PyPI: + ```bash -pip install -r requirements.txt +pip install omni-python-sdk ``` -## Usage +For development with all optional dependencies: + +```bash +pip install omni-python-sdk[dev] +``` + +## Quick Start + +### Basic Usage + ```python from omni_python_sdk import OmniAPI -# Set your API key and base URL -api_key = "your_api_key" -base_url = "https://your_domain.omniapp.co" -#these can optionally be set in an .env file with the following keys: -# OMNI_API_KEY=<> -# OMNI_BASE_URL=<> +# Initialize with credentials +api = OmniAPI( + api_key="your_api_key", + base_url="https://your_domain.omniapp.co" +) + +# Or use environment variables (OMNI_API_KEY, OMNI_BASE_URL) +api = OmniAPI() -# Define your query +# Run a query query = { "query": { "sorts": [ @@ -38,29 +66,166 @@ query = { } } -# Initialize the API with your credentials -api = OmniAPI(api_key, base_url) -# if you've optionally set your keys in a .env file no arguments are required: -# api = OmniAPI() -# if your environment variables are stored in an alternative location -# api = OmniAPI(env_file='<>') +# Execute query and get results +table, fields = api.run_query_blocking(query) -# Run the query and get a table -table = api.run_query_blocking(query) - -# Convert the table to a Pandas DataFrame +# Convert to Pandas DataFrame df = table.to_pandas() - -# Display the first few rows of the DataFrame print(df.head()) ``` -To run the example, you need to replace `your_api_key`, `your_domain`, and `your_model_id` with your own values. +### Environment Configuration + +Create a `.env` file in your project root: + +```env +OMNI_API_KEY=your_api_key_here +OMNI_BASE_URL=https://your_domain.omniapp.co +``` + +### User Management + +```python +# Find user by email +user = api.return_user_by_email("user@example.com") + +# Create or update user +api.upsert_user( + email="newuser@example.com", + displayName="New User", + attributes={"department": "Engineering"} +) + +# Add user to group +api.add_user_to_group("Developers", user_id) +``` + +### Document Operations + +```python +# Export document +document_data = api.document_export("document_id") + +# Import document +api.document_import(document_data) +``` + +## Command Line Usage + +Run queries directly from the command line: + +```bash +python -m examples.query \ + YOUR_API_KEY \ + https://your-domain.omniapp.co \ + '{"query": {"table": "your_table", "fields": ["field1", "field2"], "modelId": "your_model_id"}}' +``` + +## API Reference -To get a query object, you can use the Inspector on a Omni Workbook. The query object is a JSON object that represents the query you want to run. You can find the Inspector in the View menu on a Workbook. Look for the "Query Structure" section. +### Core Classes -For a simple command line interface, you can run the following command: +- **`OmniAPI`**: Main client class for interacting with Omni API +- **`@requests_error_handler`**: Decorator for handling API errors gracefully +- **`@memoized`**: Decorator for caching expensive operations + +### Key Methods + +| Method | Description | +|--------|-------------| +| `run_query_blocking()` | Execute queries synchronously | +| `create_user()`, `update_user()` | User management operations | +| `document_export()`, `document_import()` | Document operations | +| `create_topic()`, `get_topic()` | Topic management | +| `generate_embed_url()` | Create secure embed URLs | + +## Development + +### Setup Development Environment ```bash -python3 examples/query.py OMNI_API_KEY https://OMNI_URL '{"query": {"sorts": [{"column_name": "omni_dbt__order_items.created_at[date]", "sort_descending": false}], "table": "omni_dbt__order_items", "fields": ["omni_dbt__order_items.created_at[date]", "omni_dbt__order_items.total_sale_price"], "modelId": "OMNI_MODEL_ID", "join_paths_from_topic_name": "order_items"}} +# Clone the repository +git clone https://github.com/exploreomni/omni-python-sdk.git +cd omni-python-sdk + +# Set up development environment +make dev-setup ``` + +### Code Quality + +This project uses several tools to maintain code quality: + +- **Black**: Code formatting +- **isort**: Import sorting +- **flake8**: Linting +- **mypy**: Type checking +- **pytest**: Testing + +```bash +# Run all quality checks +make quality + +# Format code +make format + +# Run tests with coverage +make test-cov +``` + +### Testing + +```bash +# Run tests +make test + +# Run tests with coverage report +make test-cov + +# Run specific test file +pytest tests/test_api.py -v +``` + +## Examples + +Explore the [`examples/`](examples/) directory for comprehensive usage examples: + +- **Basic Queries**: [`examples/query.py`](examples/query.py) +- **User Management**: [`examples/user_management/`](examples/user_management/) +- **Data Migration**: [`examples/content_migration.py`](examples/content_migration.py) +- **Databricks Integration**: [`examples/databricks_metric_view.py`](examples/databricks_metric_view.py) +- **Snowflake Integration**: [`examples/snowflake_semantic_view.py`](examples/snowflake_semantic_view.py) + +## Requirements + +- Python 3.9+ +- requests +- pyarrow +- python-dotenv +- pandas (for DataFrame conversion) + +## Contributing + +Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository. + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/amazing-feature`) +3. Make your changes and add tests +4. Run quality checks (`make quality`) +5. Commit your changes (`git commit -m 'Add amazing feature'`) +6. Push to the branch (`git push origin feature/amazing-feature`) +7. Open a Pull Request + +## License + +This project is licensed under the MIT License - see the [`LICENSE`](LICENSE) file for details. + +## Support + +- 📖 **Documentation**: [Omni API Docs](https://docs.omni.co) +- 🐛 **Issues**: [GitHub Issues](https://github.com/exploreomni/omni-python-sdk/issues) +- 💬 **Discussions**: [GitHub Discussions](https://github.com/exploreomni/omni-python-sdk/discussions) + +## Changelog + +See [`CHANGELOG.md`](CHANGELOG.md) for a detailed history of changes to this project. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..34fb158 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,154 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "omni-python-sdk" +version = "0.1.11" +description = "A Python SDK for Omni API" +readme = "README.md" +license = {file = "LICENSE"} +authors = [ + {name = "Jamie Davidson", email = "jamie@omni.co"} +] +keywords = ["omni", "api", "sdk"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">=3.9" +dependencies = [ + "requests", + "pyarrow", + "ndjson", + "python-dotenv", + "pandas", +] + +[project.optional-dependencies] +dev = [ + "black", + "isort", + "flake8", + "mypy", + "pytest", + "pytest-cov", + "pre-commit", +] +examples = [ + "matplotlib", + "statsmodels", +] + +[project.urls] +Homepage = "https://github.com/exploreomni/omni-python-sdk" +Repository = "https://github.com/exploreomni/omni-python-sdk" +Issues = "https://github.com/exploreomni/omni-python-sdk/issues" + +[tool.setuptools.packages.find] +include = ["omni_python_sdk*"] + +[tool.black] +line-length = 88 +target-version = ['py39'] +include = '\.pyi?$' +extend-exclude = ''' +/( + # directories + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' + +[tool.isort] +profile = "black" +multi_line_output = 3 +line_length = 88 +known_first_party = ["omni_python_sdk"] + +[tool.flake8] +max-line-length = 88 +extend-ignore = ["E203", "W503"] +exclude = [ + ".git", + "__pycache__", + "build", + "dist", + ".eggs", + "*.egg-info", + ".venv", + ".mypy_cache", +] + +[tool.mypy] +python_version = "3.9" +warn_return_any = true +warn_unused_configs = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +warn_unreachable = true +strict_equality = true + +[[tool.mypy.overrides]] +module = [ + "ndjson", + "pyarrow.*", +] +ignore_missing_imports = true + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q --strict-markers --cov=omni_python_sdk --cov-report=term-missing" +testpaths = [ + "tests", +] +pythonpath = [ + ".", +] +markers = [ + "integration: marks tests as integration tests (may be slow)", + "examples: marks tests that test example functionality", +] + +[tool.coverage.run] +source = ["omni_python_sdk"] +omit = [ + "*/tests/*", + "*/test_*", + "setup.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..6ed38d0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,125 @@ +# Tests + +This directory contains the test suite for the omni-python-sdk. + +## Test Structure + +``` +tests/ +├── conftest.py # Shared fixtures and configuration +├── test_api.py # Core API functionality tests +├── test_examples.py # Tests for example functionality +├── data/ # Test data files +│ ├── order_items.topic.json +│ ├── order_items.databricks_metric_view.yaml +│ ├── order_items.databricks_metric_view.sql +│ └── order_items.snowflake_semantic_view.sql +└── README.md # This file +``` + +## Test Categories + +### Unit Tests +- **`test_api.py`**: Tests for the core `OmniAPI` class and its methods +- **`test_examples.py`**: Tests for example functionality (topic, metric views, etc.) + +### Integration Tests +- Marked with `@pytest.mark.integration` +- Test complete workflows and interactions between components +- May be slower to run + +## Running Tests + +### All Tests +```bash +make test +``` + +### With Coverage +```bash +make test-cov +``` + +### Example Tests Only +```bash +make test-examples +``` + +### Integration Tests Only +```bash +make test-integration +``` + +### Fast Tests (excluding integration) +```bash +make test-fast +``` + +### Specific Test File +```bash +pytest tests/test_api.py -v +``` + +### Specific Test Method +```bash +pytest tests/test_api.py::TestOmniAPI::test_init_with_credentials -v +``` + +## Test Data + +Test data files are stored in the `tests/data/` directory and include: + +- **`order_items.topic.json`**: Sample topic data for testing conversions +- **`*.yaml`** and **`*.sql`**: Expected output files for validation + +## Writing Tests + +### Guidelines + +1. **Use pytest fixtures** for shared setup and teardown +2. **Mock external dependencies** (API calls, file I/O when appropriate) +3. **Test both success and failure scenarios** +4. **Use descriptive test names** that explain what is being tested +5. **Mark integration tests** with `@pytest.mark.integration` + +### Example Test Structure + +```python +import pytest +from unittest.mock import Mock, patch +from omni_python_sdk import OmniAPI + +class TestOmniAPI: + def test_method_success_case(self): + # Test successful execution + pass + + def test_method_error_handling(self): + # Test error scenarios + pass + + @pytest.mark.integration + def test_end_to_end_workflow(self): + # Test complete workflows + pass +``` + +### Fixtures + +Common fixtures are defined in `conftest.py`: + +- `mock_env_vars`: Mocks environment variables for testing +- `sample_query`: Provides sample query data + +## Coverage + +The test suite aims for high coverage of the core SDK functionality. Coverage reports are generated in HTML format in the `htmlcov/` directory when running `make test-cov`. + +## CI/CD Integration + +Tests are automatically run in GitHub Actions for: +- Multiple Python versions (3.9, 3.10, 3.11, 3.12) +- Code quality checks (linting, formatting, type checking) +- Coverage reporting + +The CI pipeline ensures all tests pass before merging pull requests. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d13db7b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +import pytest +import os +from unittest.mock import patch + +@pytest.fixture +def mock_env_vars(): + """Mock environment variables for testing.""" + with patch.dict(os.environ, { + 'OMNI_API_KEY': 'test_api_key', + 'OMNI_BASE_URL': 'https://test.omniapp.co' + }): + yield + +@pytest.fixture +def sample_query(): + """Sample query for testing.""" + return { + "query": { + "sorts": [ + { + "column_name": "order_items.created_at[date]", + "sort_descending": False + } + ], + "table": "order_items", + "fields": [ + "order_items.created_at[date]", + "order_items.sale_price_sum" + ], + "modelId": "test_model_id", + "join_paths_from_topic_name": "order_items" + } + } \ No newline at end of file diff --git a/tests/data/order_items.databricks_metric_view.sql b/tests/data/order_items.databricks_metric_view.sql new file mode 100644 index 0000000..3764d50 --- /dev/null +++ b/tests/data/order_items.databricks_metric_view.sql @@ -0,0 +1,46 @@ +CREATE OR REPLACE VIEW `OMNI__order_items` +(`order_items__created_at`, `order_items__delivered_at`, `order_items__id`, `order_items__inventory_item_id`, `order_items__margin`, `order_items__order_id`, `order_items__returned_at`, `order_items__sale_price`, `order_items__shipped_at`, `order_items__status`, `order_items__user_id`, `order_items__calculation`, `order_items__created_at_min`, `order_items__margin_sum`, `order_items__count`, `order_items__order_id_count_distinct`, `order_items__sale_price_sum`) +WITH METRICS +LANGUAGE YAML + +AS $$ +dimensions: +- expr: created_at + name: order_items__created_at +- expr: delivered_at + name: order_items__delivered_at +- expr: id + name: order_items__id +- expr: inventory_item_id + name: order_items__inventory_item_id +- expr: order_items__sale_price - products__cost + name: order_items__margin +- expr: order_id + name: order_items__order_id +- expr: returned_at + name: order_items__returned_at +- expr: sale_price + name: order_items__sale_price +- expr: shipped_at + name: order_items__shipped_at +- expr: status + name: order_items__status +- expr: user_id + name: order_items__user_id +measures: +- expr: Measure(order_items__sale_price_sum) / Measure(order_items__order_id_count_distinct) + name: order_items__calculation +- expr: MIN(order_items__created_at) + name: order_items__created_at_min +- expr: SUM(order_items__margin) + name: order_items__margin_sum +- expr: COUNT(*) + name: order_items__count +- expr: COUNT(DISTINCT order_items__order_id) + name: order_items__order_id_count_distinct +- expr: SUM(order_items__sale_price) + name: order_items__sale_price_sum +source: public.order_items +version: '0.1' + +$$ \ No newline at end of file diff --git a/tests/data/order_items.databricks_metric_view.yaml b/tests/data/order_items.databricks_metric_view.yaml new file mode 100644 index 0000000..5a20a05 --- /dev/null +++ b/tests/data/order_items.databricks_metric_view.yaml @@ -0,0 +1,38 @@ +dimensions: +- expr: created_at + name: order_items__created_at +- expr: delivered_at + name: order_items__delivered_at +- expr: id + name: order_items__id +- expr: inventory_item_id + name: order_items__inventory_item_id +- expr: order_items__sale_price - products__cost + name: order_items__margin +- expr: order_id + name: order_items__order_id +- expr: returned_at + name: order_items__returned_at +- expr: sale_price + name: order_items__sale_price +- expr: shipped_at + name: order_items__shipped_at +- expr: status + name: order_items__status +- expr: user_id + name: order_items__user_id +measures: +- expr: Measure(order_items__sale_price_sum) / Measure(order_items__order_id_count_distinct) + name: order_items__calculation +- expr: MIN(order_items__created_at) + name: order_items__created_at_min +- expr: SUM(order_items__margin) + name: order_items__margin_sum +- expr: COUNT(*) + name: order_items__count +- expr: COUNT(DISTINCT order_items__order_id) + name: order_items__order_id_count_distinct +- expr: SUM(order_items__sale_price) + name: order_items__sale_price_sum +source: public.order_items +version: '0.1' \ No newline at end of file diff --git a/tests/data/order_items.snowflake_semantic_view.sql b/tests/data/order_items.snowflake_semantic_view.sql new file mode 100644 index 0000000..77195bb --- /dev/null +++ b/tests/data/order_items.snowflake_semantic_view.sql @@ -0,0 +1,99 @@ +CREATE OR REPLACE SEMANTIC VIEW OMNI__order_items + +TABLES ( + distribution_centers AS public.distribution_centers + PRIMARY KEY (id), + first_order_facts AS public.first_order_facts + PRIMARY KEY (user_id), + inventory_items AS public.inventory_items + PRIMARY KEY (id), + order_items AS public.order_items + PRIMARY KEY (id), + products AS public.products + PRIMARY KEY (id), + users AS public.users + PRIMARY KEY (id) +) + +RELATIONSHIPS ( + order_items_users AS + order_items (user_id) REFERENCES users, + order_items_inventory_items AS + order_items (inventory_item_id) REFERENCES inventory_items, + inventory_items_products AS + inventory_items (product_id) REFERENCES products, + products_distribution_centers AS + products (distribution_center_id) REFERENCES distribution_centers, + order_items_first_order_facts AS + order_items (user_id) REFERENCES first_order_facts +) + +DIMENSIONS ( + distribution_centers.id AS distribution_centers.id, + distribution_centers.latitude AS distribution_centers.latitude, + distribution_centers.longitude AS distribution_centers.longitude, + distribution_centers.name AS distribution_centers.name, + first_order_facts.created_at_min AS first_order_facts.created_at_min, + first_order_facts.user_id AS first_order_facts.user_id, + inventory_items.cost AS inventory_items.cost, + inventory_items.created_at AS inventory_items.created_at, + inventory_items.id AS inventory_items.id, + inventory_items.product_brand AS inventory_items.product_brand, + inventory_items.product_category AS inventory_items.product_category, + inventory_items.product_department AS inventory_items.product_department, + inventory_items.product_distribution_center_id AS inventory_items.product_distribution_center_id, + inventory_items.product_id AS inventory_items.product_id, + inventory_items.product_name AS inventory_items.product_name, + inventory_items.product_retail_price AS inventory_items.product_retail_price, + inventory_items.product_sku AS inventory_items.product_sku, + inventory_items.sold_at AS inventory_items.sold_at, + order_items.created_at AS order_items.created_at, + order_items.delivered_at AS order_items.delivered_at, + order_items.id AS order_items.id, + order_items.inventory_item_id AS order_items.inventory_item_id, + order_items.margin AS order_items.sale_price - products.cost, + order_items.order_id AS order_items.order_id, + order_items.returned_at AS order_items.returned_at, + order_items.sale_price AS order_items.sale_price, + order_items.shipped_at AS order_items.shipped_at, + order_items.status AS status, + order_items.user_id AS order_items.user_id, + products.brand AS products.brand, + products.category AS products.category, + products.cost AS products.cost, + products.department AS products.department, + products.distribution_center_id AS products.distribution_center_id, + products.id AS products.id, + products.name AS products.name, + products.retail_price AS products.retail_price, + products.sku AS products.sku, + users.age AS users.age, + users.city AS users.city, + users.country AS country, + users.created_at AS users.created_at, + users.email AS users.email, + users.first_name AS users.first_name, + users.gender AS users.gender, + users.id AS users.id, + users.last_name AS users.last_name, + users.latitude AS users.latitude, + users.longitude AS users.longitude, + users.state AS users.state, + users.traffic_source AS users.traffic_source, + users.zip AS users.zip +) + +METRICS ( + distribution_centers.count AS COUNT(*), + first_order_facts.count AS COUNT(*), + inventory_items.count AS COUNT(*), + order_items.calculation AS order_items.sale_price_sum / order_items.order_id_count_distinct, + order_items.created_at_min AS MIN(order_items.created_at), + order_items.margin_sum AS SUM(order_items.margin), + order_items.count AS COUNT(*), + order_items.order_id_count_distinct AS COUNT(DISTINCT order_items.order_id), + order_items.sale_price_sum AS SUM(order_items.sale_price), + products.cost_sum AS SUM(products.cost), + products.count AS COUNT(*), + users.count AS COUNT(*) +) \ No newline at end of file diff --git a/tests/data/order_items.topic.json b/tests/data/order_items.topic.json new file mode 100644 index 0000000..7e47625 --- /dev/null +++ b/tests/data/order_items.topic.json @@ -0,0 +1 @@ +{"name": "order_items", "base_view_name": "order_items", "label": "Order Items", "default_filters": {}, "join_via_map": {"users": [], "inventory_items": [], "products": ["inventory_items"], "distribution_centers": ["inventory_items", "products"], "first_order_facts": []}, "join_via_map_key_order": ["users", "inventory_items", "products", "distribution_centers", "first_order_facts"], "ignored_props": [], "always_where_filters": {}, "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "has_frozen_join_via_map": true, "relationships": [{"left_view_name": "order_items", "right_view_name": "users", "join_type": "ALWAYS_LEFT", "on": {"type": "call", "operator": "SqlStdOperatorTable.EQUALS", "operands": [{"type": "field", "field_name": "order_items.user_id"}, {"type": "field", "field_name": "users.id"}], "distinct": false}, "sql": "${order_items.user_id} = ${users.id}", "id": "order_items_users", "type": "ASSUMED_MANY_TO_ONE", "ignored": false, "bidirectional": false, "original_on_sql": "${order_items.user_id} = ${users.id}"}, {"left_view_name": "order_items", "right_view_name": "inventory_items", "join_type": "ALWAYS_LEFT", "on": {"type": "call", "operator": "SqlStdOperatorTable.EQUALS", "operands": [{"type": "field", "field_name": "order_items.inventory_item_id"}, {"type": "field", "field_name": "inventory_items.id"}], "distinct": false}, "sql": "${order_items.inventory_item_id} = ${inventory_items.id}", "id": "order_items_inventory_items", "type": "ASSUMED_MANY_TO_ONE", "ignored": false, "bidirectional": false, "original_on_sql": "${order_items.inventory_item_id} = ${inventory_items.id}"}, {"left_view_name": "inventory_items", "right_view_name": "products", "join_type": "ALWAYS_LEFT", "on": {"type": "call", "operator": "SqlStdOperatorTable.EQUALS", "operands": [{"type": "field", "field_name": "inventory_items.product_id"}, {"type": "field", "field_name": "products.id"}], "distinct": false}, "sql": "${inventory_items.product_id} = ${products.id}", "id": "inventory_items_products", "type": "ASSUMED_MANY_TO_ONE", "ignored": false, "bidirectional": false, "original_on_sql": "${inventory_items.product_id} = ${products.id}"}, {"left_view_name": "products", "right_view_name": "distribution_centers", "join_type": "ALWAYS_LEFT", "on": {"type": "call", "operator": "SqlStdOperatorTable.EQUALS", "operands": [{"type": "field", "field_name": "products.distribution_center_id"}, {"type": "field", "field_name": "distribution_centers.id"}], "distinct": false}, "sql": "${products.distribution_center_id} = ${distribution_centers.id}", "id": "products_distribution_centers", "type": "ASSUMED_MANY_TO_ONE", "ignored": false, "bidirectional": false, "original_on_sql": "${products.distribution_center_id} = ${distribution_centers.id}"}, {"left_view_name": "order_items", "right_view_name": "first_order_facts", "join_type": "ALWAYS_LEFT", "on": {"type": "call", "operator": "SqlStdOperatorTable.EQUALS", "operands": [{"type": "field", "field_name": "order_items.user_id"}, {"type": "field", "field_name": "first_order_facts.user_id"}], "distinct": false}, "sql": "${order_items.user_id} = ${first_order_facts.user_id}", "id": "order_items_first_order_facts", "type": "MANY_TO_ONE", "ignored": false, "bidirectional": false, "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc"}], "ide_file_name": "order_items.topic", "sample_queries": [{"query": {"table": "order_items", "fields": ["order_items.created_at[month]", "order_items.order_id_count_distinct"], "calculations": [], "filters": {}, "sorts": [{"column_name": "order_items.created_at[month]", "sort_descending": false, "is_column_sort": false, "null_sort": "OMNI_DEFAULT"}], "limit": 1000, "pivots": [], "fill_fields": [], "column_totals": {}, "row_totals": {}, "default_group_by": true, "join_via_map": {}, "join_paths_from_topic_name": "order_items"}, "label": "First Monthly Orders"}, {"query": {"table": "order_items", "fields": ["order_items.status", "order_items.sale_price_sum"], "calculations": [], "filters": {}, "sorts": [{"column_name": "order_items.sale_price_sum", "sort_descending": true, "is_column_sort": false, "null_sort": "OMNI_DEFAULT"}], "limit": 1000, "pivots": [], "fill_fields": [], "column_totals": {}, "row_totals": {}, "default_group_by": true, "join_via_map": {}, "join_paths_from_topic_name": "order_items"}, "label": "Revenue Status 2"}], "views": [{"name": "distribution_centers", "dimensions": [{"field_name": "id", "view_name": "distribution_centers", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "distribution_centers.id"}, {"field_name": "latitude", "view_name": "distribution_centers", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "distribution_centers.latitude"}, {"field_name": "longitude", "view_name": "distribution_centers", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "distribution_centers.longitude"}, {"field_name": "name", "view_name": "distribution_centers", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "distribution_centers.name"}], "measures": [{"type": "aggregation", "field_name": "count", "view_name": "distribution_centers", "aggregate_type": "COUNT", "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Distribution Centers Count", "format": "NUMBER_0", "display_sql": "COUNT(*)", "fully_qualified_name": "distribution_centers.count"}], "primary_key": [{"type": "field", "field_name": "distribution_centers.id"}], "schema": "public", "schema_label": "Public", "label": "Distribution Centers", "ide_file_name": "public/distribution_centers.view", "filter_only_fields": [], "is_pseudo_display_view": false, "yaml_path": "distribution_centers.view"}, {"name": "first_order_facts", "dimensions": [{"field_name": "created_at_min", "view_name": "first_order_facts", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "parent_field": "created_at_min", "parent_label": "Created At Min", "view_label": "First Order Facts", "group_label": "Created At Min", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "first_order_facts.created_at_min"}, {"field_name": "created_at_min[date]", "view_name": "first_order_facts", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "parent_field": "created_at_min", "parent_label": "Created At Min", "view_label": "First Order Facts", "group_label": "Created At Min", "is_dimension": true, "fully_qualified_name": "first_order_facts.created_at_min[date]"}, {"field_name": "created_at_min[week]", "view_name": "first_order_facts", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["first_order_facts.created_at_min[date]"], "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "parent_field": "created_at_min", "parent_label": "Created At Min", "view_label": "First Order Facts", "group_label": "Created At Min", "is_dimension": true, "fully_qualified_name": "first_order_facts.created_at_min[week]"}, {"field_name": "created_at_min[month]", "view_name": "first_order_facts", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["first_order_facts.created_at_min[date]", "first_order_facts.created_at_min[week]"], "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "parent_field": "created_at_min", "parent_label": "Created At Min", "view_label": "First Order Facts", "group_label": "Created At Min", "is_dimension": true, "fully_qualified_name": "first_order_facts.created_at_min[month]"}, {"field_name": "created_at_min[quarter]", "view_name": "first_order_facts", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["first_order_facts.created_at_min[date]", "first_order_facts.created_at_min[week]", "first_order_facts.created_at_min[month]"], "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "parent_field": "created_at_min", "parent_label": "Created At Min", "view_label": "First Order Facts", "group_label": "Created At Min", "is_dimension": true, "fully_qualified_name": "first_order_facts.created_at_min[quarter]"}, {"field_name": "created_at_min[year]", "view_name": "first_order_facts", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["first_order_facts.created_at_min[date]", "first_order_facts.created_at_min[week]", "first_order_facts.created_at_min[month]", "first_order_facts.created_at_min[quarter]"], "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "parent_field": "created_at_min", "parent_label": "Created At Min", "view_label": "First Order Facts", "group_label": "Created At Min", "is_dimension": true, "fully_qualified_name": "first_order_facts.created_at_min[year]"}, {"field_name": "user_id", "view_name": "first_order_facts", "data_type": "NUMBER", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "view_label": "First Order Facts", "is_dimension": true, "fully_qualified_name": "first_order_facts.user_id"}], "measures": [{"type": "aggregation", "field_name": "count", "view_name": "first_order_facts", "aggregate_type": "COUNT", "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "First Order Facts Count", "format": "NUMBER_0", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "view_label": "First Order Facts", "display_sql": "COUNT(*)", "fully_qualified_name": "first_order_facts.count"}], "query": {"table": "order_items", "fields": ["order_items.user_id", "order_items.created_at_min"], "calculations": [], "filters": {}, "sorts": [{"column_name": "order_items.created_at_min", "sort_descending": true, "is_column_sort": false, "null_sort": "OMNI_DEFAULT"}], "pivots": [], "fill_fields": [], "column_totals": {}, "row_totals": {}, "default_group_by": true, "join_via_map": {}, "join_paths_from_topic_name": "order_items"}, "primary_key": [{"type": "field", "field_name": "first_order_facts.user_id"}], "schema": "public", "schema_label": "Public", "label": "First Order Facts", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "ide_file_name": "public/first_order_facts.query.view", "filter_only_fields": [], "is_pseudo_display_view": false, "yaml_path": "first_order_facts.view"}, {"name": "inventory_items", "dimensions": [{"field_name": "cost", "view_name": "inventory_items", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "inventory_items.cost"}, {"field_name": "created_at", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "inventory_items.created_at"}, {"field_name": "created_at[date]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "inventory_items.created_at[date]"}, {"field_name": "created_at[week]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["inventory_items.created_at[date]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "inventory_items.created_at[week]"}, {"field_name": "created_at[month]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["inventory_items.created_at[date]", "inventory_items.created_at[week]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "inventory_items.created_at[month]"}, {"field_name": "created_at[quarter]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["inventory_items.created_at[date]", "inventory_items.created_at[week]", "inventory_items.created_at[month]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "inventory_items.created_at[quarter]"}, {"field_name": "created_at[year]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["inventory_items.created_at[date]", "inventory_items.created_at[week]", "inventory_items.created_at[month]", "inventory_items.created_at[quarter]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "inventory_items.created_at[year]"}, {"field_name": "id", "view_name": "inventory_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "inventory_items.id"}, {"field_name": "product_brand", "view_name": "inventory_items", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "inventory_items.product_brand"}, {"field_name": "product_category", "view_name": "inventory_items", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "inventory_items.product_category"}, {"field_name": "product_department", "view_name": "inventory_items", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "inventory_items.product_department"}, {"field_name": "product_distribution_center_id", "view_name": "inventory_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "inventory_items.product_distribution_center_id"}, {"field_name": "product_id", "view_name": "inventory_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "inventory_items.product_id"}, {"field_name": "product_name", "view_name": "inventory_items", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "inventory_items.product_name"}, {"field_name": "product_retail_price", "view_name": "inventory_items", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "inventory_items.product_retail_price"}, {"field_name": "product_sku", "view_name": "inventory_items", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "inventory_items.product_sku"}, {"field_name": "sold_at", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "sold_at", "parent_label": "Sold At", "group_label": "Sold At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "inventory_items.sold_at"}, {"field_name": "sold_at[date]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "sold_at", "parent_label": "Sold At", "group_label": "Sold At", "is_dimension": true, "fully_qualified_name": "inventory_items.sold_at[date]"}, {"field_name": "sold_at[week]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["inventory_items.sold_at[date]"], "parent_field": "sold_at", "parent_label": "Sold At", "group_label": "Sold At", "is_dimension": true, "fully_qualified_name": "inventory_items.sold_at[week]"}, {"field_name": "sold_at[month]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["inventory_items.sold_at[date]", "inventory_items.sold_at[week]"], "parent_field": "sold_at", "parent_label": "Sold At", "group_label": "Sold At", "is_dimension": true, "fully_qualified_name": "inventory_items.sold_at[month]"}, {"field_name": "sold_at[quarter]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["inventory_items.sold_at[date]", "inventory_items.sold_at[week]", "inventory_items.sold_at[month]"], "parent_field": "sold_at", "parent_label": "Sold At", "group_label": "Sold At", "is_dimension": true, "fully_qualified_name": "inventory_items.sold_at[quarter]"}, {"field_name": "sold_at[year]", "view_name": "inventory_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["inventory_items.sold_at[date]", "inventory_items.sold_at[week]", "inventory_items.sold_at[month]", "inventory_items.sold_at[quarter]"], "parent_field": "sold_at", "parent_label": "Sold At", "group_label": "Sold At", "is_dimension": true, "fully_qualified_name": "inventory_items.sold_at[year]"}], "measures": [{"type": "aggregation", "field_name": "count", "view_name": "inventory_items", "aggregate_type": "COUNT", "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Inventory Items Count", "format": "NUMBER_0", "display_sql": "COUNT(*)", "fully_qualified_name": "inventory_items.count"}], "primary_key": [{"type": "field", "field_name": "inventory_items.id"}], "schema": "public", "schema_label": "Public", "label": "Inventory Items", "ide_file_name": "public/inventory_items.view", "filter_only_fields": [], "is_pseudo_display_view": false, "yaml_path": "inventory_items.view"}, {"name": "order_items", "dimensions": [{"field_name": "created_at", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "order_items.created_at"}, {"field_name": "created_at[date]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "order_items.created_at[date]"}, {"field_name": "created_at[week]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["order_items.created_at[date]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "order_items.created_at[week]"}, {"field_name": "created_at[month]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["order_items.created_at[date]", "order_items.created_at[week]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "order_items.created_at[month]"}, {"field_name": "created_at[quarter]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["order_items.created_at[date]", "order_items.created_at[week]", "order_items.created_at[month]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "order_items.created_at[quarter]"}, {"field_name": "created_at[year]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["order_items.created_at[date]", "order_items.created_at[week]", "order_items.created_at[month]", "order_items.created_at[quarter]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "order_items.created_at[year]"}, {"field_name": "delivered_at", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "delivered_at", "parent_label": "Delivered At", "group_label": "Delivered At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "order_items.delivered_at"}, {"field_name": "delivered_at[date]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "delivered_at", "parent_label": "Delivered At", "group_label": "Delivered At", "is_dimension": true, "fully_qualified_name": "order_items.delivered_at[date]"}, {"field_name": "delivered_at[week]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["order_items.delivered_at[date]"], "parent_field": "delivered_at", "parent_label": "Delivered At", "group_label": "Delivered At", "is_dimension": true, "fully_qualified_name": "order_items.delivered_at[week]"}, {"field_name": "delivered_at[month]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["order_items.delivered_at[date]", "order_items.delivered_at[week]"], "parent_field": "delivered_at", "parent_label": "Delivered At", "group_label": "Delivered At", "is_dimension": true, "fully_qualified_name": "order_items.delivered_at[month]"}, {"field_name": "delivered_at[quarter]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["order_items.delivered_at[date]", "order_items.delivered_at[week]", "order_items.delivered_at[month]"], "parent_field": "delivered_at", "parent_label": "Delivered At", "group_label": "Delivered At", "is_dimension": true, "fully_qualified_name": "order_items.delivered_at[quarter]"}, {"field_name": "delivered_at[year]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["order_items.delivered_at[date]", "order_items.delivered_at[week]", "order_items.delivered_at[month]", "order_items.delivered_at[quarter]"], "parent_field": "delivered_at", "parent_label": "Delivered At", "group_label": "Delivered At", "is_dimension": true, "fully_qualified_name": "order_items.delivered_at[year]"}, {"field_name": "id", "view_name": "order_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "order_items.id"}, {"field_name": "inventory_item_id", "view_name": "order_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "order_items.inventory_item_id"}, {"field_name": "margin", "view_name": "order_items", "data_type": "NUMBER", "expr": {"type": "call", "operator": "SqlStdOperatorTable.MINUS", "operands": [{"type": "field", "field_name": "order_items.sale_price"}, {"type": "field", "field_name": "products.cost"}], "distinct": false}, "sql": "${order_items.sale_price} - ${products.cost}", "label": "Margin", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "is_dimension": true, "fully_qualified_name": "order_items.margin"}, {"field_name": "order_id", "view_name": "order_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "order_items.order_id"}, {"field_name": "returned_at", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "returned_at", "parent_label": "Returned At", "group_label": "Returned At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "order_items.returned_at"}, {"field_name": "returned_at[date]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "returned_at", "parent_label": "Returned At", "group_label": "Returned At", "is_dimension": true, "fully_qualified_name": "order_items.returned_at[date]"}, {"field_name": "returned_at[week]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["order_items.returned_at[date]"], "parent_field": "returned_at", "parent_label": "Returned At", "group_label": "Returned At", "is_dimension": true, "fully_qualified_name": "order_items.returned_at[week]"}, {"field_name": "returned_at[month]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["order_items.returned_at[date]", "order_items.returned_at[week]"], "parent_field": "returned_at", "parent_label": "Returned At", "group_label": "Returned At", "is_dimension": true, "fully_qualified_name": "order_items.returned_at[month]"}, {"field_name": "returned_at[quarter]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["order_items.returned_at[date]", "order_items.returned_at[week]", "order_items.returned_at[month]"], "parent_field": "returned_at", "parent_label": "Returned At", "group_label": "Returned At", "is_dimension": true, "fully_qualified_name": "order_items.returned_at[quarter]"}, {"field_name": "returned_at[year]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["order_items.returned_at[date]", "order_items.returned_at[week]", "order_items.returned_at[month]", "order_items.returned_at[quarter]"], "parent_field": "returned_at", "parent_label": "Returned At", "group_label": "Returned At", "is_dimension": true, "fully_qualified_name": "order_items.returned_at[year]"}, {"field_name": "sale_price", "view_name": "order_items", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "order_items.sale_price"}, {"field_name": "shipped_at", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "shipped_at", "parent_label": "Shipped At", "group_label": "Shipped At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "order_items.shipped_at"}, {"field_name": "shipped_at[date]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "shipped_at", "parent_label": "Shipped At", "group_label": "Shipped At", "is_dimension": true, "fully_qualified_name": "order_items.shipped_at[date]"}, {"field_name": "shipped_at[week]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["order_items.shipped_at[date]"], "parent_field": "shipped_at", "parent_label": "Shipped At", "group_label": "Shipped At", "is_dimension": true, "fully_qualified_name": "order_items.shipped_at[week]"}, {"field_name": "shipped_at[month]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["order_items.shipped_at[date]", "order_items.shipped_at[week]"], "parent_field": "shipped_at", "parent_label": "Shipped At", "group_label": "Shipped At", "is_dimension": true, "fully_qualified_name": "order_items.shipped_at[month]"}, {"field_name": "shipped_at[quarter]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["order_items.shipped_at[date]", "order_items.shipped_at[week]", "order_items.shipped_at[month]"], "parent_field": "shipped_at", "parent_label": "Shipped At", "group_label": "Shipped At", "is_dimension": true, "fully_qualified_name": "order_items.shipped_at[quarter]"}, {"field_name": "shipped_at[year]", "view_name": "order_items", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["order_items.shipped_at[date]", "order_items.shipped_at[week]", "order_items.shipped_at[month]", "order_items.shipped_at[quarter]"], "parent_field": "shipped_at", "parent_label": "Shipped At", "group_label": "Shipped At", "is_dimension": true, "fully_qualified_name": "order_items.shipped_at[year]"}, {"field_name": "status", "view_name": "order_items", "data_type": "STRING", "expr": {"type": "reference", "column_ref": ["status"], "is_default_alias_scoped": true}, "sql": "status", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "all_values": ["Complete", "Cancelled", "Returned", "Shipped", "Processing"], "is_dimension": true, "fully_qualified_name": "order_items.status"}, {"field_name": "user_id", "view_name": "order_items", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "order_items.user_id"}], "measures": [{"type": "metric", "field_name": "calculation", "view_name": "order_items", "expr": {"type": "call", "operator": "SqlStdOperatorTable.DIVIDE", "operands": [{"type": "field", "field_name": "order_items.sale_price_sum"}, {"type": "field", "field_name": "order_items.order_id_count_distinct"}], "distinct": false}, "data_type": "NUMBER", "sql": "${order_items.sale_price_sum} / ${order_items.order_id_count_distinct}", "ignored": false, "label": "Calculation", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "fully_qualified_name": "order_items.calculation"}, {"type": "aggregation", "field_name": "created_at_min", "view_name": "order_items", "aggregate_type": "MIN", "expr": {"type": "field", "field_name": "order_items.created_at"}, "filters": {}, "data_type": "TIMESTAMP", "ignored": false, "label": "First Order Date", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "display_sql": "MIN(${order_items.created_at})", "original_sql_for_aggregation": "${order_items.created_at}", "fully_qualified_name": "order_items.created_at_min"}, {"type": "aggregation", "field_name": "margin_sum", "view_name": "order_items", "aggregate_type": "SUM", "expr": {"type": "field", "field_name": "order_items.margin"}, "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Margin Sum", "format": "USDCURRENCY_2", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "display_sql": "SUM(${order_items.margin})", "original_sql_for_aggregation": "${order_items.margin}", "fully_qualified_name": "order_items.margin_sum"}, {"type": "aggregation", "field_name": "count", "view_name": "order_items", "aggregate_type": "COUNT", "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Order Items Count", "format": "NUMBER_0", "display_sql": "COUNT(*)", "fully_qualified_name": "order_items.count"}, {"type": "aggregation", "field_name": "order_id_count_distinct", "view_name": "order_items", "aggregate_type": "COUNT_DISTINCT", "expr": {"type": "field", "field_name": "order_items.order_id"}, "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Orders", "format": "ID", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "display_sql": "COUNT(DISTINCT ${order_items.order_id})", "original_sql_for_aggregation": "${order_items.order_id}", "fully_qualified_name": "order_items.order_id_count_distinct"}, {"type": "aggregation", "field_name": "sale_price_sum", "view_name": "order_items", "aggregate_type": "SUM", "expr": {"type": "field", "field_name": "order_items.sale_price"}, "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Revenue", "format": "USDCURRENCY_2", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "display_sql": "SUM(${order_items.sale_price})", "original_sql_for_aggregation": "${order_items.sale_price}", "fully_qualified_name": "order_items.sale_price_sum"}], "primary_key": [{"type": "field", "field_name": "order_items.id"}], "schema": "public", "schema_label": "Public", "label": "Order Items", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "ide_file_name": "public/order_items.view", "filter_only_fields": [], "is_pseudo_display_view": false, "yaml_path": "order_items.view"}, {"name": "products", "dimensions": [{"field_name": "brand", "view_name": "products", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "products.brand"}, {"field_name": "category", "view_name": "products", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "products.category"}, {"field_name": "cost", "view_name": "products", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "products.cost"}, {"field_name": "department", "view_name": "products", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "products.department"}, {"field_name": "distribution_center_id", "view_name": "products", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "products.distribution_center_id"}, {"field_name": "id", "view_name": "products", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "products.id"}, {"field_name": "name", "view_name": "products", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "products.name"}, {"field_name": "retail_price", "view_name": "products", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "products.retail_price"}, {"field_name": "sku", "view_name": "products", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "products.sku"}], "measures": [{"type": "aggregation", "field_name": "cost_sum", "view_name": "products", "aggregate_type": "SUM", "expr": {"type": "field", "field_name": "products.cost"}, "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Cost Sum", "format": "USDCURRENCY_2", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "display_sql": "SUM(${products.cost})", "original_sql_for_aggregation": "${products.cost}", "fully_qualified_name": "products.cost_sum"}, {"type": "aggregation", "field_name": "count", "view_name": "products", "aggregate_type": "COUNT", "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Products Count", "format": "NUMBER_0", "display_sql": "COUNT(*)", "fully_qualified_name": "products.count"}], "primary_key": [{"type": "field", "field_name": "products.id"}], "schema": "public", "schema_label": "Public", "label": "Products", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "ide_file_name": "public/products.view", "filter_only_fields": [], "is_pseudo_display_view": false, "yaml_path": "products.view"}, {"name": "users", "dimensions": [{"field_name": "age", "view_name": "users", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "users.age"}, {"field_name": "city", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.city"}, {"field_name": "country", "view_name": "users", "data_type": "STRING", "expr": {"type": "reference", "column_ref": ["country"], "is_default_alias_scoped": true}, "sql": "country", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "all_values": ["USA", "UK"], "is_dimension": true, "fully_qualified_name": "users.country"}, {"field_name": "created_at", "view_name": "users", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "is_group_parent_field": true, "fully_qualified_name": "users.created_at"}, {"field_name": "created_at[date]", "view_name": "users", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "DATE", "label": "Date", "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "users.created_at[date]"}, {"field_name": "created_at[week]", "view_name": "users", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "WEEK", "label": "Week", "drill_fields": ["users.created_at[date]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "users.created_at[week]"}, {"field_name": "created_at[month]", "view_name": "users", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "MONTH", "label": "Month", "drill_fields": ["users.created_at[date]", "users.created_at[week]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "users.created_at[month]"}, {"field_name": "created_at[quarter]", "view_name": "users", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "QUARTER", "label": "Quarter", "drill_fields": ["users.created_at[date]", "users.created_at[week]", "users.created_at[month]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "users.created_at[quarter]"}, {"field_name": "created_at[year]", "view_name": "users", "data_type": "TIMESTAMP", "time_frames": [null, "DATE", "WEEK", "MONTH", "QUARTER", "YEAR"], "date_type": "YEAR", "label": "Year", "drill_fields": ["users.created_at[date]", "users.created_at[week]", "users.created_at[month]", "users.created_at[quarter]"], "parent_field": "created_at", "parent_label": "Created At", "group_label": "Created At", "is_dimension": true, "fully_qualified_name": "users.created_at[year]"}, {"field_name": "email", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.email"}, {"field_name": "first_name", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.first_name"}, {"field_name": "gender", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.gender"}, {"field_name": "id", "view_name": "users", "data_type": "NUMBER", "format": "ID", "is_dimension": true, "fully_qualified_name": "users.id"}, {"field_name": "last_name", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.last_name"}, {"field_name": "latitude", "view_name": "users", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "users.latitude"}, {"field_name": "longitude", "view_name": "users", "data_type": "NUMBER", "is_dimension": true, "fully_qualified_name": "users.longitude"}, {"field_name": "state", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.state"}, {"field_name": "traffic_source", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.traffic_source"}, {"field_name": "zip", "view_name": "users", "data_type": "STRING", "is_dimension": true, "fully_qualified_name": "users.zip"}], "measures": [{"type": "aggregation", "field_name": "count", "view_name": "users", "aggregate_type": "COUNT", "filters": {}, "data_type": "NUMBER", "ignored": false, "label": "Users Count", "format": "NUMBER_0", "display_sql": "COUNT(*)", "fully_qualified_name": "users.count"}], "primary_key": [{"type": "field", "field_name": "users.id"}], "schema": "public", "schema_label": "Public", "label": "Users", "extension_model_id": "e87ab418-eec4-45d4-877f-cbd88d6c10dc", "ide_file_name": "public/users.view", "filter_only_fields": [], "is_pseudo_display_view": false, "yaml_path": "users.view"}]} \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..5fd0c5f --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,88 @@ +import pytest +from unittest.mock import Mock, patch, MagicMock +import requests +import json +from omni_python_sdk.api import OmniAPI, requests_error_handler + +class TestOmniAPI: + def test_init_with_credentials(self): + api = OmniAPI(api_key="test_key", base_url="https://test.omniapp.co") + assert api.api_key == "test_key" + assert api.base_url == "https://test.omniapp.co" + assert api.headers["Authorization"] == "Bearer test_key" + + def test_init_with_env_vars(self, mock_env_vars): + api = OmniAPI() + assert api.api_key == "test_api_key" + assert api.base_url == "https://test.omniapp.co" + + def test_trim_base_url(self): + api = OmniAPI(api_key="test", base_url="https://test.omniapp.co/api/v1/") + assert api.base_url == "https://test.omniapp.co" + + @patch('requests.post') + def test_run_query_blocking_success(self, mock_post, sample_query): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.text = '{"result": "dGVzdA=="}\n{"timed_out": "true", "summary": {"fields": []}}' + mock_post.return_value = mock_response + + api = OmniAPI(api_key="test", base_url="https://test.omniapp.co") + + with patch('base64.b64decode') as mock_b64: + with patch('io.BytesIO') as mock_io: + with patch('pyarrow.ipc.open_stream') as mock_arrow: + mock_table = Mock() + mock_reader = Mock() + mock_reader.read_all.return_value = mock_table + mock_arrow.return_value = mock_reader + + result = api.run_query_blocking(sample_query) + assert result is not None + + @patch('requests.get') + def test_find_user_by_email_success(self, mock_get): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "Resources": [ + {"id": "123", "userName": "test@example.com", "displayName": "Test User"} + ] + } + mock_get.return_value = mock_response + + api = OmniAPI(api_key="test", base_url="https://test.omniapp.co") + response = api.find_user_by_email("test@example.com") + + assert response.status_code == 200 + mock_get.assert_called_once() + + def test_requests_error_handler_decorator(self): + @requests_error_handler + def failing_function(): + raise Exception("Test error") + + result = failing_function() + assert result is None + +class TestMemoized: + def test_memoization(self): + from omni_python_sdk.api import memoized + + call_count = 0 + + @memoized + def test_func(x): + nonlocal call_count + call_count += 1 + return x * 2 + + # First call + result1 = test_func(5) + assert result1 == 10 + assert call_count == 1 + + # Second call with same args should use cache + result2 = test_func(5) + assert result2 == 10 + assert call_count == 1 # Should not increment \ No newline at end of file diff --git a/tests/test_examples.py b/tests/test_examples.py new file mode 100644 index 0000000..4b6264f --- /dev/null +++ b/tests/test_examples.py @@ -0,0 +1,197 @@ +import json +import pytest +from pathlib import Path + +# Import example modules +try: + from examples.topic import Topic + from examples.databricks_metric_view import metric_view_from_topic + from examples.snowflake_semantic_view import sematic_view_from_topic +except ImportError: + pytest.skip("Examples modules not available", allow_module_level=True) + + +class TestTopicExamples: + """Test cases for topic functionality from examples.""" + + @pytest.fixture + def topic_data(self): + """Load topic test data.""" + data_path = Path(__file__).parent / "data" / "order_items.topic.json" + with open(data_path, "r", encoding="utf-8") as f: + return json.load(f) + + @pytest.fixture + def topic(self, topic_data): + """Create Topic instance from test data.""" + return Topic.model_validate(topic_data) + + def test_topic_json_loading(self, topic_data): + """Test that topic JSON data loads as dictionary.""" + assert isinstance(topic_data, dict) + + def test_topic_model_validation(self, topic_data): + """Test Topic model validation from JSON data.""" + topic = Topic.model_validate(topic_data) + assert isinstance(topic, Topic) + assert topic.name == "order_items" + + +class TestDatabricksMetricViewExamples: + """Test cases for Databricks metric view functionality from examples.""" + + @pytest.fixture + def topic_data(self): + """Load topic test data.""" + data_path = Path(__file__).parent / "data" / "order_items.topic.json" + with open(data_path, "r", encoding="utf-8") as f: + return json.load(f) + + @pytest.fixture + def topic(self, topic_data): + """Create Topic instance from test data.""" + return Topic.model_validate(topic_data) + + @pytest.fixture + def metric_view(self, topic): + """Create metric view from topic.""" + return metric_view_from_topic(topic, None, None) + + def test_topic_to_metric_view_conversion(self, topic): + """Test conversion of Topic to DatabricksMetricView.""" + metric_view = metric_view_from_topic(topic, None, None) + + assert metric_view is not None + assert metric_view.name == "order_items" + assert metric_view.source == "public.order_items" + assert len(metric_view.joins) == 0 + + def test_metric_view_yaml_generation(self, metric_view): + """Test YAML generation from metric view.""" + yaml_output = metric_view.generate_yaml() + + assert isinstance(yaml_output, str) + assert len(yaml_output.strip()) > 0 + + # Load expected YAML for comparison + expected_path = Path(__file__).parent / "data" / "order_items.databricks_metric_view.yaml" + with open(expected_path, "r", encoding="utf-8") as f: + expected_yaml = f.read() + + assert yaml_output.strip() == expected_yaml.strip() + + def test_metric_view_sql_generation(self, metric_view): + """Test SQL generation from metric view.""" + sql_output = metric_view.generate_sql() + + assert isinstance(sql_output, str) + assert len(sql_output.strip()) > 0 + + # Load expected SQL for comparison + expected_path = Path(__file__).parent / "data" / "order_items.databricks_metric_view.sql" + with open(expected_path, "r", encoding="utf-8") as f: + expected_sql = f.read() + + assert sql_output.strip() == expected_sql.strip() + + +class TestSnowflakeSemanticViewExamples: + """Test cases for Snowflake semantic view functionality from examples.""" + + @pytest.fixture + def topic_data(self): + """Load topic test data.""" + data_path = Path(__file__).parent / "data" / "order_items.topic.json" + with open(data_path, "r", encoding="utf-8") as f: + return json.load(f) + + @pytest.fixture + def topic(self, topic_data): + """Create Topic instance from test data.""" + return Topic.model_validate(topic_data) + + @pytest.fixture + def semantic_view(self, topic): + """Create semantic view from topic.""" + return sematic_view_from_topic(topic) + + def test_topic_to_semantic_view_conversion(self, topic): + """Test conversion of Topic to SnowflakeSemanticView.""" + semantic_view = sematic_view_from_topic(topic) + + assert semantic_view is not None + assert semantic_view.name == "order_items" + assert len(semantic_view.tables) == 6 + + def test_semantic_view_sql_generation(self, semantic_view): + """Test SQL generation from semantic view.""" + sql_output = semantic_view.generate_sql() + + assert isinstance(sql_output, str) + assert len(sql_output.strip()) > 0 + + # Load expected SQL for comparison + expected_path = Path(__file__).parent / "data" / "order_items.snowflake_semantic_view.sql" + with open(expected_path, "r", encoding="utf-8") as f: + expected_sql = f.read() + + assert sql_output.strip() == expected_sql.strip() + + +class TestExamplesIntegration: + """Integration tests for examples functionality.""" + + def test_examples_can_be_imported(self): + """Test that all example modules can be imported.""" + try: + from examples import topic, databricks_metric_view, snowflake_semantic_view + assert topic is not None + assert databricks_metric_view is not None + assert snowflake_semantic_view is not None + except ImportError as e: + pytest.fail(f"Failed to import examples modules: {e}") + + @pytest.mark.integration + def test_end_to_end_databricks_workflow(self): + """Test complete workflow from topic to Databricks metric view.""" + # Load test data + data_path = Path(__file__).parent / "data" / "order_items.topic.json" + with open(data_path, "r", encoding="utf-8") as f: + topic_data = json.load(f) + + # Create topic + topic = Topic.model_validate(topic_data) + + # Convert to metric view + metric_view = metric_view_from_topic(topic, None, None) + + # Generate outputs + yaml_output = metric_view.generate_yaml() + sql_output = metric_view.generate_sql() + + # Verify outputs are generated + assert isinstance(yaml_output, str) + assert isinstance(sql_output, str) + assert len(yaml_output.strip()) > 0 + assert len(sql_output.strip()) > 0 + + @pytest.mark.integration + def test_end_to_end_snowflake_workflow(self): + """Test complete workflow from topic to Snowflake semantic view.""" + # Load test data + data_path = Path(__file__).parent / "data" / "order_items.topic.json" + with open(data_path, "r", encoding="utf-8") as f: + topic_data = json.load(f) + + # Create topic + topic = Topic.model_validate(topic_data) + + # Convert to semantic view + semantic_view = sematic_view_from_topic(topic) + + # Generate output + sql_output = semantic_view.generate_sql() + + # Verify output is generated + assert isinstance(sql_output, str) + assert len(sql_output.strip()) > 0 \ No newline at end of file