kiba-core 0.5.3.dev70__tar.gz → 0.5.3.dev71__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/CHANGELOG.md +2 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/logging.py +18 -8
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/kiba_core.egg-info/PKG-INFO +1 -1
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/kiba_core.egg-info/SOURCES.txt +1 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/pyproject.toml +1 -1
- kiba_core-0.5.3.dev71/tests/test_logging.py +259 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/.github/pull_request_template.md +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/.github/workflows/deploy.yml +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/.github/workflows/pull-request.yml +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/.github/workflows/release.yml +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/.gitignore +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/Dockerfile +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/README.md +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/api_request.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/api_response.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/authorizer.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/default_routes.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/health.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/json_route.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/database_connection_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/logging_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/server_headers_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/openapi.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/rate_limit.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/request_context.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/route.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/route_auth.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/route_metadata.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/aws_requester.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/caching/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/caching/cache.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/caching/dict_cache.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/caching/file_cache.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/cron/api_request_cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/cron/cron_executor.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/cron/cron_job_processor.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/cron/model.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/exceptions.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/http/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/http/basic_authentication.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/http/jwt.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/http/rest_method.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/locks/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/locks/lock.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/locks/model.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/locks/sql.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/notifications/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/notifications/discord_client.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/notifications/notification_client.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/notifications/slack_client.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/py.typed +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/aqs.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/cosmos.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/message_queue.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/message_queue_processor.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/model.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/sql.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/queues/sqs.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/requester/requester.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/s3_manager.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/service_client.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/store/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/store/database.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/store/retriever.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/store/saver.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/async_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/chain_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/date_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/dict_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/file_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/hashing_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/http_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/json_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/list_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/process_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/string_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/typing_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/url_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/util/value_holder.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/web3/eth_client.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/web3/multicall3.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/kiba_core.egg-info/dependency_links.txt +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/kiba_core.egg-info/requires.txt +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/kiba_core.egg-info/top_level.txt +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/makefile +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/setup.cfg +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_api_response.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_authorizer.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_exception_handling_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_json_route.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_openapi.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_origin_ip_middleware.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_rate_limit.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_request_context.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_route.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_streaming_json_route.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/caching/test_dict_cache.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/caching/test_file_cache.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/cron/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/cron/test_cron_executor.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/locks/test_sql_lock.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/queues/conftest.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/queues/test_aqs_queue.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/queues/test_cosmos_queue.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/queues/test_sql_queue.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/queues/test_sqs_queue.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/requester/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/requester/test_requester.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/test_database.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/test_exceptions.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_async_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_chain_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_date_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_dict_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_file_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_json_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_list_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_process_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_string_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/util/test_url_util.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/web3/__init__.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/web3/test_rest_eth_client.py +0 -0
- {kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/uv.lock +0 -0
|
@@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- [PATCH] Fixed log records from `logging.info`, `warning`, `error`, `critical`, `debug`, `exception`, `stat` and `api` recording their location as `core/logging.py` instead of the calling code; this was only visible in `init_logging`'s text format, since the JSON and basic formats don't output the location
|
|
12
|
+
- [MINOR] Updated `init_logging`, `init_json_logging` and `init_basic_logging` to set the loggers of the third-party libraries core uses (`DEFAULT_EXTERNAL_LOGGER_NAMES`: httpx2, httpcore2, sqlalchemy, aiosqlite, asyncpg, botocore, aiobotocore, azure, apscheduler, web3) to `WARNING`; pass `extraLoggerNames` to quieten more loggers on top of these, or `loggerNames` to replace them (`loggerNames=[]` keeps the previous behaviour). `init_external_loggers` takes the same arguments and now defaults to the same list
|
|
11
13
|
- [MINOR] Added `core.cron` (install with the `cron` extra): `CronJob` schedules (interval with jitter, or crontab) that declare a `jobType`, `CronJobProcessor`s that declare the `jobTypes` they process, and `CronExecutor`, which takes a list of processors, routes each job to the processor for its type (failing at startup if a job type has no processor or more than one), runs jobs on an APScheduler scheduler, logs each run as a `CRON` api log with its status and duration, and keeps running when a job fails; also added `ApiRequestCronJob` with `ApiRequestCronJobProcessor` (needs the `requester` extra) for jobs that call a URL
|
|
12
14
|
- [MAJOR] Updated `Requester` to raise `RequesterTimeoutException` (a `GatewayTimeoutException`, 504) instead of `httpx` timeout exceptions; it carries the method, the URL without credentials or query string, the timeout, how long the request ran and the `httpx` timeout type, and keeps the original exception as its cause. Code catching `httpx.TimeoutException` (or `ReadTimeout`, `ConnectTimeout`, …) around `Requester` calls must catch `RequesterTimeoutException` instead
|
|
13
15
|
- [PATCH] Updated `MessageQueueProcessor` failure notifications to show the exception type before its message, so failures from exceptions without text (e.g. `httpx` timeouts) are still identifiable
|
|
@@ -120,36 +120,43 @@ def init_logger(logger: Logger, loggingLevel: int, handler: StreamHandler) -> No
|
|
|
120
120
|
logger.setLevel(level=loggingLevel)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
# NOTE(krishan711): third-party libraries pulled in by core's extras; their info-level logs drown out the app's own
|
|
124
|
+
DEFAULT_EXTERNAL_LOGGER_NAMES = ('httpx2', 'httpcore2', 'sqlalchemy', 'aiosqlite', 'asyncpg', 'botocore', 'aiobotocore', 'azure', 'apscheduler', 'web3')
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def init_external_loggers(loggerNames: Collection[str] | None = None, extraLoggerNames: Collection[str] | None = None, loggingLevel: int = logging.WARNING) -> None:
|
|
128
|
+
for loggerName in {*(loggerNames if loggerNames is not None else DEFAULT_EXTERNAL_LOGGER_NAMES), *(extraLoggerNames or [])}:
|
|
125
129
|
logging.getLogger(loggerName).setLevel(loggingLevel)
|
|
126
130
|
|
|
127
131
|
|
|
128
|
-
def init_logging(name: str, version: str, environment: str, showDebug: bool = False, requestIdHolder: RequestIdHolder | None = None) -> None:
|
|
132
|
+
def init_logging(name: str, version: str, environment: str, showDebug: bool = False, requestIdHolder: RequestIdHolder | None = None, loggerNames: Collection[str] | None = None, extraLoggerNames: Collection[str] | None = None) -> None:
|
|
129
133
|
loggingLevel = logging.DEBUG if showDebug else logging.INFO
|
|
130
134
|
for logFormat in ALL_LOGGER_FORMATS:
|
|
131
135
|
logger = logging.getLogger(name=logFormat.loggerType)
|
|
132
136
|
handler = StreamHandler(stream=sys.stdout)
|
|
133
137
|
handler.setFormatter(fmt=KibaLoggingFormatter(logFormat=logFormat, name=name, version=version, environment=environment, requestIdHolder=requestIdHolder))
|
|
134
138
|
init_logger(logger=logger, loggingLevel=loggingLevel, handler=handler)
|
|
139
|
+
init_external_loggers(loggerNames=loggerNames, extraLoggerNames=extraLoggerNames)
|
|
135
140
|
|
|
136
141
|
|
|
137
|
-
def init_json_logging(name: str, version: str, environment: str, showDebug: bool = False, requestIdHolder: RequestIdHolder | None = None) -> None:
|
|
142
|
+
def init_json_logging(name: str, version: str, environment: str, showDebug: bool = False, requestIdHolder: RequestIdHolder | None = None, loggerNames: Collection[str] | None = None, extraLoggerNames: Collection[str] | None = None) -> None:
|
|
138
143
|
loggingLevel = logging.DEBUG if showDebug else logging.INFO
|
|
139
144
|
for logFormat in ALL_LOGGER_FORMATS:
|
|
140
145
|
logger = logging.getLogger(name=logFormat.loggerType)
|
|
141
146
|
handler = StreamHandler(stream=sys.stdout)
|
|
142
147
|
handler.setFormatter(fmt=KibaJsonLoggingFormatter(logFormat=logFormat, name=name, version=version, environment=environment, requestIdHolder=requestIdHolder))
|
|
143
148
|
init_logger(logger=logger, loggingLevel=loggingLevel, handler=handler)
|
|
149
|
+
init_external_loggers(loggerNames=loggerNames, extraLoggerNames=extraLoggerNames)
|
|
144
150
|
|
|
145
151
|
|
|
146
|
-
def init_basic_logging(showDebug: bool = False) -> None:
|
|
152
|
+
def init_basic_logging(showDebug: bool = False, loggerNames: Collection[str] | None = None, extraLoggerNames: Collection[str] | None = None) -> None:
|
|
147
153
|
loggingLevel = logging.DEBUG if showDebug else logging.INFO
|
|
148
154
|
for logFormat in ALL_LOGGER_FORMATS:
|
|
149
155
|
logger = logging.getLogger(name=logFormat.loggerType)
|
|
150
156
|
handler = StreamHandler(stream=sys.stdout)
|
|
151
157
|
handler.setFormatter(fmt=Formatter(fmt=logFormat.loggerFormat))
|
|
152
158
|
init_logger(logger=logger, loggingLevel=loggingLevel, handler=handler)
|
|
159
|
+
init_external_loggers(loggerNames=loggerNames, extraLoggerNames=extraLoggerNames)
|
|
153
160
|
|
|
154
161
|
|
|
155
162
|
def _serialize_numeric_value(value: Union[float, None]) -> str:
|
|
@@ -168,7 +175,8 @@ def stat(name: str, key: str, value: float = 1) -> None:
|
|
|
168
175
|
nameValue = _serialize_string_value(value=str(name))
|
|
169
176
|
keyValue = _serialize_string_value(value=str(key))
|
|
170
177
|
statValue = _serialize_numeric_value(value=value)
|
|
171
|
-
|
|
178
|
+
# NOTE(krishan711): stacklevel=2 skips this function so records point at the caller
|
|
179
|
+
STAT_LOGGER.log(level=logging.INFO, msg='', extra={'statName': nameValue, 'statKey': keyValue, 'statValue': statValue}, stacklevel=2)
|
|
172
180
|
|
|
173
181
|
|
|
174
182
|
# TODO(krishan711): make pathPattern mandatory in next major release
|
|
@@ -184,6 +192,7 @@ def api(action: str, path: str, query: str, pathPattern: str | None = None, resp
|
|
|
184
192
|
level=logging.INFO,
|
|
185
193
|
msg='',
|
|
186
194
|
extra={'apiAction': actionString, 'apiPath': pathString, 'apiPathPattern': pathPatternString, 'apiQuery': queryString, 'apiResponse': responseString or '', 'apiDuration': durationString or ''},
|
|
195
|
+
stacklevel=2,
|
|
187
196
|
)
|
|
188
197
|
|
|
189
198
|
|
|
@@ -197,9 +206,10 @@ INFO = logging.INFO
|
|
|
197
206
|
DEBUG = logging.DEBUG
|
|
198
207
|
|
|
199
208
|
|
|
200
|
-
def _log(level: int, msg: str, *args: Any, **kwargs: Any) -> None: # type: ignore[explicit-any]
|
|
209
|
+
def _log(level: int, msg: str, *args: Any, stacklevel: int = 1, **kwargs: Any) -> None: # type: ignore[explicit-any]
|
|
201
210
|
if ROOT_LOGGER.isEnabledFor(level=level):
|
|
202
|
-
|
|
211
|
+
# NOTE(krishan711): +2 skips this function and the public wrapper (info, error, ...) so records point at the caller
|
|
212
|
+
ROOT_LOGGER._log(level=level, msg=msg, args=args, stacklevel=stacklevel + 2, **kwargs) # noqa: SLF001
|
|
203
213
|
|
|
204
214
|
|
|
205
215
|
def critical(msg: str, *args: Any, **kwargs: Any) -> None: # type: ignore[explicit-any]
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import logging as pythonLogging
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from core import logging
|
|
9
|
+
from core.exceptions import NotFoundException
|
|
10
|
+
from core.util.value_holder import RequestIdHolder
|
|
11
|
+
|
|
12
|
+
APP_LOGGER_NAME = 'test-app-library'
|
|
13
|
+
CORE_LOGGER_NAMES = [logFormat.loggerType for logFormat in logging.ALL_LOGGER_FORMATS]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True)
|
|
17
|
+
def restore_logging_state() -> Iterator[None]:
|
|
18
|
+
externalLoggerNames = [*logging.DEFAULT_EXTERNAL_LOGGER_NAMES, APP_LOGGER_NAME]
|
|
19
|
+
originalExternalLevels = {loggerName: pythonLogging.getLogger(loggerName).level for loggerName in externalLoggerNames}
|
|
20
|
+
originalCoreState = {loggerName: (pythonLogging.getLogger(loggerName).handlers, pythonLogging.getLogger(loggerName).level, pythonLogging.getLogger(loggerName).propagate) for loggerName in CORE_LOGGER_NAMES}
|
|
21
|
+
for loggerName in externalLoggerNames:
|
|
22
|
+
pythonLogging.getLogger(loggerName).setLevel(pythonLogging.NOTSET)
|
|
23
|
+
yield
|
|
24
|
+
for loggerName, level in originalExternalLevels.items():
|
|
25
|
+
pythonLogging.getLogger(loggerName).setLevel(level)
|
|
26
|
+
for loggerName, (handlers, level, propagate) in originalCoreState.items():
|
|
27
|
+
logger = pythonLogging.getLogger(loggerName)
|
|
28
|
+
logger.handlers, logger.propagate = handlers, propagate
|
|
29
|
+
logger.setLevel(level)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _levels(loggerNames: list[str]) -> list[int]:
|
|
33
|
+
return [pythonLogging.getLogger(loggerName).level for loggerName in loggerNames]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _lines(capsys: pytest.CaptureFixture[str]) -> list[str]:
|
|
37
|
+
return capsys.readouterr().out.splitlines()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _json_lines(capsys: pytest.CaptureFixture[str]) -> list[dict[str, object]]:
|
|
41
|
+
return [json.loads(line) for line in _lines(capsys)]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class TestExternalLoggers:
|
|
45
|
+
def test_init_quietens_core_external_loggers_by_default(self) -> None:
|
|
46
|
+
logging.init_basic_logging()
|
|
47
|
+
assert _levels(['httpx2', 'sqlalchemy', APP_LOGGER_NAME]) == [pythonLogging.WARNING, pythonLogging.WARNING, pythonLogging.NOTSET]
|
|
48
|
+
|
|
49
|
+
def test_extra_logger_names_are_added_to_the_defaults(self) -> None:
|
|
50
|
+
logging.init_basic_logging(extraLoggerNames=[APP_LOGGER_NAME])
|
|
51
|
+
assert _levels(['httpx2', APP_LOGGER_NAME]) == [pythonLogging.WARNING, pythonLogging.WARNING]
|
|
52
|
+
|
|
53
|
+
def test_logger_names_replace_the_defaults(self) -> None:
|
|
54
|
+
logging.init_basic_logging(loggerNames=[APP_LOGGER_NAME])
|
|
55
|
+
assert _levels(['httpx2', APP_LOGGER_NAME]) == [pythonLogging.NOTSET, pythonLogging.WARNING]
|
|
56
|
+
|
|
57
|
+
def test_every_init_function_applies_the_logger_names(self) -> None:
|
|
58
|
+
logging.init_logging(name='app', version='v1', environment='test', extraLoggerNames=[APP_LOGGER_NAME])
|
|
59
|
+
assert _levels(['httpx2', APP_LOGGER_NAME]) == [pythonLogging.WARNING, pythonLogging.WARNING]
|
|
60
|
+
pythonLogging.getLogger(APP_LOGGER_NAME).setLevel(pythonLogging.NOTSET)
|
|
61
|
+
logging.init_json_logging(name='app', version='v1', environment='test', loggerNames=[APP_LOGGER_NAME])
|
|
62
|
+
assert pythonLogging.getLogger(APP_LOGGER_NAME).level == pythonLogging.WARNING
|
|
63
|
+
|
|
64
|
+
def test_init_external_loggers_uses_the_given_level(self) -> None:
|
|
65
|
+
logging.init_external_loggers(extraLoggerNames=[APP_LOGGER_NAME], loggingLevel=pythonLogging.ERROR)
|
|
66
|
+
assert _levels(['httpx2', APP_LOGGER_NAME]) == [pythonLogging.ERROR, pythonLogging.ERROR]
|
|
67
|
+
|
|
68
|
+
def test_external_logger_level_applies_to_child_loggers(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
69
|
+
logging.init_basic_logging()
|
|
70
|
+
pythonLogging.getLogger('sqlalchemy.engine').info('noisy query log')
|
|
71
|
+
pythonLogging.getLogger('sqlalchemy.engine').warning('important warning')
|
|
72
|
+
assert _lines(capsys) == ['important warning']
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TestBasicLogging:
|
|
76
|
+
def test_logs_info_and_above_but_not_debug(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
77
|
+
logging.init_basic_logging()
|
|
78
|
+
logging.debug('debug line')
|
|
79
|
+
logging.info('info line')
|
|
80
|
+
logging.warning('warning line')
|
|
81
|
+
logging.error('error line')
|
|
82
|
+
logging.critical('critical line')
|
|
83
|
+
assert _lines(capsys) == ['info line', 'warning line', 'error line', 'critical line']
|
|
84
|
+
|
|
85
|
+
def test_show_debug_includes_debug(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
86
|
+
logging.init_basic_logging(showDebug=True)
|
|
87
|
+
logging.debug('debug line')
|
|
88
|
+
assert _lines(capsys) == ['debug line']
|
|
89
|
+
|
|
90
|
+
def test_messages_are_formatted_with_args(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
91
|
+
logging.init_basic_logging()
|
|
92
|
+
logging.info('hello %s, you are %d', 'kiba', 3)
|
|
93
|
+
assert _lines(capsys) == ['hello kiba, you are 3']
|
|
94
|
+
|
|
95
|
+
def test_core_loggers_do_not_propagate_to_avoid_duplicate_output(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
96
|
+
logging.init_basic_logging()
|
|
97
|
+
logging.init_basic_logging()
|
|
98
|
+
logging.stat(name='events', key='created')
|
|
99
|
+
assert _lines(capsys) == ['events:created:1']
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TestStat:
|
|
103
|
+
def test_logs_name_key_and_value(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
104
|
+
logging.init_basic_logging()
|
|
105
|
+
logging.stat(name='events', key='created', value=2.5)
|
|
106
|
+
assert _lines(capsys) == ['events:created:2.5']
|
|
107
|
+
|
|
108
|
+
def test_escapes_separators_and_rounds_values(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
109
|
+
logging.init_basic_logging()
|
|
110
|
+
logging.stat(name='db:query', key='user:read', value=1.23456789)
|
|
111
|
+
logging.stat(name='whole', key='number', value=3.0)
|
|
112
|
+
assert _lines(capsys) == ['db__query:user__read:1.234568', 'whole:number:3']
|
|
113
|
+
|
|
114
|
+
def test_nothing_is_logged_when_the_stat_logger_is_disabled(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
115
|
+
logging.init_basic_logging()
|
|
116
|
+
logging.STAT_LOGGER.setLevel(pythonLogging.WARNING)
|
|
117
|
+
logging.stat(name='events', key='created')
|
|
118
|
+
assert _lines(capsys) == []
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestApi:
|
|
122
|
+
def test_logs_action_path_query_response_and_duration(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
123
|
+
logging.init_basic_logging()
|
|
124
|
+
logging.api(action='GET', path='/v1/users', query='limit=10', response=200, duration=0.25)
|
|
125
|
+
assert _lines(capsys) == ['GET:/v1/users:limit=10:200:0.25']
|
|
126
|
+
|
|
127
|
+
def test_missing_response_and_duration_are_empty(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
128
|
+
logging.init_basic_logging()
|
|
129
|
+
logging.api(action='CRON', path='job', query='')
|
|
130
|
+
assert _lines(capsys) == ['CRON:job:::']
|
|
131
|
+
|
|
132
|
+
def test_escapes_separators(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
133
|
+
logging.init_basic_logging()
|
|
134
|
+
logging.api(action='MESSAGE', path='a:b', query='x=1:2')
|
|
135
|
+
assert _lines(capsys) == ['MESSAGE:a__b:x=1__2::']
|
|
136
|
+
|
|
137
|
+
def test_nothing_is_logged_when_the_api_logger_is_disabled(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
138
|
+
logging.init_basic_logging()
|
|
139
|
+
logging.API_LOGGER.setLevel(pythonLogging.WARNING)
|
|
140
|
+
logging.api(action='GET', path='/', query='')
|
|
141
|
+
assert _lines(capsys) == []
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class TestException:
|
|
145
|
+
def test_logs_kiba_exception_message_with_traceback(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
146
|
+
logging.init_basic_logging()
|
|
147
|
+
try:
|
|
148
|
+
raise NotFoundException(message='user not found')
|
|
149
|
+
except NotFoundException as exception:
|
|
150
|
+
logging.exception(exception)
|
|
151
|
+
lines = _lines(capsys)
|
|
152
|
+
assert lines[0] == 'user not found'
|
|
153
|
+
assert lines[1] == 'Traceback (most recent call last):'
|
|
154
|
+
assert lines[-1].endswith("message='user not found')")
|
|
155
|
+
|
|
156
|
+
def test_logs_plain_exception_text(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
157
|
+
logging.init_basic_logging()
|
|
158
|
+
try:
|
|
159
|
+
raise ValueError('bad value')
|
|
160
|
+
except ValueError as exception:
|
|
161
|
+
logging.exception(exception)
|
|
162
|
+
lines = _lines(capsys)
|
|
163
|
+
assert (lines[0], lines[-1]) == ('bad value', 'ValueError: bad value')
|
|
164
|
+
|
|
165
|
+
def test_logs_string_message_without_traceback_when_disabled(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
166
|
+
logging.init_basic_logging()
|
|
167
|
+
logging.exception('something failed', exc_info=False)
|
|
168
|
+
assert _lines(capsys) == ['something failed']
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestTextLogging:
|
|
172
|
+
def test_includes_service_details_request_id_level_and_location(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
173
|
+
requestIdHolder = RequestIdHolder()
|
|
174
|
+
requestIdHolder.set_value(value='req-123')
|
|
175
|
+
logging.init_logging(name='app', version='v1', environment='test', requestIdHolder=requestIdHolder)
|
|
176
|
+
logging.warning('hello')
|
|
177
|
+
parts = _lines(capsys)[0].split(' - ')
|
|
178
|
+
datetime.datetime.strptime(parts[0], '%Y-%m-%dT%H:%M:%S.%f') # noqa: DTZ007
|
|
179
|
+
assert parts[1:8] == ['KIBA_1', 'app', 'v1', 'test', 'req-123', 'WARNING', 'root']
|
|
180
|
+
assert parts[8].startswith('tests.test_logging.py:test_includes_service_details_request_id_level_and_location:')
|
|
181
|
+
assert parts[9] == 'hello'
|
|
182
|
+
|
|
183
|
+
def test_stat_api_and_exception_report_the_caller_location(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
184
|
+
logging.init_logging(name='app', version='v1', environment='test')
|
|
185
|
+
logging.stat(name='events', key='created')
|
|
186
|
+
logging.api(action='GET', path='/', query='')
|
|
187
|
+
logging.exception('failed', exc_info=False)
|
|
188
|
+
locations = [line.split(' - ')[8].rsplit(':', 1)[0] for line in _lines(capsys)]
|
|
189
|
+
assert locations == ['tests.test_logging.py:test_stat_api_and_exception_report_the_caller_location'] * 3
|
|
190
|
+
|
|
191
|
+
def test_uses_each_loggers_own_format(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
192
|
+
logging.init_logging(name='app', version='v1', environment='test')
|
|
193
|
+
logging.stat(name='events', key='created')
|
|
194
|
+
logging.api(action='GET', path='/', query='', response=200)
|
|
195
|
+
statParts, apiParts = (line.split(' - ') for line in _lines(capsys))
|
|
196
|
+
assert (statParts[1], statParts[-1]) == ('KIBA_STAT_1', 'events:created:1')
|
|
197
|
+
assert (apiParts[1], apiParts[-1]) == ('KIBA_API_1', 'GET:/::200:')
|
|
198
|
+
|
|
199
|
+
def test_request_id_is_empty_without_a_holder(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
200
|
+
logging.init_logging(name='app', version='v1', environment='test')
|
|
201
|
+
logging.info('hello')
|
|
202
|
+
assert _lines(capsys)[0].split(' - ')[5] == ''
|
|
203
|
+
|
|
204
|
+
def test_show_debug_includes_debug(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
205
|
+
logging.init_logging(name='app', version='v1', environment='test')
|
|
206
|
+
logging.debug('hidden')
|
|
207
|
+
logging.init_logging(name='app', version='v1', environment='test', showDebug=True)
|
|
208
|
+
logging.debug('shown')
|
|
209
|
+
assert [line.split(' - ')[-1] for line in _lines(capsys)] == ['shown']
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class TestJsonLogging:
|
|
213
|
+
def test_writes_one_json_object_per_log_with_service_details(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
214
|
+
requestIdHolder = RequestIdHolder()
|
|
215
|
+
requestIdHolder.set_value(value='req-123')
|
|
216
|
+
logging.init_json_logging(name='app', version='v1', environment='test', requestIdHolder=requestIdHolder)
|
|
217
|
+
logging.info('hello %s', 'kiba')
|
|
218
|
+
record = _json_lines(capsys)[0]
|
|
219
|
+
loggedDate = datetime.datetime.strptime(str(record.pop('date')), '%Y-%m-%dT%H:%M:%S.%f').replace(tzinfo=datetime.UTC)
|
|
220
|
+
assert abs((datetime.datetime.now(tz=datetime.UTC) - loggedDate).total_seconds()) < 60 # noqa: PLR2004
|
|
221
|
+
# NOTE(krishan711): the json formatter logs the unformatted msg, not msg % args
|
|
222
|
+
assert record == {'message': 'hello %s', 'level': 'INFO', 'logger': 'root', 'format': 'KIBA_1', 'name': 'app', 'version': 'v1', 'environment': 'test', 'requestId': 'req-123'}
|
|
223
|
+
|
|
224
|
+
def test_request_id_is_null_without_a_holder(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
225
|
+
logging.init_json_logging(name='app', version='v1', environment='test')
|
|
226
|
+
logging.info('hello')
|
|
227
|
+
assert _json_lines(capsys)[0]['requestId'] is None
|
|
228
|
+
|
|
229
|
+
def test_exceptions_log_the_traceback_as_the_message(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
230
|
+
logging.init_json_logging(name='app', version='v1', environment='test')
|
|
231
|
+
try:
|
|
232
|
+
raise ValueError('bad value')
|
|
233
|
+
except ValueError as exception:
|
|
234
|
+
logging.exception(exception)
|
|
235
|
+
record = _json_lines(capsys)[0]
|
|
236
|
+
assert (record['level'], str(record['message']).splitlines()[0], str(record['message']).splitlines()[-1]) == ('ERROR', 'Traceback (most recent call last):', 'ValueError: bad value')
|
|
237
|
+
|
|
238
|
+
def test_stat_fields_are_typed(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
239
|
+
logging.init_json_logging(name='app', version='v1', environment='test')
|
|
240
|
+
logging.stat(name='events', key='created', value=2.5)
|
|
241
|
+
record = _json_lines(capsys)[0]
|
|
242
|
+
assert {key: record[key] for key in ('format', 'message', 'statName', 'statKey', 'statValue')} == {'format': 'KIBA_STAT_1', 'message': None, 'statName': 'events', 'statKey': 'created', 'statValue': 2.5}
|
|
243
|
+
|
|
244
|
+
def test_api_fields_are_typed_and_missing_values_are_null(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
245
|
+
logging.init_json_logging(name='app', version='v1', environment='test')
|
|
246
|
+
logging.api(action='GET', path='/v1/users/123', pathPattern='/v1/users/{userId}', query='limit=10', response=200, duration=0.25)
|
|
247
|
+
logging.api(action='CRON', path='job', query='')
|
|
248
|
+
fields = ('format', 'apiAction', 'apiPath', 'apiPathPattern', 'apiQuery', 'apiResponse', 'apiDuration')
|
|
249
|
+
completeRecord, startRecord = ({key: record[key] for key in fields} for record in _json_lines(capsys))
|
|
250
|
+
assert completeRecord == {'format': 'KIBA_API_1', 'apiAction': 'GET', 'apiPath': '/v1/users/123', 'apiPathPattern': '/v1/users/{userId}', 'apiQuery': 'limit=10', 'apiResponse': 200, 'apiDuration': 0.25}
|
|
251
|
+
assert startRecord == {'format': 'KIBA_API_1', 'apiAction': 'CRON', 'apiPath': 'job', 'apiPathPattern': 'job', 'apiQuery': None, 'apiResponse': None, 'apiDuration': None}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class TestJsonFieldParsers:
|
|
255
|
+
def test_empty_values_parse_to_none(self) -> None:
|
|
256
|
+
assert (logging.json_parse_string_value(''), logging.json_parse_int_value(''), logging.json_parse_float_value('')) == (None, None, None)
|
|
257
|
+
|
|
258
|
+
def test_values_parse_to_their_types(self) -> None:
|
|
259
|
+
assert (logging.json_parse_string_value('abc'), logging.json_parse_int_value('42'), logging.json_parse_float_value('1.5')) == ('abc', 42, 1.5)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/exception_handling_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/core/api/middleware/server_headers_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kiba_core-0.5.3.dev70 → kiba_core-0.5.3.dev71}/tests/api/test_exception_handling_middleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|