diff --git a/CHANGELOG.md b/CHANGELOG.md index f525a1f..aa5e5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## _v2.0.0_ + +### **Date: 28-APRIL-2025** + +- Custom logger support + ## _v1.11.2_ ### **Date: 21-APRIL-2025** diff --git a/contentstack/__init__.py b/contentstack/__init__.py index 25fd958..a9b3190 100644 --- a/contentstack/__init__.py +++ b/contentstack/__init__.py @@ -22,7 +22,7 @@ __title__ = 'contentstack-delivery-python' __author__ = 'contentstack' __status__ = 'debug' -__version__ = 'v1.11.2' +__version__ = 'v2.0.0' __endpoint__ = 'cdn.contentstack.io' __email__ = 'mobile@contentstack.com' __developer_email__ = 'shailesh.mishra@contentstack.com' diff --git a/contentstack/asset.py b/contentstack/asset.py index 601f406..cd21586 100644 --- a/contentstack/asset.py +++ b/contentstack/asset.py @@ -7,13 +7,10 @@ import logging from urllib import parse -log = logging.getLogger(__name__) - - class Asset: r"""`Asset` refer to all the media files (images, videos, PDFs, audio files, and so on).""" - def __init__(self, http_instance, uid=None): + def __init__(self, http_instance, uid=None, logger=None): self.http_instance = http_instance self.asset_params = {} self.__uid = uid @@ -22,6 +19,7 @@ def __init__(self, http_instance, uid=None): self.base_url = f'{self.http_instance.endpoint}/assets/{self.__uid}' if 'environment' in self.http_instance.headers: self.asset_params['environment'] = self.http_instance.headers['environment'] + self.logger = logger or logging.getLogger(__name__) def environment(self, environment): r"""Provide the name of the environment if you wish to retrieve the assets published diff --git a/contentstack/assetquery.py b/contentstack/assetquery.py index 110045a..ecdc34b 100644 --- a/contentstack/assetquery.py +++ b/contentstack/assetquery.py @@ -9,15 +9,12 @@ from contentstack.basequery import BaseQuery from contentstack.utility import Utils -log = logging.getLogger(__name__) - - class AssetQuery(BaseQuery): """ This call fetches the list of all the assets of a particular stack. """ - def __init__(self, http_instance): + def __init__(self, http_instance, logger=None): super().__init__() self.http_instance = http_instance self.asset_query_params = {} @@ -25,6 +22,7 @@ def __init__(self, http_instance): if "environment" in self.http_instance.headers: env = self.http_instance.headers["environment"] self.base_url = f"{self.base_url}?environment={env}" + self.logger = logger or logging.getLogger(__name__) def environment(self, environment): r"""Provide the name of the environment if you wish to retrieve the assets published diff --git a/contentstack/basequery.py b/contentstack/basequery.py index 169a8bc..f2d9fca 100644 --- a/contentstack/basequery.py +++ b/contentstack/basequery.py @@ -1,9 +1,6 @@ import enum import logging -log = logging.getLogger(__name__) - - class QueryOperation(enum.Enum): """ QueryOperation is enum that Provides Options to perform operation to query the result. @@ -38,9 +35,10 @@ class BaseQuery: Common Query class works for Query As well as Asset """ - def __init__(self): + def __init__(self, logger=None): self.parameters = {} self.query_params = {} + self.logger = logger or logging.getLogger(__name__) def where(self, field_uid: str, query_operation: QueryOperation, fields=None): """ diff --git a/contentstack/contenttype.py b/contentstack/contenttype.py index c6eb2f4..363a206 100644 --- a/contentstack/contenttype.py +++ b/contentstack/contenttype.py @@ -14,9 +14,6 @@ from contentstack.entry import Entry from contentstack.query import Query -log = logging.getLogger(__name__) - - class ContentType: """ Content type defines the structure or schema of a page or a @@ -26,10 +23,11 @@ class ContentType: content type. """ - def __init__(self, http_instance, content_type_uid): + def __init__(self, http_instance, content_type_uid, logger=None): self.http_instance = http_instance self.__content_type_uid = content_type_uid self.local_param = {} + self.logger = logger or logging.getLogger(__name__) def entry(self, entry_uid: str): r""" diff --git a/contentstack/entry.py b/contentstack/entry.py index 260a52a..7cfcb13 100644 --- a/contentstack/entry.py +++ b/contentstack/entry.py @@ -9,9 +9,6 @@ from contentstack.deep_merge_lp import DeepMergeMixin from contentstack.entryqueryable import EntryQueryable -log = logging.getLogger(__name__) - - class Entry(EntryQueryable): """ An entry is the actual piece of content that you want to publish. @@ -23,7 +20,7 @@ class Entry(EntryQueryable): locale={locale_code} """ - def __init__(self, http_instance, content_type_uid, entry_uid): + def __init__(self, http_instance, content_type_uid, entry_uid, logger=None): super().__init__() EntryQueryable.__init__(self) self.entry_param = {} @@ -31,6 +28,7 @@ def __init__(self, http_instance, content_type_uid, entry_uid): self.content_type_id = content_type_uid self.entry_uid = entry_uid self.base_url = self.__get_base_url() + self.logger = logger or logging.getLogger(__name__) def environment(self, environment): """ diff --git a/contentstack/entryqueryable.py b/contentstack/entryqueryable.py index cb917c6..16ac6c7 100644 --- a/contentstack/entryqueryable.py +++ b/contentstack/entryqueryable.py @@ -4,16 +4,14 @@ """ import logging -log = logging.getLogger(__name__) - - class EntryQueryable: """ This class is base class for the Entry and Query class that shares common functions """ - def __init__(self): + def __init__(self, logger=None): self.entry_queryable_param = {} + self.logger = logger or logging.getLogger(__name__) def locale(self, locale: str): """ diff --git a/contentstack/https_connection.py b/contentstack/https_connection.py index 1caf336..e95ba8e 100644 --- a/contentstack/https_connection.py +++ b/contentstack/https_connection.py @@ -9,9 +9,6 @@ import contentstack from contentstack.controller import get_request -log = logging.getLogger(__name__) - - def __get_os_platform(): os_platform = platform.system() if os_platform == 'Darwin': diff --git a/contentstack/image_transform.py b/contentstack/image_transform.py index 37a1901..dcac64d 100644 --- a/contentstack/image_transform.py +++ b/contentstack/image_transform.py @@ -8,8 +8,6 @@ import logging -log = logging.getLogger(__name__) - class ImageTransform: # pylint: disable=too-few-public-methods """ @@ -17,7 +15,7 @@ class ImageTransform: # pylint: disable=too-few-public-methods files """ - def __init__(self, http_instance, image_url, **kwargs): + def __init__(self, http_instance, image_url, logger=None, **kwargs): """ creates instance of the ImageTransform class :param httpInstance: instance of HttpsConnection @@ -35,6 +33,7 @@ def __init__(self, http_instance, image_url, **kwargs): self.http_instance = http_instance self.image_url = image_url self.image_params = kwargs + self.logger = logger or logging.getLogger(__name__) def get_url(self): """ diff --git a/contentstack/query.py b/contentstack/query.py index 4cbbc3e..40b9bbf 100644 --- a/contentstack/query.py +++ b/contentstack/query.py @@ -12,8 +12,6 @@ from contentstack.deep_merge_lp import DeepMergeMixin from contentstack.entryqueryable import EntryQueryable -log = logging.getLogger(__name__) - class QueryType(enum.Enum): """ @@ -40,7 +38,7 @@ class Query(BaseQuery, EntryQueryable): >>> result = query.locale('locale-code').excepts('field_uid').limit(4).skip(5).find() """ - def __init__(self, http_instance, content_type_uid): + def __init__(self, http_instance, content_type_uid, logger=None): super().__init__() EntryQueryable.__init__(self) self.content_type_uid = content_type_uid @@ -50,6 +48,7 @@ def __init__(self, http_instance, content_type_uid): 'You are not allowed here without content_type_uid') self.base_url = f'{self.http_instance.endpoint}/content_types/{self.content_type_uid}/entries' self.base_url = self.__get_base_url() + self.logger = logger or logging.getLogger(__name__) def __get_base_url(self, endpoint=''): if endpoint is not None and endpoint.strip(): # .strip() removes leading/trailing whitespace diff --git a/contentstack/stack.py b/contentstack/stack.py index 21086f8..30aabfa 100644 --- a/contentstack/stack.py +++ b/contentstack/stack.py @@ -9,7 +9,6 @@ from contentstack.https_connection import HTTPSConnection from contentstack.image_transform import ImageTransform -log = logging.getLogger(__name__) DEFAULT_HOST = 'cdn.contentstack.io' @@ -42,6 +41,7 @@ def __init__(self, api_key: str, delivery_token: str, environment: str, live_preview=None, branch=None, early_access = None, + logger=None, ): """ # Class that wraps the credentials of the authenticated user. Think of @@ -78,7 +78,7 @@ def __init__(self, api_key: str, delivery_token: str, environment: str, live_preview={enable=True, authorization='your auth token'}, retry_strategy= _strategy) ``` """ - logging.basicConfig(level=logging.DEBUG) + self.logger = logger or logging.getLogger(__name__) self.headers = {} self._query_params = {} self.sync_param = {} diff --git a/contentstack/utility.py b/contentstack/utility.py index 598db4a..4a49d4a 100644 --- a/contentstack/utility.py +++ b/contentstack/utility.py @@ -1,76 +1,93 @@ """ +Utility functions for logging and URL manipulation. Last modified by ishaileshmishra on 06/08/20. Copyright 2019 Contentstack. All rights reserved. """ import json import logging -from urllib.parse import urlencode, urljoin +from urllib.parse import urlencode -log = logging.getLogger(__name__) - -def config_logging(logging_type: logging.WARNING): +def setup_logging(logging_type=logging.INFO, filename='app.log'): """ - This is to create logging config - :param logging_type: Level of the logging - :return: basicConfig instance + Global one-time logging configuration. + Should be called from your main application entry point. """ logging.basicConfig( - filename='app.log', + filename=filename, level=logging_type, format='[%(asctime)s] {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s', - datefmt='%H:%M:%S' + datefmt='%Y-%m-%d %H:%M:%S' ) class Utils: - @staticmethod - def config_logging(): - """ Setting up logging """ - logging.basicConfig( - filename='report_log.log', - format='%(asctime)s - %(message)s', - level=logging.INFO - ) + def setup_logger(name="AppLogger", level=logging.INFO, filename='app.log'): + """ + Creates and configures a named logger with file and console output. + Prevents duplicate handlers. + """ + logger = logging.getLogger(name) + if not logger.handlers: + logger.setLevel(level) - @staticmethod - def setup_logger(): - """setup logger for the application""" - return logging.getLogger("Config") + formatter = logging.Formatter( + '[%(asctime)s] %(levelname)s - %(name)s - %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + file_handler = logging.FileHandler(filename) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + return logger @staticmethod - def log(message): - """this generates log message""" - logging.debug(message) + def log(message, level=logging.DEBUG): + """ + Log a message with the specified level. + Default is DEBUG. + """ + logger = logging.getLogger("AppLogger") + logger.log(level, message) @staticmethod def do_url_encode(params): """ - To encode url with query parameters - :param params: - :return: encoded url + Encode query parameters to URL-safe format. + :param params: Dictionary of parameters + :return: Encoded URL query string """ - return parse.urlencode(params) + if not isinstance(params, dict): + raise ValueError("params must be a dictionary") + return urlencode(params, doseq=True) @staticmethod - def get_complete_url(base_url: str, params: dict) -> str: + def get_complete_url(base_url: str, params: dict, skip_encoding=False) -> str: """ - Creates a complete URL using base_url and their respective parameters. - :param base_url: The base URL to which parameters are appended. - :param params: A dictionary of parameters to be included in the URL. - :return: A complete URL with encoded parameters. + Construct a full URL by combining base URL and encoded parameters. + Handles JSON stringification for the `query` key. + :param base_url: Base API URL + :param params: Dictionary of query parameters + :param skip_encoding: Set True to skip URL encoding + :return: Complete URL """ - # Ensure 'query' is properly serialized as a JSON string without extra quotes - if 'query' in params: + if not isinstance(base_url, str) or not isinstance(params, dict): + raise ValueError("base_url must be a string and params must be a dictionary") + + if 'query' in params and not skip_encoding: params["query"] = json.dumps(params["query"], separators=(',', ':')) - # Encode parameters - query_string = urlencode(params, doseq=True) - - # Join base_url and query_string - if '?' in base_url: - return f'{base_url}&{query_string}' + if not skip_encoding: + query_string = urlencode(params, doseq=True) else: - return f'{base_url}?{query_string}' + query_string = "&".join(f"{k}={v}" for k, v in params.items()) + + # Append with appropriate separator + return f'{base_url}&{query_string}' if '?' in base_url else f'{base_url}?{query_string}' diff --git a/tests/test_stack.py b/tests/test_stack.py index f141949..205c3f7 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1,7 +1,10 @@ import unittest import config import contentstack +import logging +import io from contentstack.stack import ContentstackRegion +from contentstack.stack import Stack API_KEY = config.APIKEY DELIVERY_TOKEN = config.DELIVERYTOKEN @@ -182,4 +185,23 @@ def test_22_check_early_access_headers(self): def test_23_get_early_access(self): stack = contentstack.Stack( config.APIKEY, config.DELIVERYTOKEN, config.ENVIRONMENT, early_access=["taxonomy", "teams"]) - self.assertEqual(self.early_access, stack.get_early_access) \ No newline at end of file + self.assertEqual(self.early_access, stack.get_early_access) + + def test_stack_with_custom_logger(self): + log_stream = io.StringIO() + custom_logger = logging.getLogger("contentstack.custom.test_logger") + custom_logger.setLevel(logging.INFO) + + if custom_logger.hasHandlers(): + custom_logger.handlers.clear() + + handler = logging.StreamHandler(log_stream) + formatter = logging.Formatter('%(levelname)s - %(name)s - %(message)s') + handler.setFormatter(formatter) + custom_logger.addHandler(handler) + Stack("api_key", "delivery_token", "dev", logger=custom_logger) + custom_logger.info("INFO - contentstack.custom.test_logger - Test log entry") + handler.flush() + logs = log_stream.getvalue() + print("\nCaptured Logs:\n", logs) + self.assertIn("INFO - contentstack.custom.test_logger - Test log entry", logs) \ No newline at end of file