diff --git a/.github/workflows/feature-test.yml b/.github/workflows/feature-test.yml index 9c75d31..8e26d18 100644 --- a/.github/workflows/feature-test.yml +++ b/.github/workflows/feature-test.yml @@ -48,11 +48,76 @@ jobs: env: PYTHONPATH: ${{ github.workspace }}/src + # ๐Ÿ“ฆ Validate optional dependencies installation + validate-extras: + name: ๐Ÿ“ฆ Validate Optional Dependencies + runs-on: ubuntu-latest + needs: unit-tests + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + extra: ["cli", "async", "config", "dotenv", "dev"] + + steps: + - name: ๐Ÿ“ฅ Checkout code + uses: actions/checkout@v4 + + - name: ๐Ÿ Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: ๐Ÿ“ฆ Install base package + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: ๐Ÿ“ฆ Install optional extra [${{ matrix.extra }}] + run: | + pip install -e ".[${{ matrix.extra }}]" + + - name: โœ… Verify installation + run: | + echo "=== Installed packages ===" + pip list | grep -E "(nocodb|click|rich|aiohttp|aiofiles|pyyaml|tomli|python-dotenv)" || true + + echo "=== Testing imports ===" + python -c "from nocodb_simple_client import NocoDBClient; print('โœ“ Base client imports')" + + # Test extra-specific imports + case "${{ matrix.extra }}" in + cli) + python -c "import click; import rich; print('โœ“ CLI dependencies available')" + python -c "from nocodb_simple_client.cli import main; print('โœ“ CLI module imports')" + ;; + async) + python -c "import aiohttp; import aiofiles; print('โœ“ Async dependencies available')" + python -c "from nocodb_simple_client.async_client import AsyncNocoDBClient; print('โœ“ Async client imports')" + ;; + config) + python -c "import yaml; print('โœ“ PyYAML available')" + if [ "${{ matrix.python-version }}" != "3.11" ] && [ "${{ matrix.python-version }}" != "3.12" ] && [ "${{ matrix.python-version }}" != "3.13" ]; then + python -c "import tomli; print('โœ“ tomli available')" + else + python -c "import tomllib; print('โœ“ tomllib available (stdlib)')" + fi + python -c "from nocodb_simple_client.config import NocoDBConfig; from pathlib import Path; print('โœ“ Config module imports')" + ;; + dotenv) + python -c "import dotenv; print('โœ“ python-dotenv available')" + ;; + dev) + python -c "import pytest; import ruff; import mypy; print('โœ“ Dev dependencies available')" + ;; + esac + + echo "โœ… Extra '${{ matrix.extra }}' validated successfully!" + # ๐Ÿ”— Integration tests with Python-managed NocoDB instance integration-test: name: ๐Ÿ”— Integration Tests (Python-Managed) runs-on: ubuntu-latest - needs: unit-tests + needs: validate-extras steps: - name: ๐Ÿ“ฅ Checkout code diff --git a/docs/ADVANCED-USEAGE.MD b/docs/ADVANCED-USEAGE.MD index 31cfc75..41c007b 100644 --- a/docs/ADVANCED-USEAGE.MD +++ b/docs/ADVANCED-USEAGE.MD @@ -44,13 +44,23 @@ Optional environment variables: ### File-based Configuration +**Required Dependencies:** + +```bash +# Install with configuration file support (YAML/TOML) +pip install "nocodb-simple-client[config]" + +# For .env file support +pip install "nocodb-simple-client[dotenv]" +``` + Load configuration from JSON, YAML, or TOML files: ```python from pathlib import Path from nocodb_simple_client.config import load_config -# Load from file +# Load from file (supports .json, .yaml, .yml, .toml) config = load_config(config_path=Path("config.yaml")) ``` @@ -93,6 +103,13 @@ config.setup_logging() ## Async Support +**Required Dependencies:** + +```bash +# Install with async dependencies +pip install "nocodb-simple-client[async]" +``` + For high-performance applications, use the async client: ```python diff --git a/docs/README.template.MD b/docs/README.template.MD index a4aac1e..aa89f4f 100644 --- a/docs/README.template.MD +++ b/docs/README.template.MD @@ -46,6 +46,8 @@ A simple and powerful Python client for interacting with [NocoDB](https://nocodb ### Installation +#### Basic Installation + Install from PyPI using pip: ```bash @@ -65,6 +67,39 @@ pip install git+{{REPO_URL}}.git@v{{VERSION}} pip install git+{{REPO_URL}}.git@main ``` +#### Optional Features + +Install with optional features based on your needs: + +```bash +# Command-line interface support +pip install "nocodb-simple-client[cli]" + +# Async client support +pip install "nocodb-simple-client[async]" + +# Configuration file support (YAML/TOML) +pip install "nocodb-simple-client[config]" + +# .env file support +pip install "nocodb-simple-client[dotenv]" + +# Multiple features +pip install "nocodb-simple-client[cli,async,config]" + +# All features for development +pip install "nocodb-simple-client[dev]" +``` + +**Available extras:** + +- `cli` - Command-line interface with rich output (requires `click`, `rich`) +- `async` - Async client support (requires `aiohttp`, `aiofiles`) +- `config` - YAML/TOML configuration file support (requires `PyYAML`, `tomli` for Python <3.11) +- `dotenv` - .env file support for configuration (requires `python-dotenv`) +- `dev` - All development dependencies (testing, linting, etc.) +- `docs` - Documentation generation dependencies + ### Basic Usage ```python @@ -1003,7 +1038,9 @@ except Exception as e: ## ๐Ÿงช Examples -Check out the [`examples/`](examples/) directory for comprehensive examples: +Check out the [`examples/`](examples/) directory for comprehensive examples. See the [Examples README](examples/README.md) for detailed documentation. + +### Core Examples - **[Basic Usage](examples/basic_usage.py)**: CRUD operations and fundamentals - **[API Version Support](examples/api_version_example.py)**: Using v2 and v3 APIs with automatic conversion @@ -1017,6 +1054,14 @@ Check out the [`examples/`](examples/) directory for comprehensive examples: - **[Link Management](examples/link_examples.py)**: Managing relationships between tables - **[Configuration](examples/config_examples.py)**: Different configuration methods +### Optional Dependencies Examples + +These examples demonstrate the optional dependency groups: + +- **[Async Client](examples/async_example.py)**: High-performance concurrent operations (`[async]`) +- **[Config Files](examples/config_file_example.py)**: YAML/TOML configuration loading (`[config]`) +- **[Environment Files](examples/dotenv_example.py)**: Secure .env file management (`[dotenv]`) + ## ๐Ÿ“‹ Requirements - Python 3.8 or higher diff --git a/examples/README.md b/examples/README.md index a066a15..3459ce9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -61,6 +61,81 @@ This directory contains comprehensive examples demonstrating how to use the Noco **Best for**: Production applications requiring robust error handling +--- + +## Optional Dependencies Examples + +These examples demonstrate how to use the optional dependency groups. Each group provides additional functionality that can be installed separately based on your needs. + +### 5. Async Client (`async_example.py`) + +**Installation**: `pip install "nocodb-simple-client[async]"` + +**Purpose**: High-performance concurrent operations using async/await + +**What you'll learn**: + +- Setting up and using `AsyncNocoDBClient` +- Basic async CRUD operations with context managers +- Parallel bulk operations with automatic concurrency limiting +- Custom semaphore-based concurrency control +- Async error handling patterns +- Running multiple queries in parallel with `asyncio.gather()` + +**Best for**: High-throughput applications, batch processing, real-time data pipelines + +**Dependencies installed**: `aiohttp`, `aiofiles` + +### 6. Configuration Files (`config_file_example.py`) + +**Installation**: `pip install "nocodb-simple-client[config]"` + +**Purpose**: Load configuration from YAML and TOML files + +**What you'll learn**: + +- Loading configuration from YAML files +- Loading configuration from TOML files +- JSON configuration (no extra dependencies required) +- Managing environment-specific configurations (dev/staging/prod) +- Graceful fallback patterns when dependencies are missing +- Python 3.11+ `tomllib` vs `tomli` for older versions + +**Best for**: Applications with complex configuration needs, multi-environment deployments + +**Dependencies installed**: `PyYAML`, `tomli` (Python < 3.11 only) + +### 7. Environment Files (`dotenv_example.py`) + +**Installation**: `pip install "nocodb-simple-client[dotenv]"` + +**Purpose**: Secure secrets management with .env files + +**What you'll learn**: + +- Basic `.env` file loading with `load_dotenv()` +- Managing multiple environment files (`.env.development`, `.env.production`) +- Secrets management best practices and gitignore patterns +- Reading `.env` without modifying `os.environ` using `dotenv_values()` +- Combining `.env` files (secrets) with config files (settings) +- Docker and cloud platform compatibility + +**Best for**: 12-factor apps, Docker deployments, secure credential management + +**Dependencies installed**: `python-dotenv` + +### Quick Reference: Optional Extras + +| Extra | Command | Use Case | +| ---------- | ----------------------------------------------------------- | --------------------------- | +| `[async]` | `pip install "nocodb-simple-client[async]"` | Async/concurrent operations | +| `[config]` | `pip install "nocodb-simple-client[config]"` | YAML/TOML config files | +| `[dotenv]` | `pip install "nocodb-simple-client[dotenv]"` | .env file support | +| `[cli]` | `pip install "nocodb-simple-client[cli]"` | Command-line interface | +| Combined | `pip install "nocodb-simple-client[async,config,dotenv]"` | Multiple features | + +--- + ## Configuration Before running the examples, you'll need to configure: diff --git a/examples/async_example.py b/examples/async_example.py new file mode 100644 index 0000000..aa5124d --- /dev/null +++ b/examples/async_example.py @@ -0,0 +1,274 @@ +""" +Async client examples for NocoDB Simple Client. + +This example demonstrates how to use the async client for high-performance +concurrent operations with NocoDB. + +INSTALLATION: + pip install "nocodb-simple-client[async]" + +This installs the required dependencies: + - aiohttp: Async HTTP client + - aiofiles: Async file operations +""" + +import asyncio +import importlib.util +import sys + +# ============================================================================= +# DEPENDENCY CHECK +# ============================================================================= +# Check if async dependencies are available before importing + +ASYNC_AVAILABLE = ( + importlib.util.find_spec("aiohttp") is not None + and importlib.util.find_spec("aiofiles") is not None +) + +if not ASYNC_AVAILABLE: + print("=" * 60) + print("ERROR: Async dependencies not installed!") + print("=" * 60) + print() + print("To use the async client, install the [async] extra:") + print() + print(' pip install "nocodb-simple-client[async]"') + print() + print("This will install: aiohttp, aiofiles") + print("=" * 60) + sys.exit(1) + +# Now we can safely import the async client +from nocodb_simple_client import NocoDBConfig # noqa: E402 +from nocodb_simple_client.async_client import ( # noqa: E402 + AsyncNocoDBClient, + AsyncNocoDBTable, +) +from nocodb_simple_client.exceptions import ( # noqa: E402 + AuthenticationException, + NocoDBException, + RateLimitException, + RecordNotFoundException, +) + +# ============================================================================= +# CONFIGURATION +# ============================================================================= +NOCODB_BASE_URL = "https://your-nocodb-instance.com" +API_TOKEN = "your-api-token-here" +TABLE_ID = "your-table-id-here" + + +# ============================================================================= +# EXAMPLE 1: Basic Async Operations +# ============================================================================= +async def example_basic_operations(): + """Demonstrate basic async CRUD operations.""" + print("\n" + "=" * 60) + print("Example 1: Basic Async Operations") + print("=" * 60) + + config = NocoDBConfig( + base_url=NOCODB_BASE_URL, + api_token=API_TOKEN, + timeout=30.0, + ) + + # Use async context manager for automatic session management + async with AsyncNocoDBClient(config) as client: + table = AsyncNocoDBTable(client, TABLE_ID) + + # Insert a record + record_id = await table.insert_record( + { + "Name": "Async User", + "Email": "async@example.com", + } + ) + print(f"Inserted record: {record_id}") + + # Get the record + record = await table.get_record(record_id) + print(f"Retrieved record: {record}") + + # Update the record + await table.update_record({"Id": record_id, "Name": "Updated Async User"}) + print(f"Updated record: {record_id}") + + # Delete the record + await table.delete_record(record_id) + print(f"Deleted record: {record_id}") + + print("Basic async operations completed!") + + +# ============================================================================= +# EXAMPLE 2: Parallel Bulk Operations +# ============================================================================= +async def example_bulk_operations(): + """Demonstrate high-performance bulk operations with concurrency control.""" + print("\n" + "=" * 60) + print("Example 2: Parallel Bulk Operations") + print("=" * 60) + + config = NocoDBConfig( + base_url=NOCODB_BASE_URL, + api_token=API_TOKEN, + pool_connections=20, # Increase for bulk operations + pool_maxsize=50, + ) + + async with AsyncNocoDBClient(config) as client: + table = AsyncNocoDBTable(client, TABLE_ID) + + # Prepare records for bulk insert + records = [{"Name": f"User {i}", "Email": f"user{i}@example.com"} for i in range(100)] + + # Bulk insert with automatic concurrency limiting (10 concurrent requests) + print(f"Inserting {len(records)} records in parallel...") + inserted_ids = await table.bulk_insert_records(records) + print(f"Inserted {len(inserted_ids)} records") + + # Bulk update + updates = [{"Id": id, "Name": f"Updated User {i}"} for i, id in enumerate(inserted_ids)] + print(f"Updating {len(updates)} records in parallel...") + await table.bulk_update_records(updates) + print("Bulk update completed") + + print("Bulk operations completed!") + + +# ============================================================================= +# EXAMPLE 3: Custom Concurrency Control +# ============================================================================= +async def example_custom_concurrency(): + """Demonstrate custom concurrency control with semaphores.""" + print("\n" + "=" * 60) + print("Example 3: Custom Concurrency Control") + print("=" * 60) + + config = NocoDBConfig( + base_url=NOCODB_BASE_URL, + api_token=API_TOKEN, + ) + + async with AsyncNocoDBClient(config) as client: + table = AsyncNocoDBTable(client, TABLE_ID) + + # Custom concurrency limit + max_concurrent = 5 + semaphore = asyncio.Semaphore(max_concurrent) + + async def process_with_limit(record_data: dict) -> int | str: + """Insert a record with concurrency limiting.""" + async with semaphore: + return await table.insert_record(record_data) + + # Create tasks + records = [{"Name": f"Concurrent {i}"} for i in range(20)] + tasks = [process_with_limit(record) for record in records] + + # Execute with controlled concurrency + print(f"Processing {len(records)} records with max {max_concurrent} concurrent...") + results = await asyncio.gather(*tasks) + print(f"Processed {len(results)} records") + + print("Custom concurrency example completed!") + + +# ============================================================================= +# EXAMPLE 4: Error Handling in Async Context +# ============================================================================= +async def example_error_handling(): + """Demonstrate proper error handling in async operations.""" + print("\n" + "=" * 60) + print("Example 4: Async Error Handling") + print("=" * 60) + + config = NocoDBConfig( + base_url=NOCODB_BASE_URL, + api_token=API_TOKEN, + ) + + async with AsyncNocoDBClient(config) as client: + table = AsyncNocoDBTable(client, TABLE_ID) + + try: + # Try to get a non-existent record + _record = await table.get_record(999999) # noqa: F841 + except RecordNotFoundException: + print("Record not found (expected)") + except AuthenticationException: + print("Authentication failed - check your API token") + except RateLimitException as e: + print(f"Rate limited - retry after {e.retry_after} seconds") + except NocoDBException as e: + print(f"NocoDB error: {e.message}") + + print("Error handling example completed!") + + +# ============================================================================= +# EXAMPLE 5: Parallel Queries +# ============================================================================= +async def example_parallel_queries(): + """Demonstrate running multiple queries in parallel.""" + print("\n" + "=" * 60) + print("Example 5: Parallel Queries") + print("=" * 60) + + config = NocoDBConfig( + base_url=NOCODB_BASE_URL, + api_token=API_TOKEN, + ) + + async with AsyncNocoDBClient(config) as client: + table = AsyncNocoDBTable(client, TABLE_ID) + + # Run multiple queries in parallel + results = await asyncio.gather( + table.get_records(limit=10, sort="Name"), + table.get_records(limit=10, sort="-CreatedAt"), + table.count_records(), + table.count_records(where="(Status,eq,Active)"), + ) + + sorted_by_name, recent_records, total_count, active_count = results + + print(f"Records sorted by name: {len(sorted_by_name)}") + print(f"Recent records: {len(recent_records)}") + print(f"Total count: {total_count}") + print(f"Active count: {active_count}") + + print("Parallel queries completed!") + + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= +async def main(): + """Run all async examples.""" + print("\n" + "=" * 60) + print("NOCODB SIMPLE CLIENT - ASYNC EXAMPLES") + print("=" * 60) + print() + print("These examples demonstrate the [async] optional dependency group.") + print() + print("Installation: pip install 'nocodb-simple-client[async]'") + print() + + # Uncomment the examples you want to run: + # await example_basic_operations() + # await example_bulk_operations() + # await example_custom_concurrency() + # await example_error_handling() + # await example_parallel_queries() + + print("\n" + "=" * 60) + print("Uncomment the examples in main() to run them.") + print("=" * 60) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/config_file_example.py b/examples/config_file_example.py new file mode 100644 index 0000000..3daec3b --- /dev/null +++ b/examples/config_file_example.py @@ -0,0 +1,402 @@ +""" +Configuration file examples for NocoDB Simple Client. + +This example demonstrates how to load configuration from YAML and TOML files +using the [config] optional dependency group. + +INSTALLATION: + pip install "nocodb-simple-client[config]" + +This installs the required dependencies: + - PyYAML: YAML file support + - tomli: TOML file support (Python < 3.11 only; Python 3.11+ uses stdlib tomllib) +""" + +import importlib.util +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +from nocodb_simple_client.config import NocoDBConfig + +# ============================================================================= +# DEPENDENCY CHECK +# ============================================================================= + +# Check for YAML support (PyYAML) +YAML_AVAILABLE = importlib.util.find_spec("yaml") is not None + +# Check for TOML support (stdlib tomllib for 3.11+ or tomli for older versions) +TOML_AVAILABLE = ( + importlib.util.find_spec("tomllib") is not None or importlib.util.find_spec("tomli") is not None +) + +if not YAML_AVAILABLE and not TOML_AVAILABLE: + print("=" * 60) + print("ERROR: Config file dependencies not installed!") + print("=" * 60) + print() + print("To use YAML/TOML configuration files, install the [config] extra:") + print() + print(' pip install "nocodb-simple-client[config]"') + print() + print("This will install: PyYAML, tomli (for Python < 3.11)") + print() + print("Note: Python 3.11+ includes tomllib in the standard library.") + print("=" * 60) + sys.exit(1) + + +# ============================================================================= +# EXAMPLE 1: YAML Configuration +# ============================================================================= +def example_yaml_config(): + """Demonstrate loading configuration from a YAML file.""" + print("\n" + "=" * 60) + print("Example 1: YAML Configuration") + print("=" * 60) + + if not YAML_AVAILABLE: + print("YAML support not available. Install PyYAML:") + print(' pip install "nocodb-simple-client[config]"') + return + + # Create a sample YAML configuration file + yaml_content = """ +# NocoDB Configuration +# Save this as: nocodb-config.yaml + +base_url: "https://your-nocodb-instance.com" +api_token: "your-api-token-here" + +# Connection settings +timeout: 60.0 +max_retries: 3 +backoff_factor: 0.5 + +# Connection pooling +pool_connections: 20 +pool_maxsize: 50 + +# Security +verify_ssl: true + +# Debugging +debug: false +log_level: "INFO" + +# Custom headers (optional) +extra_headers: + X-Request-Source: "my-application" + X-Client-Version: "1.0.0" +""" + + with TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "nocodb-config.yaml" + config_path.write_text(yaml_content) + + print(f"Created sample config: {config_path}") + print() + print("YAML Content:") + print("-" * 40) + print(yaml_content.strip()) + print("-" * 40) + print() + + # Load configuration from YAML + config = NocoDBConfig.from_file(config_path) + + print("Loaded configuration:") + print(f" Base URL: {config.base_url}") + print(f" Timeout: {config.timeout}s") + print(f" Max Retries: {config.max_retries}") + print(f" Pool Size: {config.pool_connections}/{config.pool_maxsize}") + print(f" SSL Verify: {config.verify_ssl}") + print(f" Debug: {config.debug}") + + print("\nYAML configuration example completed!") + + +# ============================================================================= +# EXAMPLE 2: TOML Configuration +# ============================================================================= +def example_toml_config(): + """Demonstrate loading configuration from a TOML file.""" + print("\n" + "=" * 60) + print("Example 2: TOML Configuration") + print("=" * 60) + + if not TOML_AVAILABLE: + print("TOML support not available.") + print("For Python < 3.11, install tomli:") + print(' pip install "nocodb-simple-client[config]"') + print() + print("Python 3.11+ includes tomllib in the standard library.") + return + + # Detect which TOML library is being used + if importlib.util.find_spec("tomllib") is not None: + toml_source = "tomllib (stdlib, Python 3.11+)" + else: + toml_source = "tomli (third-party)" + + print(f"Using: {toml_source}") + print() + + # Create a sample TOML configuration file + toml_content = """ +# NocoDB Configuration +# Save this as: nocodb-config.toml + +base_url = "https://your-nocodb-instance.com" +api_token = "your-api-token-here" + +# Connection settings +timeout = 60.0 +max_retries = 3 +backoff_factor = 0.5 + +# Connection pooling +pool_connections = 20 +pool_maxsize = 50 + +# Security +verify_ssl = true + +# Debugging +debug = false +log_level = "INFO" + +# Custom headers (optional) +[extra_headers] +X-Request-Source = "my-application" +X-Client-Version = "1.0.0" +""" + + with TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "nocodb-config.toml" + config_path.write_text(toml_content) + + print(f"Created sample config: {config_path}") + print() + print("TOML Content:") + print("-" * 40) + print(toml_content.strip()) + print("-" * 40) + print() + + # Load configuration from TOML + config = NocoDBConfig.from_file(config_path) + + print("Loaded configuration:") + print(f" Base URL: {config.base_url}") + print(f" Timeout: {config.timeout}s") + print(f" Max Retries: {config.max_retries}") + print(f" Pool Size: {config.pool_connections}/{config.pool_maxsize}") + + print("\nTOML configuration example completed!") + + +# ============================================================================= +# EXAMPLE 3: JSON Configuration (Built-in, no extra dependencies) +# ============================================================================= +def example_json_config(): + """Demonstrate loading configuration from a JSON file (no extra dependencies).""" + print("\n" + "=" * 60) + print("Example 3: JSON Configuration (Built-in)") + print("=" * 60) + + # JSON is always available (stdlib) + json_content = """{ + "base_url": "https://your-nocodb-instance.com", + "api_token": "your-api-token-here", + "timeout": 60.0, + "max_retries": 3, + "backoff_factor": 0.5, + "pool_connections": 20, + "pool_maxsize": 50, + "verify_ssl": true, + "debug": false, + "log_level": "INFO", + "extra_headers": { + "X-Request-Source": "my-application" + } +}""" + + with TemporaryDirectory() as tmpdir: + config_path = Path(tmpdir) / "nocodb-config.json" + config_path.write_text(json_content) + + print("Note: JSON support requires NO extra dependencies!") + print() + print("JSON Content:") + print("-" * 40) + print(json_content) + print("-" * 40) + print() + + # Load configuration from JSON + config = NocoDBConfig.from_file(config_path) + + print("Loaded configuration:") + print(f" Base URL: {config.base_url}") + print(f" Timeout: {config.timeout}s") + + print("\nJSON configuration example completed!") + + +# ============================================================================= +# EXAMPLE 4: Environment-Specific Configurations +# ============================================================================= +def example_environment_configs(): + """Demonstrate managing multiple environment configurations.""" + print("\n" + "=" * 60) + print("Example 4: Environment-Specific Configurations") + print("=" * 60) + + if not YAML_AVAILABLE: + print("YAML support required for this example.") + return + + # Development configuration + dev_config = """ +base_url: "http://localhost:8080" +api_token: "dev-token" +timeout: 10.0 +max_retries: 1 +verify_ssl: false +debug: true +log_level: "DEBUG" +""" + + # Staging configuration + staging_config = """ +base_url: "https://staging.nocodb.example.com" +api_token: "staging-token" +timeout: 30.0 +max_retries: 3 +verify_ssl: true +debug: false +log_level: "INFO" +""" + + # Production configuration + prod_config = """ +base_url: "https://nocodb.example.com" +api_token: "prod-token" +timeout: 120.0 +max_retries: 5 +backoff_factor: 1.0 +pool_connections: 50 +pool_maxsize: 100 +verify_ssl: true +debug: false +log_level: "WARNING" +""" + + with TemporaryDirectory() as tmpdir: + configs = { + "development": dev_config, + "staging": staging_config, + "production": prod_config, + } + + for env_name, content in configs.items(): + config_path = Path(tmpdir) / f"config.{env_name}.yaml" + config_path.write_text(content) + + config = NocoDBConfig.from_file(config_path) + print(f"\n{env_name.upper()}:") + print(f" URL: {config.base_url}") + print(f" Timeout: {config.timeout}s") + print(f" Debug: {config.debug}") + print(f" Log Level: {config.log_level}") + + print("\nEnvironment configurations example completed!") + + +# ============================================================================= +# EXAMPLE 5: Graceful Fallback Pattern +# ============================================================================= +def example_graceful_fallback(): + """Demonstrate graceful fallback when config dependencies are missing.""" + print("\n" + "=" * 60) + print("Example 5: Graceful Fallback Pattern") + print("=" * 60) + + print( + """ +This pattern allows your application to work with or without +the [config] extra installed: + + from pathlib import Path + from nocodb_simple_client.config import NocoDBConfig + + def load_configuration(config_path: Path | None = None): + '''Load config with graceful fallback.''' + + # Try file-based configuration first + if config_path and config_path.exists(): + suffix = config_path.suffix.lower() + + if suffix == '.json': + # JSON always works (stdlib) + return NocoDBConfig.from_file(config_path) + + elif suffix in ['.yaml', '.yml']: + try: + return NocoDBConfig.from_file(config_path) + except ValueError as e: + if 'PyYAML' in str(e): + print("YAML not available, falling back to env") + else: + raise + + elif suffix == '.toml': + try: + return NocoDBConfig.from_file(config_path) + except ValueError as e: + if 'tomli' in str(e): + print("TOML not available, falling back to env") + else: + raise + + # Fallback to environment variables + return NocoDBConfig.from_env() +""" + ) + + print("\nGraceful fallback pattern demonstrated!") + + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= +def main(): + """Run all configuration file examples.""" + print("\n" + "=" * 60) + print("NOCODB SIMPLE CLIENT - CONFIG FILE EXAMPLES") + print("=" * 60) + print() + print("These examples demonstrate the [config] optional dependency group.") + print() + print("Installation: pip install 'nocodb-simple-client[config]'") + print() + print("Dependency status:") + print(f" YAML support (PyYAML): {'Available' if YAML_AVAILABLE else 'Not installed'}") + print(f" TOML support: {'Available' if TOML_AVAILABLE else 'Not installed'}") + + # Run examples + example_json_config() # Always works (no extra deps) + example_yaml_config() # Requires PyYAML + example_toml_config() # Requires tomli (< 3.11) or stdlib tomllib (3.11+) + example_environment_configs() + example_graceful_fallback() + + print("\n" + "=" * 60) + print("All configuration file examples completed!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/examples/dotenv_example.py b/examples/dotenv_example.py new file mode 100644 index 0000000..1721a09 --- /dev/null +++ b/examples/dotenv_example.py @@ -0,0 +1,414 @@ +""" +Environment file (.env) examples for NocoDB Simple Client. + +This example demonstrates how to use .env files for configuration +using the [dotenv] optional dependency group. + +INSTALLATION: + pip install "nocodb-simple-client[dotenv]" + +This installs the required dependency: + - python-dotenv: .env file support + +WHY USE .env FILES? + - Keep secrets out of source code + - Easy environment switching (dev/staging/prod) + - Standard practice for 12-factor apps + - Works with Docker and cloud platforms +""" + +import importlib.util +import os +import sys +from pathlib import Path +from tempfile import TemporaryDirectory + +from nocodb_simple_client.config import NocoDBConfig + +# ============================================================================= +# DEPENDENCY CHECK +# ============================================================================= + +DOTENV_AVAILABLE = importlib.util.find_spec("dotenv") is not None + +if not DOTENV_AVAILABLE: + print("=" * 60) + print("ERROR: python-dotenv not installed!") + print("=" * 60) + print() + print("To use .env file support, install the [dotenv] extra:") + print() + print(' pip install "nocodb-simple-client[dotenv]"') + print() + print("This will install: python-dotenv") + print("=" * 60) + sys.exit(1) + +# Import after check +from dotenv import dotenv_values, load_dotenv # noqa: E402 + + +# ============================================================================= +# EXAMPLE 1: Basic .env File Usage +# ============================================================================= +def example_basic_dotenv(): + """Demonstrate basic .env file loading.""" + print("\n" + "=" * 60) + print("Example 1: Basic .env File Usage") + print("=" * 60) + + # Sample .env file content + env_content = """ +# NocoDB Configuration +# Save this as: .env + +NOCODB_BASE_URL=https://your-nocodb-instance.com +NOCODB_API_TOKEN=your-api-token-here +NOCODB_TIMEOUT=60.0 +NOCODB_MAX_RETRIES=3 +NOCODB_DEBUG=false +""" + + with TemporaryDirectory() as tmpdir: + env_path = Path(tmpdir) / ".env" + env_path.write_text(env_content) + + print(".env Content:") + print("-" * 40) + print(env_content.strip()) + print("-" * 40) + print() + + # Load .env file into environment + load_dotenv(env_path) + + # Now NocoDBConfig.from_env() can read these values + config = NocoDBConfig.from_env() + + print("Loaded configuration:") + print(f" Base URL: {config.base_url}") + print(f" Timeout: {config.timeout}s") + print(f" Max Retries: {config.max_retries}") + print(f" Debug: {config.debug}") + + print("\nBasic .env example completed!") + + +# ============================================================================= +# EXAMPLE 2: Multiple Environment Files +# ============================================================================= +def example_multiple_environments(): + """Demonstrate using different .env files for different environments.""" + print("\n" + "=" * 60) + print("Example 2: Multiple Environment Files") + print("=" * 60) + + # Development environment + dev_env = """ +NOCODB_BASE_URL=http://localhost:8080 +NOCODB_API_TOKEN=dev-token-12345 +NOCODB_TIMEOUT=10.0 +NOCODB_DEBUG=true +NOCODB_LOG_LEVEL=DEBUG +""" + + # Staging environment + staging_env = """ +NOCODB_BASE_URL=https://staging.nocodb.example.com +NOCODB_API_TOKEN=staging-token-67890 +NOCODB_TIMEOUT=30.0 +NOCODB_DEBUG=false +NOCODB_LOG_LEVEL=INFO +""" + + # Production environment + prod_env = """ +NOCODB_BASE_URL=https://nocodb.example.com +NOCODB_API_TOKEN=prod-token-secure +NOCODB_TIMEOUT=120.0 +NOCODB_DEBUG=false +NOCODB_LOG_LEVEL=WARNING +NOCODB_VERIFY_SSL=true +""" + + with TemporaryDirectory() as tmpdir: + envs = { + ".env.development": dev_env, + ".env.staging": staging_env, + ".env.production": prod_env, + } + + for filename, content in envs.items(): + env_path = Path(tmpdir) / filename + env_path.write_text(content) + + print("File structure:") + print(" .env.development <- Local development") + print(" .env.staging <- Staging environment") + print(" .env.production <- Production environment") + print() + + # Load based on APP_ENV environment variable + app_env = os.getenv("APP_ENV", "development") + env_file = Path(tmpdir) / f".env.{app_env}" + + print(f"Loading: .env.{app_env}") + load_dotenv(env_file, override=True) + + config = NocoDBConfig.from_env() + print(f" Base URL: {config.base_url}") + print(f" Debug: {config.debug}") + + print("\nMultiple environments example completed!") + + +# ============================================================================= +# EXAMPLE 3: .env with Secrets Management +# ============================================================================= +def example_secrets_management(): + """Demonstrate secure secrets handling with .env files.""" + print("\n" + "=" * 60) + print("Example 3: Secrets Management Best Practices") + print("=" * 60) + + print( + """ +BEST PRACTICES FOR .env FILES: + +1. NEVER commit .env files to version control: + + # .gitignore + .env + .env.* + !.env.example + +2. Create an .env.example template (safe to commit): + + # .env.example + NOCODB_BASE_URL=https://your-instance.com + NOCODB_API_TOKEN= + NOCODB_TIMEOUT=60.0 + +3. Use different files for different environments: + + .env <- Local overrides (gitignored) + .env.development <- Development defaults + .env.production <- Production settings + .env.example <- Template (committed) + +4. Load with precedence (local overrides defaults): + + from dotenv import load_dotenv + + # Load base configuration + load_dotenv('.env.development') + + # Override with local settings + load_dotenv('.env', override=True) + +5. Validate required variables: + + import os + + required = ['NOCODB_BASE_URL', 'NOCODB_API_TOKEN'] + missing = [var for var in required if not os.getenv(var)] + + if missing: + raise ValueError(f"Missing required env vars: {missing}") +""" + ) + + print("Secrets management best practices shown!") + + +# ============================================================================= +# EXAMPLE 4: Reading .env Without Modifying Environment +# ============================================================================= +def example_dotenv_values(): + """Demonstrate reading .env without polluting the environment.""" + print("\n" + "=" * 60) + print("Example 4: Read .env Without Modifying os.environ") + print("=" * 60) + + env_content = """ +NOCODB_BASE_URL=https://isolated-example.com +NOCODB_API_TOKEN=isolated-token +NOCODB_TIMEOUT=45.0 +""" + + with TemporaryDirectory() as tmpdir: + env_path = Path(tmpdir) / ".env" + env_path.write_text(env_content) + + # Read values without modifying os.environ + config_dict = dotenv_values(env_path) + + print("Read without modifying environment:") + for key, value in config_dict.items(): + print(f" {key}={value}") + + # Verify os.environ was not modified + print() + print("os.environ['NOCODB_BASE_URL'] =", os.getenv("NOCODB_BASE_URL", "(not set)")) + + # Create config manually from dotenv_values + config = NocoDBConfig( + base_url=config_dict.get("NOCODB_BASE_URL", ""), + api_token=config_dict.get("NOCODB_API_TOKEN", ""), + timeout=float(config_dict.get("NOCODB_TIMEOUT", "30.0")), + ) + print() + print(f"Created config with timeout: {config.timeout}s") + + print("\ndotenv_values example completed!") + + +# ============================================================================= +# EXAMPLE 5: Combining .env with Config Files +# ============================================================================= +def example_combined_config(): + """Demonstrate combining .env for secrets with config files for settings.""" + print("\n" + "=" * 60) + print("Example 5: Combining .env with Config Files") + print("=" * 60) + + print( + """ +RECOMMENDED PATTERN: Separate secrets from configuration + +1. .env file (gitignored) - Contains ONLY secrets: + + NOCODB_API_TOKEN=secret-token-here + NOCODB_PROTECTION_AUTH=protection-secret + +2. config.yaml (committed) - Contains non-secret settings: + + base_url: "https://nocodb.example.com" + timeout: 60.0 + max_retries: 3 + pool_connections: 20 + +3. Load both in your application: + + from dotenv import load_dotenv + from nocodb_simple_client.config import NocoDBConfig + import os + + # Load secrets into environment + load_dotenv() + + # Load settings from config file + config = NocoDBConfig.from_file(Path('config.yaml')) + + # Override with environment secrets + config.api_token = os.getenv('NOCODB_API_TOKEN') + +This approach: + - Keeps secrets out of config files + - Allows config files to be version controlled + - Works seamlessly with CI/CD pipelines + - Supports Docker secrets and cloud KMS +""" + ) + + print("Combined configuration pattern shown!") + + +# ============================================================================= +# EXAMPLE 6: Docker and Cloud Compatibility +# ============================================================================= +def example_docker_cloud(): + """Demonstrate .env usage with Docker and cloud platforms.""" + print("\n" + "=" * 60) + print("Example 6: Docker and Cloud Compatibility") + print("=" * 60) + + print( + """ +DOCKER USAGE: + +1. docker-compose.yml: + + services: + app: + image: my-app + env_file: + - .env + environment: + - NOCODB_BASE_URL=https://nocodb.example.com + +2. Docker run: + + docker run --env-file .env my-app + +3. In your Python code: + + # No need to call load_dotenv() - Docker already loaded vars + config = NocoDBConfig.from_env() + + +CLOUD PLATFORMS: + +AWS ECS/Lambda: + - Use AWS Secrets Manager or Parameter Store + - Set environment variables in task definition + +Google Cloud Run: + - Use Secret Manager + - Set environment variables in service config + +Azure: + - Use Azure Key Vault + - Set in App Service configuration + +Kubernetes: + - Use ConfigMaps for settings + - Use Secrets for tokens + - Mount as environment variables + + +LOCAL DEVELOPMENT with dotenv: + + from dotenv import load_dotenv + + # Only load .env in development + if os.getenv('ENVIRONMENT') != 'production': + load_dotenv() + + config = NocoDBConfig.from_env() +""" + ) + + print("Docker and cloud patterns shown!") + + +# ============================================================================= +# MAIN ENTRY POINT +# ============================================================================= +def main(): + """Run all dotenv examples.""" + print("\n" + "=" * 60) + print("NOCODB SIMPLE CLIENT - DOTENV EXAMPLES") + print("=" * 60) + print() + print("These examples demonstrate the [dotenv] optional dependency group.") + print() + print("Installation: pip install 'nocodb-simple-client[dotenv]'") + print() + print(f"python-dotenv available: {DOTENV_AVAILABLE}") + + # Run examples + example_basic_dotenv() + example_multiple_environments() + example_secrets_management() + example_dotenv_values() + example_combined_config() + example_docker_cloud() + + print("\n" + "=" * 60) + print("All dotenv examples completed!") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index 44ead1d..0da0f7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,6 +48,28 @@ dependencies = [ ] [project.optional-dependencies] +# Runtime optional features +cli = [ + # Command-line interface dependencies + "click>=8.0.0", + "rich>=13.0.0", +] +async = [ + # Async client dependencies + "aiohttp>=3.8.0", + "aiofiles>=0.8.0", +] +config = [ + # Configuration file format support (YAML/TOML) + "PyYAML>=6.0.0", + "tomli>=2.0.0; python_version<'3.11'", +] +dotenv = [ + # .env file support for configuration + "python-dotenv>=1.0.0", +] + +# Development dependencies dev = [ # Testing "pytest>=7.0.0", @@ -73,13 +95,10 @@ dev = [ # Development Tools "pre-commit>=2.20.0", "tox>=4.0.0", - "tomli>=2.0.0; python_version<'3.11'", "types-requests>=2.31.0", "types-PyYAML>=6.0.0", "types-aiofiles>=0.8.0", "python-dotenv>=1.0.0", - "aiohttp>=3.8.0", - "aiofiles>=0.8.0", # Integration Testing "docker>=6.0.0", @@ -92,6 +111,9 @@ docs = [ "mkdocstrings[python]>=0.19.0", ] +[project.scripts] +nocodb = "nocodb_simple_client.cli:main" + [project.urls] Homepage = "https://github.com/bauer-group/LIB-NocoDB_SimpleClient" Documentation = "https://github.com/bauer-group/LIB-NocoDB_SimpleClient#readme" diff --git a/src/nocodb_simple_client/config.py b/src/nocodb_simple_client/config.py index cafc56f..f742e83 100644 --- a/src/nocodb_simple_client/config.py +++ b/src/nocodb_simple_client/config.py @@ -140,12 +140,19 @@ def from_file(cls, config_path: Path) -> "NocoDBConfig": raise ValueError("PyYAML is required to load YAML configuration files") from e elif suffix == ".toml": try: - import tomli + # Use tomllib (stdlib) for Python 3.11+, otherwise tomli + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore[no-redef] with open(config_path, "rb") as f: - data = tomli.load(f) + data = tomllib.load(f) except ImportError as e: - raise ValueError("tomli is required to load TOML configuration files") from e + raise ValueError( + "tomli is required to load TOML configuration files " + "(pre-installed in Python 3.11+)" + ) from e else: raise ValueError(f"Unsupported configuration file format: {suffix}")