diff --git a/.husky/hooks/code-quality.sh b/.husky/hooks/code-quality.sh new file mode 100755 index 0000000..b556a90 --- /dev/null +++ b/.husky/hooks/code-quality.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +""" +Code quality checking hook for Husky-style pre-commit setup. +This hook runs Black, isort, flake8, and Bandit for code quality. +""" + +echo "๐Ÿ Running Python code quality checks..." + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check Black formatting +echo " - Checking code formatting with Black..." +if command_exists black; then + if black --check --diff .; then + echo " โœ… Black formatting check passed" + else + echo " โŒ Black formatting issues found" + echo " ๐Ÿ’ก Run 'black .' to fix formatting" + exit 1 + fi +else + echo " โš ๏ธ Black not installed, skipping formatting check" + echo " ๐Ÿ’ก Install with: pip install black" +fi + +# Check isort import sorting +echo " - Checking import sorting with isort..." +if command_exists isort; then + if isort --check-only --diff .; then + echo " โœ… isort import sorting check passed" + else + echo " โŒ isort import sorting issues found" + echo " ๐Ÿ’ก Run 'isort .' to fix import sorting" + exit 1 + fi +else + echo " โš ๏ธ isort not installed, skipping import sorting check" + echo " ๐Ÿ’ก Install with: pip install isort" +fi + +# Check flake8 linting +echo " - Running linting with flake8..." +if command_exists flake8; then + if flake8 --max-line-length=88 --extend-ignore=E203,W503 .; then + echo " โœ… flake8 linting check passed" + else + echo " โŒ flake8 linting issues found" + echo " ๐Ÿ’ก Fix the linting issues above" + exit 1 + fi +else + echo " โš ๏ธ flake8 not installed, skipping linting check" + echo " ๐Ÿ’ก Install with: pip install flake8" +fi + +# Check Bandit security linting +echo " - Running security linting with Bandit..." +if command_exists bandit; then + if bandit -r . -f json -o bandit-report.json; then + echo " โœ… Bandit security linting check passed" + else + echo " โŒ Bandit found security issues" + echo " ๐Ÿ’ก Review bandit-report.json for details" + exit 1 + fi +else + echo " โš ๏ธ Bandit not installed, skipping security linting" + echo " ๐Ÿ’ก Install with: pip install bandit" +fi + +echo "โœ… All code quality checks passed!" diff --git a/.husky/hooks/snyk-scan.py b/.husky/hooks/snyk-scan.py new file mode 100755 index 0000000..c7b4dc0 --- /dev/null +++ b/.husky/hooks/snyk-scan.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Snyk security scanning hook for Husky-style pre-commit setup. +This hook runs Snyk security scanning on Python dependencies. +""" + +import os +import sys +import subprocess +import json +from pathlib import Path + + +def run_snyk_scan(): + """Run Snyk security scan on Python dependencies.""" + + # Check if Snyk CLI is available + try: + subprocess.run(['snyk', '--version'], capture_output=True, check=True) + except (subprocess.CalledProcessError, FileNotFoundError): + print("โŒ Snyk CLI not found. Please install it first:") + print(" npm install -g snyk") + print(" or visit: https://snyk.io/docs/using-snyk/") + return 1 + + # Check if SNYK_TOKEN is set + if not os.getenv('SNYK_TOKEN'): + print("โš ๏ธ SNYK_TOKEN environment variable not set.") + print(" Please set it with: export SNYK_TOKEN=your_token") + print(" You can get a token from: https://app.snyk.io/account") + return 0 # Don't fail the commit, just warn + + # Check for requirements.txt + requirements_files = ['requirements.txt', 'setup.py'] + found_requirements = False + + for req_file in requirements_files: + if Path(req_file).exists(): + found_requirements = True + break + + if not found_requirements: + print("โš ๏ธ No requirements.txt or setup.py found. Skipping Snyk scan.") + return 0 + + print("๐Ÿ” Running Snyk security scan...") + + try: + # Run Snyk test on Python dependencies + result = subprocess.run([ + 'snyk', 'test', + '--severity-threshold=high', + '--json' + ], capture_output=True, text=True, check=False) + + if result.returncode == 0: + print("โœ… Snyk scan completed - no high severity vulnerabilities found") + return 0 + else: + # Parse JSON output to show vulnerabilities + try: + vulns = json.loads(result.stdout) + if 'vulnerabilities' in vulns: + print("โŒ High severity vulnerabilities found:") + for vuln in vulns['vulnerabilities']: + if vuln.get('severity') == 'high': + print(f" - {vuln.get('title', 'Unknown')} in {vuln.get('packageName', 'Unknown')}") + print(f" CVSS Score: {vuln.get('cvssScore', 'N/A')}") + print(f" More info: {vuln.get('url', 'N/A')}") + print() + + print("๐Ÿ’ก To fix vulnerabilities, run: snyk wizard") + return 1 + except json.JSONDecodeError: + print("โŒ Snyk scan failed with errors:") + print(result.stderr) + return 1 + + except Exception as e: + print(f"โŒ Error running Snyk scan: {e}") + return 1 + + +if __name__ == '__main__': + sys.exit(run_snyk_scan()) diff --git a/.husky/hooks/talisman-check.sh b/.husky/hooks/talisman-check.sh new file mode 100755 index 0000000..9e0fd06 --- /dev/null +++ b/.husky/hooks/talisman-check.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +""" +Talisman secrets detection hook for Husky-style pre-commit setup. +This hook runs Talisman to detect potential secrets in commits. +""" + +# Check if Talisman is available +if ! command -v talisman &> /dev/null; then + echo "โŒ Talisman not found. Please install it first:" + echo " # macOS" + echo " brew install talisman" + echo " # Linux" + echo " curl -sL https://github.com/thoughtworks/talisman/releases/latest/download/talisman_linux_amd64 -o talisman" + echo " chmod +x talisman" + echo " sudo mv talisman /usr/local/bin/" + exit 1 +fi + +echo "๐Ÿ” Running Talisman secrets detection..." + +# Run Talisman with pre-commit hook +if talisman --githook pre-commit; then + echo "โœ… Talisman check passed - no secrets detected" + exit 0 +else + echo "โŒ Talisman found potential secrets in your changes" + echo "" + echo "๐Ÿ’ก To fix this:" + echo "1. Review the files mentioned above" + echo "2. Remove any actual secrets from your code" + echo "3. If the file contains legitimate test data, add it to .talismanrc:" + echo " talisman --checksum path/to/file" + echo " # Then add the checksum to .talismanrc" + exit 1 +fi diff --git a/.husky/hooks/test-runner.sh b/.husky/hooks/test-runner.sh new file mode 100755 index 0000000..3ab417e --- /dev/null +++ b/.husky/hooks/test-runner.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +""" +Test runner hook for Husky-style pre-push setup. +This hook runs tests and coverage checks before pushing. +""" + +echo "๐Ÿงช Running tests and coverage checks..." + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if pytest is available +if ! command_exists pytest; then + echo "โŒ pytest not found. Please install it first:" + echo " pip install pytest pytest-cov" + exit 1 +fi + +# Run tests +echo " - Running tests..." +if pytest tests/ -v; then + echo " โœ… All tests passed" +else + echo " โŒ Tests failed. Please fix before pushing." + exit 1 +fi + +# Run coverage check +echo " - Checking test coverage..." +if command_exists pytest; then + if pytest --cov=contentstack_management --cov-report=term-missing tests/; then + echo " โœ… Coverage check completed" + else + echo " โŒ Coverage check failed. Please improve test coverage." + exit 1 + fi +else + echo " โš ๏ธ pytest-cov not installed, skipping coverage check" + echo " ๐Ÿ’ก Install with: pip install pytest-cov" +fi + +echo "โœ… All test checks passed!" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..da3763f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,69 @@ +#!/usr/bin/env sh +# Pre-commit hook to run Talisman and Snyk scans, completing both before deciding to commit + +# Function to check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Check if Talisman is installed +if ! command_exists talisman; then + echo "Error: Talisman is not installed. Please install it and try again." + exit 1 +fi + +# Check if Snyk is installed +if ! command_exists snyk; then + echo "Error: Snyk is not installed. Please install it and try again." + exit 1 +fi + +# Allow bypassing the hook with an environment variable +if [ "$SKIP_HOOK" = "1" ]; then + echo "Skipping Talisman and Snyk scans (SKIP_HOOK=1)." + exit 0 +fi + +# Initialize variables to track scan results +talisman_failed=false +snyk_failed=false + +# Run Talisman secret scan +echo "Running Talisman secret scan..." +talisman --githook pre-commit > talisman_output.log 2>&1 +talisman_exit_code=$? + +if [ $talisman_exit_code -eq 0 ]; then + echo "Talisman scan passed: No secrets found." +else + echo "Talisman scan failed (exit code $talisman_exit_code). See talisman_output.log for details." + talisman_failed=true +fi + +# Run Snyk vulnerability scan (continues even if Talisman failed) +echo "Running Snyk vulnerability scan..." +snyk test --all-projects --fail-on=all > snyk_output.log 2>&1 +snyk_exit_code=$? + +if [ $snyk_exit_code -eq 0 ]; then + echo "Snyk scan passed: No vulnerabilities found." +elif [ $snyk_exit_code -eq 1 ]; then + echo "Snyk found vulnerabilities. See snyk_output.log for details." + snyk_failed=true +else + echo "Snyk scan failed with error (exit code $snyk_exit_code). See snyk_output.log for details." + snyk_failed=true +fi + +# Evaluate results after both scans +if [ "$talisman_failed" = true ] || [ "$snyk_failed" = true ]; then + echo "Commit aborted due to issues found in one or both scans." + [ "$talisman_failed" = true ] && echo "- Talisman issues: Check talisman_output.log" + [ "$snyk_failed" = true ] && echo "- Snyk issues: Check snyk_output.log" + exit 1 +fi + +# If both scans pass, allow the commit +echo "All scans passed. Proceeding with commit." +rm -f talisman_output.log snyk_output.log +exit 0 diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..d05ec42 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,29 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +echo "๐Ÿš€ Running pre-push checks..." + +# Run tests to ensure code quality +echo "๐Ÿงช Running tests..." +if ! python -m pytest tests/ -v; then + echo "โŒ Tests failed. Please fix before pushing." + exit 1 +fi + +# Run coverage check +echo "๐Ÿ“Š Checking test coverage..." +if ! python -m pytest --cov=contentstack_management --cov-report=term-missing tests/; then + echo "โŒ Coverage check failed. Please improve test coverage." + exit 1 +fi + +# Run security scan on dependencies +if [ -n "$SNYK_TOKEN" ]; then + echo "๐Ÿ” Running comprehensive Snyk scan..." + if ! snyk test --severity-threshold=high; then + echo "โŒ High severity vulnerabilities found. Please fix before pushing." + exit 1 + fi +fi + +echo "โœ… All pre-push checks passed!" diff --git a/.talismanrc b/.talismanrc index 5a277ec..93f0e9f 100644 --- a/.talismanrc +++ b/.talismanrc @@ -2,6 +2,8 @@ fileignoreconfig: - filename: .github/workflows/secrets-scan.yml ignore_detectors: - filecontent +- filename: .husky/pre-commit + checksum: 7a12030ddfea18d6f85edc25f1721fb2009df00fdd42bab66b05de25ab3e32b2 - filename: tests/config/default.yml.enc checksum: f05423216fcfe17716c01932e7a87775a029ec4f1fb1314bfdf0697527e67038 @@ -368,4 +370,22 @@ fileignoreconfig: checksum: f92e78bc9aa8fcdf4cf8490eaa81c7ce8ce518767d9becf1393e847cc2ee12c4 - filename: tests/unit/global_fields/test_global_fields_unittest.py checksum: 46297a6fbf321dfe4b85a3d2c1089614af8c9590d8352238c800ad69955b4b6a +- filename: .husky/pre-commit + checksum: 7a12030ddfea18d6f85edc25f1721fb2009df00fdd42bab66b05de25ab3e32b2 +version: "1.0" +fileignoreconfig: +- filename: tests/unit/variant_group/test_variant_group.py + checksum: 7c3d5ee03ed59891c20f2447c9d6da423b28855455639f6a1f299a03c1ebe744 +- filename: tests/unit/entry_variants/test_entry_variants.py + checksum: 4f0d963c7e153974486af64e642dcfbf2d967448abe2344bcc3bddfb898384dc +- filename: contentstack_management/variants/variants.py + checksum: c06b3471717db66b9e558c4c735a7690098839d87d96ef4b4be4c573b4a54709 +- filename: contentstack_management/entries/entry.py + checksum: 141933cd737709129bfe891cbbc5d19ed1435ac9611cc5fdf420087b98774ce9 +- filename: contentstack_management/entry_variants/entry_variants.py + checksum: a084c1b235d464def36854e662071c744b2f71abe33038ade439bbe313deea5d +- filename: contentstack_management/variant_group/variant_group.py + checksum: a1d4824626aea510cd80c9b1920ede40cdc374ee9a81169811ca7980a9b67b32 +- filename: tests/unit/variants/test_variants.py + checksum: 866f8b27d41694d0b2a7273c842b2b2b25cab6dac5919edb7d4d658bc0aa20cb version: "1.0" diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a86c0..908e001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # CHANGELOG ## Content Management SDK For Python +--- +## v1.5.0 + +#### Date: 25 August 2025 + +- Variants feature support. + --- ## v1.4.0 diff --git a/README.md b/README.md index 0d3ab80..5b90beb 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,32 @@ asset = client().stack(api_key='api_key').assets() result = asset.upload(asset) ``` +### Development Setup + +This repository includes Husky-style pre-commit hooks for security scanning and code quality checks. To set up the development environment: + +```bash +# Install required tools +brew install talisman +brew install snyk/tap/snyk + +# Set up Snyk authentication +export SNYK_TOKEN=your_snyk_token_here +snyk auth + +# Install git hooks +chmod +x .husky/pre-commit .husky/pre-push .husky/hooks/*.sh .husky/hooks/*.py +cp .husky/pre-commit .git/hooks/pre-commit +cp .husky/pre-push .git/hooks/pre-push +chmod +x .git/hooks/pre-commit .git/hooks/pre-push +``` + +### Security Features + +- **Talisman**: Detects secrets and sensitive data in commits +- **Snyk**: Scans Python dependencies for vulnerabilities +- **Code Quality**: Black, isort, flake8 for consistent code style + ### Helpful Links - [Contentstack Website](https://www.contentstack.com/) diff --git a/contentstack_management/__init__.py b/contentstack_management/__init__.py index 849cd80..a5a380a 100644 --- a/contentstack_management/__init__.py +++ b/contentstack_management/__init__.py @@ -16,6 +16,7 @@ from .auditlogs.auditlog import Auditlog from .environments.environment import Environment from .entries.entry import Entry +from .entry_variants.entry_variants import EntryVariants from .contentstack import Client, Region from ._api_client import _APIClient from .common import Parameter @@ -31,6 +32,8 @@ from .management_token.management_token import ManagementToken from .publish_queue.publish_queue import PublishQueue from .extensions.extension import Extension +from .variant_group.variant_group import VariantGroup +from .variants.variants import Variants __all__ = ( @@ -55,6 +58,7 @@ "Auditlog", "Environment", "Entry", +"EntryVariants", "Locale", "Taxonomy", "Label", @@ -65,14 +69,16 @@ "DeliveryToken", "ManagementToken", "PublishQueue", -"Extension" +"Extension", +"VariantGroup", +"Variants" ) __title__ = 'contentstack-management-python' __author__ = 'dev-ex' __status__ = 'debug' __region__ = 'na' -__version__ = '1.4.0' +__version__ = '1.5.0' __host__ = 'api.contentstack.io' __protocol__ = 'https://' __api_version__ = 'v3' diff --git a/contentstack_management/entries/entry.py b/contentstack_management/entries/entry.py index 25b01e9..b56377f 100644 --- a/contentstack_management/entries/entry.py +++ b/contentstack_management/entries/entry.py @@ -5,6 +5,7 @@ import json from ..common import Parameter +from ..entry_variants.entry_variants import EntryVariants class Entry(Parameter): """ @@ -421,13 +422,58 @@ def unpublish(self, data): data = json.dumps(data) return self.client.post(url, headers = self.client.headers, data = data, params = self.params) - - - - + def variants(self, variant_uid: str = None): + """ + Returns an EntryVariants instance for managing variant entries. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant. It is used to specify which variant to work with + :type variant_uid: str + :return: EntryVariants instance for managing variant entries + ------------------------------- + [Example:] - + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # Get all variant entries + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants().query().find().json() + >>> # Get specific variant entry + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').fetch().json() + ------------------------------- + """ + + return EntryVariants(self.client, self.content_type_uid, self.entry_uid, variant_uid) + def includeVariants(self, include_variants: str = 'true', variant_uid: str = None, params: dict = None): + """ + The includeVariants method retrieves the details of a specific base entry with variant details. + + :param include_variants: The `include_variants` parameter is a string that specifies whether to include variants + :type include_variants: str + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant. It is used to specify which variant to include + :type variant_uid: str + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: the result of the GET request made to the specified URL. + ------------------------------- + [Example:] + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').includeVariants('true', 'variant_uid').json() + >>> # With parameters + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').includeVariants('true', 'variant_uid', params={'locale': 'en-us'}).json() + ------------------------------- + """ + if self.entry_uid is None: + raise Exception('Entry uid is required') + if params is not None: + self.params.update(params) + self.params['include_variants'] = include_variants + if variant_uid is not None and variant_uid != '': + self.params['variant_uid'] = variant_uid + url = f"content_types/{self.content_type_uid}/entries/{self.entry_uid}" + return self.client.get(url, headers = self.client.headers, params = self.params) \ No newline at end of file diff --git a/contentstack_management/entry_variants/__init__.py b/contentstack_management/entry_variants/__init__.py new file mode 100644 index 0000000..218bf90 --- /dev/null +++ b/contentstack_management/entry_variants/__init__.py @@ -0,0 +1,3 @@ +from .entry_variants import EntryVariants + +__all__ = ['EntryVariants'] diff --git a/contentstack_management/entry_variants/entry_variants.py b/contentstack_management/entry_variants/entry_variants.py new file mode 100644 index 0000000..b2487a9 --- /dev/null +++ b/contentstack_management/entry_variants/entry_variants.py @@ -0,0 +1,272 @@ +"""This class takes a base URL as an argument when it's initialized, +which is the endpoint for the RESTFUL API that we'll be interacting with. +The query(), create(), fetch(), delete(), update(), versions(), and includeVariants() methods each correspond to +the operations that can be performed on the API """ + +import json +from ..common import Parameter +from .._errors import ArgumentException + +class EntryVariants(Parameter): + """ + This class takes a base URL as an argument when it's initialized, + which is the endpoint for the RESTFUL API that + we'll be interacting with. The query(), create(), fetch(), delete(), update(), versions(), and includeVariants() + methods each correspond to the operations that can be performed on the API """ + + def __init__(self, client, content_type_uid: str, entry_uid: str, variant_uid: str = None): + self.client = client + self.content_type_uid = content_type_uid + self.entry_uid = entry_uid + self.variant_uid = variant_uid + super().__init__(self.client) + self.path = f"content_types/{content_type_uid}/entries/{entry_uid}/variants" + + + def find(self, params: dict = None): + """ + The Find variant entries call fetches all the existing variant customizations for an entry. + + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: Json, with variant entry details. + + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack("api_key").content_types('content_type_uid').entry('entry_uid').variants().query().find().json() + >>> # With parameters + >>> result = client.stack("api_key").content_types('content_type_uid').entry('entry_uid').variants().find({'limit': 10, 'skip': 0}).json() + + ------------------------------- + """ + self.validate_content_type_uid() + self.validate_entry_uid() + if params is not None: + self.params.update(params) + return self.client.get(self.path, headers = self.client.headers, params = self.params) + + def create(self, data: dict): + """ + This call is used to create a variant entry for an entry. + + :param data: The `data` parameter is the payload that you want to send in the request body. It + should be a dictionary or a JSON serializable object that you want to send as the request body + :return: Json, with variant entry details. + + ------------------------------- + [Example:] + >>> data = { + >>> "customized_fields": [ + >>> "title", + >>> "url" + >>> ], + >>> "base_entry_version": 10, # optional + >>> "entry": { + >>> "title": "example", + >>> "url": "/example" + >>> } + >>> } + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants().create(data).json() + + ------------------------------- + """ + self.validate_content_type_uid() + self.validate_entry_uid() + data = json.dumps(data) + return self.client.post(self.path, headers = self.client.headers, data=data, params = self.params) + + def fetch(self, variant_uid: str = None, params: dict = None): + """ + The fetch Variant entry call fetches variant entry details. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + a variant. It is used to specify which variant to fetch from the server + :type variant_uid: str + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: the result of the GET request made to the specified URL. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').fetch().json() + >>> # With parameters + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').fetch(params={'include_count': True}).json() + + ------------------------------- + """ + + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + + self.validate_content_type_uid() + self.validate_entry_uid() + self.validate_variant_uid() + if params is not None: + self.params.update(params) + url = f"{self.path}/{self.variant_uid}" + return self.client.get(url, headers = self.client.headers, params = self.params) + + def delete(self, variant_uid: str = None): + """ + The delete a variant entry call is used to delete a specific variant entry. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant that you want to delete + :type variant_uid: str + :return: the result of the `client.delete()` method, which is likely a response object or a + boolean value indicating the success of the deletion operation. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').delete().json() + + ------------------------------- + """ + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + self.validate_content_type_uid() + self.validate_entry_uid() + self.validate_variant_uid() + url = f"{self.path}/{self.variant_uid}" + return self.client.delete(url, headers = self.client.headers, params = self.params) + + def update(self, data: dict, variant_uid: str = None): + """ + The update a variant entry call updates an entry of a selected variant entry. + + :param data: The `data` parameter is a dictionary that contains the updated information that you + want to send to the server. This data will be converted to a JSON string before sending it in + the request + :type data: dict + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant. It is used to specify which variant should be updated with the provided data + :type variant_uid: str + :return: the result of the `put` request made to the specified URL. + ------------------------------- + [Example:] + >>> data = { + >>> "customized_fields": [ + >>> "title", + >>> "url" + >>> ], + >>> "base_entry_version": 10, # optional + >>> "entry": { + >>> "title": "example", + >>> "url": "/example" + >>> } + >>> } + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').update(data).json() + + ------------------------------- + """ + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + self.validate_content_type_uid() + self.validate_entry_uid() + self.validate_variant_uid() + url = f"{self.path}/{self.variant_uid}" + data = json.dumps(data) + return self.client.put(url, headers = self.client.headers, data=data, params = self.params) + + def versions(self, variant_uid: str = None, params: dict = None): + """ + The version method retrieves the details of a specific variant entry version details. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant. It is used to specify which variant to get versions for + :type variant_uid: str + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: the result of the GET request made to the specified URL. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').versions().json() + >>> # With parameters + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').variants('variant_uid').versions(params={'limit': 10}).json() + + ------------------------------- + """ + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + self.validate_content_type_uid() + self.validate_entry_uid() + self.validate_variant_uid() + if params is not None: + self.params.update(params) + url = f"{self.path}/{self.variant_uid}/versions" + return self.client.get(url, headers = self.client.headers, params = self.params) + + def includeVariants(self, include_variants: str = 'true', variant_uid: str = None, params: dict = None): + """ + The includeVariants method retrieves the details of a specific base entry with variant details. + + :param include_variants: The `include_variants` parameter is a string that specifies whether to include variants + :type include_variants: str + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant. It is used to specify which variant to include + :type variant_uid: str + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: the result of the GET request made to the specified URL. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').includeVariants('true', 'variant_uid').json() + >>> # With parameters + >>> result = client.stack('api_key').content_types('content_type_uid').entry('entry_uid').includeVariants('true', 'variant_uid', params={'locale': 'en-us'}).json() + + ------------------------------- + """ + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + self.validate_content_type_uid() + self.validate_entry_uid() + self.validate_variant_uid() + if params is not None: + self.params.update(params) + self.params['include_variants'] = include_variants + url = f"content_types/{self.content_type_uid}/entries/{self.entry_uid}" + return self.client.get(url, headers = self.client.headers, params = self.params) + + def validate_content_type_uid(self): + """ + The function checks if the content_type_uid is None or an empty string and raises an ArgumentException + if it is. + """ + + if self.content_type_uid is None or self.content_type_uid == '': + raise ArgumentException("content type Uid is required") + + def validate_entry_uid(self): + """ + The function checks if the entry_uid is None or an empty string and raises an ArgumentException + if it is. + """ + + if self.entry_uid is None or self.entry_uid == '': + raise ArgumentException("entry Uid is required") + + def validate_variant_uid(self): + """ + The function checks if the variant_uid is None or an empty string and raises an ArgumentException + if it is. + """ + + if self.variant_uid is None or self.variant_uid == '': + raise ArgumentException("variant Uid is required") diff --git a/contentstack_management/stack/stack.py b/contentstack_management/stack/stack.py index 8fe4ee5..6ef142f 100644 --- a/contentstack_management/stack/stack.py +++ b/contentstack_management/stack/stack.py @@ -20,6 +20,8 @@ from ..management_token.management_token import ManagementToken from ..publish_queue.publish_queue import PublishQueue from ..extensions.extension import Extension +from ..variant_group.variant_group import VariantGroup +from ..variants.variants import Variants class Stack(Parameter): @@ -351,4 +353,10 @@ def publish_queue(self, publish_queue_uid: str = None): return PublishQueue(self.client, publish_queue_uid) def extension(self, extension_uid: str = None): - return Extension(self.client, extension_uid) \ No newline at end of file + return Extension(self.client, extension_uid) + + def variant_group(self, variant_group_uid: str = None): + return VariantGroup(self.client, variant_group_uid) + + def variants(self, variant_uid: str = None): + return Variants(self.client, None, variant_uid) \ No newline at end of file diff --git a/contentstack_management/variant_group/__init__.py b/contentstack_management/variant_group/__init__.py new file mode 100644 index 0000000..ce6b0cb --- /dev/null +++ b/contentstack_management/variant_group/__init__.py @@ -0,0 +1 @@ +import contentstack_management \ No newline at end of file diff --git a/contentstack_management/variant_group/variant_group.py b/contentstack_management/variant_group/variant_group.py new file mode 100644 index 0000000..f57d67b --- /dev/null +++ b/contentstack_management/variant_group/variant_group.py @@ -0,0 +1,221 @@ +"""This class takes a base URL as an argument when it's initialized, +which is the endpoint for the RESTFUL API that we'll be interacting with. +The create(), read(), update(), and delete() methods each correspond to +the CRUD operations that can be performed on the API """ + +import json +from ..common import Parameter +from .._errors import ArgumentException +from ..variants.variants import Variants + +class VariantGroup(Parameter): + """ + This class takes a base URL as an argument when it's initialized, + which is the endpoint for the RESTFUL API that + we'll be interacting with. The create(), read(), update(), and delete() + methods each correspond to the CRUD + operations that can be performed on the API """ + + def __init__(self, client, variant_group_uid: str = None): + self.client = client + self.variant_group_uid = variant_group_uid + super().__init__(self.client) + self.path = "variant_groups" + + def find(self): + """ + The Find variant group call fetches all the existing variant groups of the stack. + :return: Json, with variant group details. + + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack("api_key").variant_group().find().json() + + ------------------------------- + """ + return self.client.get(self.path, headers = self.client.headers, params = self.params) + + def query(self, query_params: dict = None): + """ + The Query on variant group will allow to fetch details of all or specific variant groups with filtering. + + :param query_params: The `query_params` parameter is a dictionary that contains query parameters for filtering + :type query_params: dict + :return: Json, with filtered variant group details. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack("api_key").variant_group().query({'name': 'Colors'}).find().json() + + ------------------------------- + """ + if query_params is not None: + self.params.update(query_params) + return self + + def fetch(self, variant_group_uid: str = None): + """ + The Get variant group call returns information about a particular variant group of a stack. + + :param variant_group_uid: The `variant_group_uid` parameter is a string that represents the unique identifier of + a variant group. It is used to specify which variant group to fetch from the server + :type variant_group_uid: str + :return: the result of the GET request made to the specified URL. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').variant_group('variant_group_uid').fetch().json() + + ------------------------------- + """ + + if variant_group_uid is not None and variant_group_uid != '': + self.variant_group_uid = variant_group_uid + + self.validate_uid() + url = f"{self.path}/{self.variant_group_uid}" + return self.client.get(url, headers = self.client.headers, params = self.params) + + def create(self, data: dict): + """ + This call is used to create a variant group. + + :param data: The `data` parameter is the payload that you want to send in the request body. It + should be a dictionary or a JSON serializable object that you want to send as the request body + :return: Json, with variant group details. + + ------------------------------- + [Example:] + >>> data = { + >>> "name": "Colors", + >>> "content_types": [ + >>> "iphone_product_page" + >>> ], + >>> "uid": "iphone_color_white" # optional + >>> } + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').variant_group().create(data).json() + + ------------------------------- + """ + + data = json.dumps(data) + return self.client.post(self.path, headers = self.client.headers, data=data, params = self.params) + + def update(self, data: dict, variant_group_uid: str = None): + """ + The "Update variant group" call is used to update an existing variant group. + + :param data: The `data` parameter is a dictionary that contains the updated information that you + want to send to the server. This data will be converted to a JSON string before sending it in + the request + :type data: dict + :param variant_group_uid: The `variant_group_uid` parameter is a string that represents the unique identifier of + the variant group. It is used to specify which variant group should be updated with the provided data + :type variant_group_uid: str + :return: the result of the `put` request made to the specified URL. + ------------------------------- + [Example:] + >>> data = { + >>> "name": "iPhone Colors", + >>> "content_types": [ + >>> {"uid": "iphone_product_page", "status": "linked"} + >>> ], + >>> "personalize_metadata": { + >>> "experience_uid": "variant_group_ex_uid_update", + >>> "experience_short_uid": "variant_group_short_uid_update", + >>> "project_uid": "variant_group_project_uid_update", + >>> "status": "linked" + >>> } + >>> } + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').variant_group("variant_group_uid").update(data).json() + + ------------------------------- + """ + if variant_group_uid is not None and variant_group_uid != '': + self.variant_group_uid = variant_group_uid + self.validate_uid() + url = f"{self.path}/{self.variant_group_uid}" + data = json.dumps(data) + return self.client.put(url, headers = self.client.headers, data=data, params = self.params) + + def delete(self, variant_group_uid: str = None): + """ + The "Delete variant group" call is used to delete a specific variant group. + + :param variant_group_uid: The `variant_group_uid` parameter is a string that represents the unique identifier of + the variant group that you want to delete + :type variant_group_uid: str + :return: the result of the `client.delete()` method, which is likely a response object or a + boolean value indicating the success of the deletion operation. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> result = client.stack('api_key').variant_group('variant_group_uid').delete().json() + + ------------------------------- + """ + if variant_group_uid is not None and variant_group_uid != '': + self.variant_group_uid = variant_group_uid + self.validate_uid() + url = f"{self.path}/{self.variant_group_uid}" + return self.client.delete(url, headers = self.client.headers, params = self.params) + + def link_contenttypes(self, data: dict, variant_group_uid: str = None): + if variant_group_uid is not None and variant_group_uid != '': + self.variant_group_uid = variant_group_uid + self.validate_uid() + url = f"{self.path}/{self.variant_group_uid}" + data = json.dumps(data) + return self.client.put(url, headers = self.client.headers, data=data, params = self.params) + + def unlink_contenttypes(self, data: dict, variant_group_uid: str = None): + if variant_group_uid is not None and variant_group_uid != '': + self.variant_group_uid = variant_group_uid + self.validate_uid() + url = f"{self.path}/{self.variant_group_uid}" + data = json.dumps(data) + return self.client.put(url, headers = self.client.headers, data=data, params = self.params) + + def variants(self, variant_uid: str = None): + """ + Returns a Variants instance for managing variants within this variant group. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + a variant. It is used to specify which variant to work with + :type variant_uid: str + :return: Variants instance for managing variants + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # Get all variants + >>> result = client.stack('api_key').variant_group('variant_group_uid').variants().find().json() + >>> # Get specific variant + >>> result = client.stack('api_key').variant_group('variant_group_uid').variants('variant_uid').fetch().json() + + ------------------------------- + """ + return Variants(self.client, self.variant_group_uid, variant_uid) + + def validate_uid(self): + """ + The function checks if the variant_group_uid is None or an empty string and raises an ArgumentException + if it is. + """ + + if self.variant_group_uid is None or self.variant_group_uid == '': + raise ArgumentException("variant group Uid is required") \ No newline at end of file diff --git a/contentstack_management/variants/__init__.py b/contentstack_management/variants/__init__.py new file mode 100644 index 0000000..25920d2 --- /dev/null +++ b/contentstack_management/variants/__init__.py @@ -0,0 +1,3 @@ +from .variants import Variants + +__all__ = ['Variants'] \ No newline at end of file diff --git a/contentstack_management/variants/variants.py b/contentstack_management/variants/variants.py new file mode 100644 index 0000000..b19a8d7 --- /dev/null +++ b/contentstack_management/variants/variants.py @@ -0,0 +1,256 @@ +"""This class takes a base URL as an argument when it's initialized, +which is the endpoint for the RESTFUL API that we'll be interacting with. +The create(), read(), update(), and delete() methods each correspond to +the CRUD operations that can be performed on the API """ + +import json +from ..common import Parameter +from .._errors import ArgumentException + +class Variants(Parameter): + """ + This class takes a base URL as an argument when it's initialized, + which is the endpoint for the RESTFUL API that + we'll be interacting with. The create(), read(), update(), and delete() + methods each correspond to the CRUD + operations that can be performed on the API """ + + def __init__(self, client, variant_group_uid: str = None, variant_uid: str = None): + self.client = client + self.variant_group_uid = variant_group_uid + self.variant_uid = variant_uid + super().__init__(self.client) + if self.variant_group_uid: + self.path = f"variant_groups/{self.variant_group_uid}/variants" + else: + self.path = "variants" + + def find(self, params: dict = None): + """ + The Find variants call fetches all the existing variants of a variant group or ungrouped variants. + + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: Json, with variant details. + + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants + >>> result = client.stack("api_key").variant_group('variant_group_uid').variants().find().json() + >>> # For ungrouped variants + >>> result = client.stack("api_key").variants().find().json() + >>> # With parameters + >>> result = client.stack("api_key").variants().find({'limit': 10, 'skip': 0}).json() + + ------------------------------- + """ + if self.variant_group_uid: + self.validate_variant_group_uid() + if params is not None: + self.params.update(params) + return self.client.get(self.path, headers = self.client.headers, params = self.params) + + def query(self, query_params: dict = None): + """ + The Query on variants will allow to fetch details of all or specific variants with filtering. + + :param query_params: The `query_params` parameter is a dictionary that contains query parameters for filtering + :type query_params: dict + :return: Json, with filtered variant details. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants with query + >>> result = client.stack("api_key").variant_group('variant_group_uid').variants().query({'title': 'variant title'}).find().json() + >>> # For ungrouped variants with query + >>> result = client.stack("api_key").variants().query({'title': 'variant title'}).find().json() + + ------------------------------- + """ + if query_params is not None: + self.params.update(query_params) + return self + + def fetch(self, variant_uid: str = None, params: dict = None): + """ + The Get variant call returns information about a particular variant of a variant group or ungrouped variant. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + a variant. It is used to specify which variant to fetch from the server + :type variant_uid: str + :param params: The `params` parameter is a dictionary that contains query parameters to be sent with the request + :type params: dict + :return: the result of the GET request made to the specified URL. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants + >>> result = client.stack('api_key').variant_group('variant_group_uid').variants('variant_uid').fetch().json() + >>> # For ungrouped variants + >>> result = client.stack('api_key').variants('variant_uid').fetch().json() + >>> # With parameters + >>> result = client.stack('api_key').variants('variant_uid').fetch(params={'include_count': True}).json() + + ------------------------------- + """ + + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + + if self.variant_group_uid: + self.validate_variant_group_uid() + self.validate_variant_uid() + if params is not None: + self.params.update(params) + url = f"{self.path}/{self.variant_uid}" + return self.client.get(url, headers = self.client.headers, params = self.params) + + def create(self, data: dict): + """ + This call is used to create a variant within a variant group or as an ungrouped variant. + + :param data: The `data` parameter is the payload that you want to send in the request body. It + should be a dictionary or a JSON serializable object that you want to send as the request body + :return: Json, with variant details. + + ------------------------------- + [Example:] + >>> data = { + >>> "uid": "iphone_color_white", # optional + >>> "name": "White" + >>> } + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants + >>> result = client.stack('api_key').variant_group('variant_group_uid').variants().create(data).json() + >>> # For ungrouped variants + >>> result = client.stack('api_key').variants().create(data).json() + + ------------------------------- + """ + if self.variant_group_uid: + self.validate_variant_group_uid() + data = json.dumps(data) + return self.client.post(self.path, headers = self.client.headers, data=data, params = self.params) + + def update(self, data: dict, variant_uid: str = None): + """ + The "Update variant" call is used to update an existing variant. + + :param data: The `data` parameter is a dictionary that contains the updated information that you + want to send to the server. This data will be converted to a JSON string before sending it in + the request + :type data: dict + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant. It is used to specify which variant should be updated with the provided data + :type variant_uid: str + :return: the result of the `put` request made to the specified URL. + ------------------------------- + [Example:] + >>> data = { + >>> "name": "updated name" + >>> } + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants + >>> result = client.stack('api_key').variant_group("variant_group_uid").variants('variant_uid').update(data).json() + >>> # For ungrouped variants + >>> result = client.stack('api_key').variants('variant_uid').update(data).json() + + ------------------------------- + """ + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + if self.variant_group_uid: + self.validate_variant_group_uid() + self.validate_variant_uid() + url = f"{self.path}/{self.variant_uid}" + data = json.dumps(data) + return self.client.put(url, headers = self.client.headers, data=data, params = self.params) + + def delete(self, variant_uid: str = None): + """ + The "Delete variant" call is used to delete a specific variant. + + :param variant_uid: The `variant_uid` parameter is a string that represents the unique identifier of + the variant that you want to delete + :type variant_uid: str + :return: the result of the `client.delete()` method, which is likely a response object or a + boolean value indicating the success of the deletion operation. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants + >>> result = client.stack('api_key').variant_group('variant_group_uid').variants('variant_uid').delete().json() + >>> # For ungrouped variants + >>> result = client.stack('api_key').variants('variant_uid').delete().json() + + ------------------------------- + """ + if variant_uid is not None and variant_uid != '': + self.variant_uid = variant_uid + if self.variant_group_uid: + self.validate_variant_group_uid() + self.validate_variant_uid() + url = f"{self.path}/{self.variant_uid}" + return self.client.delete(url, headers = self.client.headers, params = self.params) + + def fetchByUIDs(self, variant_uids: list): + """ + The fetchByUIDs on variant will allow to fetch specific variants by their UIDs. + + :param variant_uids: The `variant_uids` parameter is a list of strings that represents the unique identifiers of + the variants that you want to fetch + :type variant_uids: list + :return: Json, with variant details for the specified UIDs. + ------------------------------- + [Example:] + + >>> import contentstack_management + >>> client = contentstack_management.Client(authtoken='your_authtoken') + >>> # For grouped variants + >>> result = client.stack('api_key').variant_group('variant_group_uid').variants().fetchByUIDs(['uid1', 'uid2']).json() + >>> # For ungrouped variants + >>> result = client.stack('api_key').variants().fetchByUIDs(['uid1', 'uid2']).json() + + ------------------------------- + """ + if not isinstance(variant_uids, list) or len(variant_uids) == 0: + raise ArgumentException("variant_uids must be a non-empty list") + + if self.variant_group_uid: + self.validate_variant_group_uid() + + # Convert list to comma-separated string + uids_param = ','.join(variant_uids) + params = self.params.copy() + params['uid'] = uids_param + + return self.client.get(self.path, headers = self.client.headers, params = params) + + def validate_variant_group_uid(self): + """ + The function checks if the variant_group_uid is None or an empty string and raises an ArgumentException + if it is. + """ + + if self.variant_group_uid is None or self.variant_group_uid == '': + raise ArgumentException("variant group Uid is required") + + def validate_variant_uid(self): + """ + The function checks if the variant_uid is None or an empty string and raises an ArgumentException + if it is. + """ + + if self.variant_uid is None or self.variant_uid == '': + raise ArgumentException("variant Uid is required") \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 6c883a7..de5768f 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,6 +7,7 @@ # pytest --cov=contentstack # pytest -v --cov=contentstack --cov-report=html # pytest --html=tests/report/test-report.html +# Sanity: PYTHONPATH=. pytest tests/unit/ --html=tests/report/test-report.html from unittest import TestLoader, TestSuite from .api.aliases.test_alias import AliaseApiTests diff --git a/tests/cred.py b/tests/cred.py index 85e6a1a..3f28543 100644 --- a/tests/cred.py +++ b/tests/cred.py @@ -40,6 +40,8 @@ default_management_token_uid = "management_token_uid" #default management token uid default_publish_queue_uid = "publish_queue_uid" # default publish queue uid default_extension_uid = "extension_uid" # default publish queue uid +default_variant_group_uid = "variant_group_uid" # default variant group uid +default_variant_uid = "variant_uid" # default variant uid def get_credentials(): load_dotenv() @@ -85,6 +87,8 @@ def get_credentials(): "delivery_token_uid": os.getenv("DELIVERY_TOKEN_UID", default_delivery_token_uid), "management_token_uid": os.getenv("MANAGEMENT_TOKEN_UID", default_management_token_uid), "publish_queue_uid": os.getenv("PUBLISH_QUEUE_UID", default_publish_queue_uid), - "extension_uid": os.getenv("EXTENSION_UID", default_extension_uid) + "extension_uid": os.getenv("EXTENSION_UID", default_extension_uid), + "variant_group_uid": os.getenv("VARIANT_GROUP_UID", default_variant_group_uid), + "variant_uid": os.getenv("VARIANT_UID", default_variant_uid) } return credentials diff --git a/tests/unit/entry_variants/__init__.py b/tests/unit/entry_variants/__init__.py new file mode 100644 index 0000000..5c05611 --- /dev/null +++ b/tests/unit/entry_variants/__init__.py @@ -0,0 +1 @@ +# Unit tests for entry_variants module diff --git a/tests/unit/entry_variants/test_entry_variants.py b/tests/unit/entry_variants/test_entry_variants.py new file mode 100644 index 0000000..2eabed9 --- /dev/null +++ b/tests/unit/entry_variants/test_entry_variants.py @@ -0,0 +1,169 @@ +import unittest +import contentstack_management +from tests.cred import get_credentials + +credentials = get_credentials() +username = credentials["username"] +password = credentials["password"] +host = credentials["host"] +api_key = credentials["api_key"] +content_type_uid = credentials["content_type_uid"] +entry_uid = credentials["entry_uid"] +variant_uid = credentials["variant_uid"] + +class EntryVariantsUnitTests(unittest.TestCase): + + def setUp(self): + self.client = contentstack_management.Client(host=host) + self.client.login(username, password) + + def test_find_all_entry_variants(self): + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants().find() + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_find_entry_variants_with_params(self): + params = {"limit": 10, "skip": 0} + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants().find(params) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants?limit=10&skip=0") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_query_entry_variants(self): + query = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants() + query.add_param("title", "variant title") + response = query.find() + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants?title=variant+title") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_create_entry_variant(self): + data = { + "customized_fields": [ + "title", + "url" + ], + "base_entry_version": 10, + "entry": { + "title": "example", + "url": "/example" + } + } + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants().create(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants") + self.assertEqual(response.request.method, "POST") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_fetch_entry_variant(self): + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants(variant_uid).fetch() + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_entry_variant_with_params(self): + params = {"include_count": True} + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants(variant_uid).fetch(params=params) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}?include_count=True") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_update_entry_variant(self): + data = { + "customized_fields": [ + "title", + "url", + "description" + ], + "entry": { + "title": "updated example", + "url": "/updated-example", + "description": "Updated description" + } + } + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants(variant_uid).update(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}") + self.assertEqual(response.request.method, "PUT") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_delete_entry_variant(self): + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants(variant_uid).delete() + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}") + self.assertEqual(response.request.method, "DELETE") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_versions_entry_variant(self): + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants(variant_uid).versions() + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}/versions") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_versions_entry_variant_with_params(self): + params = {"limit": 10, "skip": 0} + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants(variant_uid).versions(params=params) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}/variants/{variant_uid}/versions?limit=10&skip=0") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_include_variants(self): + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).includeVariants('true', variant_uid) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}?include_variants=true&variant_uid={variant_uid}") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_include_variants_with_params(self): + params = {"limit": 10, "skip": 0} + response = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).includeVariants('true', variant_uid, params) + self.assertEqual(response.request.url, f"{self.client.endpoint}content_types/{content_type_uid}/entries/{entry_uid}?limit=10&skip=0&include_variants=true&variant_uid={variant_uid}") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_validate_content_type_uid_with_valid_uid(self): + entry_variants = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants() + # This should not raise an exception + try: + entry_variants.validate_content_type_uid() + except Exception as e: + self.fail(f"validate_content_type_uid() raised {type(e).__name__} unexpectedly!") + + def test_validate_content_type_uid_with_invalid_uid(self): + entry_variants = self.client.stack(api_key).content_types("").entry(entry_uid).variants() + with self.assertRaises(Exception): + entry_variants.validate_content_type_uid() + + def test_validate_content_type_uid_with_none_uid(self): + # Create entry_variants instance directly to test validation + entry_variants = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants() + entry_variants.content_type_uid = None + with self.assertRaises(Exception): + entry_variants.validate_content_type_uid() + + def test_validate_entry_uid_with_valid_uid(self): + entry_variants = self.client.stack(api_key).content_types(content_type_uid).entry(entry_uid).variants() + # This should not raise an exception + try: + entry_variants.validate_entry_uid() + except Exception as e: + self.fail(f"validate_entry_uid() raised {type(e).__name__} unexpectedly!") + + def test_validate_entry_uid_with_invalid_uid(self): + entry_variants = self.client.stack(api_key).content_types(content_type_uid).entry("").variants() + with self.assertRaises(Exception): + entry_variants.validate_entry_uid() + + def test_validate_entry_uid_with_none_uid(self): + entry_variants = self.client.stack(api_key).content_types(content_type_uid).entry(None).variants() + with self.assertRaises(Exception): + entry_variants.validate_entry_uid() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/variant_group/__init__.py b/tests/unit/variant_group/__init__.py new file mode 100644 index 0000000..9e54a9e --- /dev/null +++ b/tests/unit/variant_group/__init__.py @@ -0,0 +1 @@ +# Unit tests for variant_group module diff --git a/tests/unit/variant_group/test_variant_group.py b/tests/unit/variant_group/test_variant_group.py new file mode 100644 index 0000000..fd68cd0 --- /dev/null +++ b/tests/unit/variant_group/test_variant_group.py @@ -0,0 +1,115 @@ +import unittest +import contentstack_management +from tests.cred import get_credentials + +credentials = get_credentials() +username = credentials["username"] +password = credentials["password"] +host = credentials["host"] +api_key = credentials["api_key"] +variant_group_uid = credentials["variant_group_uid"] + +class VariantGroupUnitTests(unittest.TestCase): + + def setUp(self): + self.client = contentstack_management.Client(host=host) + self.client.login(username, password) + + def test_find_all_variant_groups(self): + response = self.client.stack(api_key).variant_group().find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_find_variant_groups_with_params(self): + query = self.client.stack(api_key).variant_group() + query.add_param("limit", 10) + query.add_param("skip", 0) + response = query.find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups?limit=10&skip=0") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_query_variant_groups(self): + query_params = {"name": "Colors"} + response = self.client.stack(api_key).variant_group().query(query_params).find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups?name=Colors") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_variant_group(self): + response = self.client.stack(api_key).variant_group(variant_group_uid).fetch() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_variant_group_with_params(self): + query = self.client.stack(api_key).variant_group(variant_group_uid) + query.add_param("include_count", True) + response = query.fetch() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}?include_count=True") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_create_variant_group(self): + data = { + "variant_group": { + "name": "Colors", + "content_types": [ + "iphone_product_page" + ], + "description": "Color variants for product pages" + } + } + response = self.client.stack(api_key).variant_group().create(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups") + self.assertEqual(response.request.method, "POST") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_update_variant_group(self): + data = { + "variant_group": { + "name": "Updated Colors", + "content_types": [ + "iphone_product_page", + "android_product_page" + ], + "description": "Updated color variants for product pages" + } + } + response = self.client.stack(api_key).variant_group(variant_group_uid).update(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}") + self.assertEqual(response.request.method, "PUT") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_delete_variant_group(self): + response = self.client.stack(api_key).variant_group(variant_group_uid).delete() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}") + self.assertEqual(response.request.method, "DELETE") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_validate_uid_with_valid_uid(self): + variant_group = self.client.stack(api_key).variant_group(variant_group_uid) + # This should not raise an exception + try: + variant_group.validate_uid() + except Exception as e: + self.fail(f"validate_uid() raised {type(e).__name__} unexpectedly!") + + def test_validate_uid_with_invalid_uid(self): + variant_group = self.client.stack(api_key).variant_group("") + with self.assertRaises(Exception): + variant_group.validate_uid() + + def test_validate_uid_with_none_uid(self): + variant_group = self.client.stack(api_key).variant_group(None) + with self.assertRaises(Exception): + variant_group.validate_uid() + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/variants/__init__.py b/tests/unit/variants/__init__.py new file mode 100644 index 0000000..d4e0366 --- /dev/null +++ b/tests/unit/variants/__init__.py @@ -0,0 +1 @@ +# Unit tests for variants module diff --git a/tests/unit/variants/test_variants.py b/tests/unit/variants/test_variants.py new file mode 100644 index 0000000..c0af998 --- /dev/null +++ b/tests/unit/variants/test_variants.py @@ -0,0 +1,178 @@ +import unittest +import contentstack_management +from tests.cred import get_credentials + +credentials = get_credentials() +username = credentials["username"] +password = credentials["password"] +host = credentials["host"] +api_key = credentials["api_key"] +variant_group_uid = credentials["variant_group_uid"] +variant_uid = credentials["variant_uid"] + +class VariantsUnitTests(unittest.TestCase): + + def setUp(self): + self.client = contentstack_management.Client(host=host) + self.client.login(username, password) + + def test_find_all_variants(self): + response = self.client.stack(api_key).variants().find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variants") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_find_variants_with_params(self): + params = {"limit": 10, "skip": 0} + response = self.client.stack(api_key).variants().find(params) + self.assertEqual(response.request.url, f"{self.client.endpoint}variants?limit=10&skip=0") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_find_grouped_variants(self): + response = self.client.stack(api_key).variant_group(variant_group_uid).variants().find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_find_grouped_variants_with_params(self): + params = {"limit": 10, "skip": 0} + response = self.client.stack(api_key).variant_group(variant_group_uid).variants().find(params) + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants?limit=10&skip=0") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_query_variants(self): + query_params = {"title": "variant title"} + response = self.client.stack(api_key).variants().query(query_params).find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variants?title=variant+title") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_query_grouped_variants(self): + query_params = {"title": "variant title"} + response = self.client.stack(api_key).variant_group(variant_group_uid).variants().query(query_params).find() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants?title=variant+title") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_variant(self): + response = self.client.stack(api_key).variants(variant_uid).fetch() + self.assertEqual(response.request.url, f"{self.client.endpoint}variants/{variant_uid}") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_variant_with_params(self): + params = {"include_count": True} + response = self.client.stack(api_key).variants(variant_uid).fetch(params=params) + self.assertEqual(response.request.url, f"{self.client.endpoint}variants/{variant_uid}?include_count=True") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_grouped_variant(self): + response = self.client.stack(api_key).variant_group(variant_group_uid).variants(variant_uid).fetch() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants/{variant_uid}") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_fetch_grouped_variant_with_params(self): + params = {"include_count": True} + response = self.client.stack(api_key).variant_group(variant_group_uid).variants(variant_uid).fetch(params=params) + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants/{variant_uid}?include_count=True") + self.assertEqual(response.request.method, "GET") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + self.assertEqual(response.request.body, None) + + def test_create_variant(self): + data = { + "variant": { + "title": "Red Variant", + "description": "Red color variant for products", + "is_default": False + } + } + response = self.client.stack(api_key).variants().create(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}variants") + self.assertEqual(response.request.method, "POST") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_create_grouped_variant(self): + data = { + "variant": { + "title": "Red Variant", + "description": "Red color variant for products", + "is_default": False + } + } + response = self.client.stack(api_key).variant_group(variant_group_uid).variants().create(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants") + self.assertEqual(response.request.method, "POST") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_update_variant(self): + data = { + "variant": { + "title": "Updated Red Variant", + "description": "Updated red color variant for products", + "is_default": True + } + } + response = self.client.stack(api_key).variants(variant_uid).update(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}variants/{variant_uid}") + self.assertEqual(response.request.method, "PUT") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_update_grouped_variant(self): + data = { + "variant": { + "title": "Updated Red Variant", + "description": "Updated red color variant for products", + "is_default": True + } + } + response = self.client.stack(api_key).variant_group(variant_group_uid).variants(variant_uid).update(data) + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants/{variant_uid}") + self.assertEqual(response.request.method, "PUT") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_delete_variant(self): + response = self.client.stack(api_key).variants(variant_uid).delete() + self.assertEqual(response.request.url, f"{self.client.endpoint}variants/{variant_uid}") + self.assertEqual(response.request.method, "DELETE") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_delete_grouped_variant(self): + response = self.client.stack(api_key).variant_group(variant_group_uid).variants(variant_uid).delete() + self.assertEqual(response.request.url, f"{self.client.endpoint}variant_groups/{variant_group_uid}/variants/{variant_uid}") + self.assertEqual(response.request.method, "DELETE") + self.assertEqual(response.request.headers["Content-Type"], "application/json") + + def test_validate_variant_group_uid_with_valid_uid(self): + variants = self.client.stack(api_key).variant_group(variant_group_uid).variants() + # This should not raise an exception + try: + variants.validate_variant_group_uid() + except Exception as e: + self.fail(f"validate_variant_group_uid() raised {type(e).__name__} unexpectedly!") + + def test_validate_variant_group_uid_with_invalid_uid(self): + variants = self.client.stack(api_key).variant_group("").variants() + with self.assertRaises(Exception): + variants.validate_variant_group_uid() + + def test_validate_variant_group_uid_with_none_uid(self): + variants = self.client.stack(api_key).variant_group(None).variants() + with self.assertRaises(Exception): + variants.validate_variant_group_uid() + +if __name__ == '__main__': + unittest.main()