From ab9a5f47d532c987ef94214354c91903a340dac4 Mon Sep 17 00:00:00 2001 From: "shining.wz" Date: Tue, 2 Jun 2026 16:16:56 +0800 Subject: [PATCH 1/2] Add log update and delete v2 APIs --- aliyun/log/__init__.py | 4 + aliyun/log/__init__.pyi | 4 + aliyun/log/deletelogsv2request.py | 114 +++++++++++++++++++++++++ aliyun/log/deletelogsv2request.pyi | 17 ++++ aliyun/log/deletelogsv2response.py | 23 +++++ aliyun/log/deletelogsv2response.pyi | 9 ++ aliyun/log/logclient.py | 94 +++++++++++++++++--- aliyun/log/logclient.pyi | 6 ++ aliyun/log/updatelogsrequest.py | 128 ++++++++++++++++++++++++++++ aliyun/log/updatelogsrequest.pyi | 21 +++++ aliyun/log/updatelogsresponse.py | 23 +++++ aliyun/log/updatelogsresponse.pyi | 9 ++ tests/unit/test_logclient_mock.py | 128 +++++++++++++++++++++++++++- 13 files changed, 567 insertions(+), 13 deletions(-) create mode 100644 aliyun/log/deletelogsv2request.py create mode 100644 aliyun/log/deletelogsv2request.pyi create mode 100644 aliyun/log/deletelogsv2response.py create mode 100644 aliyun/log/deletelogsv2response.pyi create mode 100644 aliyun/log/updatelogsrequest.py create mode 100644 aliyun/log/updatelogsrequest.pyi create mode 100644 aliyun/log/updatelogsresponse.py create mode 100644 aliyun/log/updatelogsresponse.pyi diff --git a/aliyun/log/__init__.py b/aliyun/log/__init__.py index 3075e20..52471f6 100755 --- a/aliyun/log/__init__.py +++ b/aliyun/log/__init__.py @@ -39,6 +39,10 @@ from .rebuild_index_response import * from .deletelogsrequest import * from .deletelogssresponse import * +from .deletelogsv2request import * +from .deletelogsv2response import * +from .updatelogsrequest import * +from .updatelogsresponse import * from .getdeletelogsstatusrequest import * from .getdeletelogsstatusresponse import * from .listdeletelogsstasksrequest import * diff --git a/aliyun/log/__init__.pyi b/aliyun/log/__init__.pyi index d519bd5..8a86130 100644 --- a/aliyun/log/__init__.pyi +++ b/aliyun/log/__init__.pyi @@ -39,6 +39,10 @@ from .proto import LogGroupRaw as LogGroup from .rebuild_index_response import CreateRebuildIndexResponse as CreateRebuildIndexResponse, GetRebuildIndexResponse as GetRebuildIndexResponse from .deletelogsrequest import DeleteLogsRequest as DeleteLogsRequest from .deletelogssresponse import DeleteLogsResponse as DeleteLogsResponse +from .deletelogsv2request import DeleteLogsV2Request as DeleteLogsV2Request +from .deletelogsv2response import DeleteLogsV2Response as DeleteLogsV2Response +from .updatelogsrequest import UpdateLogsRequest as UpdateLogsRequest +from .updatelogsresponse import UpdateLogsResponse as UpdateLogsResponse from .getdeletelogsstatusrequest import GetDeleteLogsStatusRequest as GetDeleteLogsStatusRequest from .getdeletelogsstatusresponse import GetDeleteLogsStatusResponse as GetDeleteLogsStatusResponse from .listdeletelogsstasksrequest import ListDeleteLogsTasksRequest as ListDeleteLogsTasksRequest diff --git a/aliyun/log/deletelogsv2request.py b/aliyun/log/deletelogsv2request.py new file mode 100644 index 0000000..86101f3 --- /dev/null +++ b/aliyun/log/deletelogsv2request.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + +from .logrequest import LogRequest +from .util import parse_timestamp + + +class DeleteLogsV2Request(LogRequest): + """The request used to delete logs from a logstore. + + :type project: string + :param project: project name + + :type logstore: string + :param logstore: logstore name + + :type fromTime: int/string + :param fromTime: the begin time + + :type toTime: int/string + :param toTime: the end time + + :type query: string + :param query: user defined query + + :type rowId: string + :param rowId: row id of the log + """ + + def __init__(self, project=None, logstore=None, fromTime=None, toTime=None, query=None, rowId=None): + LogRequest.__init__(self, project) + self.logstore = logstore + self.fromTime = parse_timestamp(fromTime) + self.toTime = parse_timestamp(toTime) + self.query = query + self.rowId = rowId + + def get_logstore(self): + """Get logstore name. + + :return: string, logstore name. + """ + return self.logstore if self.logstore else '' + + def set_logstore(self, logstore): + """Set logstore name. + + :type logstore: string + :param logstore: logstore name + """ + self.logstore = logstore + + def get_from(self): + """Get begin time. + + :return: int, begin time + """ + return self.fromTime + + def set_from(self, fromTime): + """Set begin time. + + :type fromTime: int/string + :param fromTime: begin time + """ + self.fromTime = parse_timestamp(fromTime) + + def get_to(self): + """Get end time. + + :return: int, end time + """ + return self.toTime + + def set_to(self, toTime): + """Set end time. + + :type toTime: int/string + :param toTime: end time + """ + self.toTime = parse_timestamp(toTime) + + def get_query(self): + """Get user defined query. + + :return: string, user defined query + """ + return self.query + + def set_query(self, query): + """Set user defined query. + + :type query: string + :param query: user defined query + """ + self.query = query + + def get_row_id(self): + """Get row id. + + :return: string, row id + """ + return self.rowId + + def set_row_id(self, rowId): + """Set row id. + + :type rowId: string + :param rowId: row id + """ + self.rowId = rowId diff --git a/aliyun/log/deletelogsv2request.pyi b/aliyun/log/deletelogsv2request.pyi new file mode 100644 index 0000000..b569ca2 --- /dev/null +++ b/aliyun/log/deletelogsv2request.pyi @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from typing import Optional, Union + +from .logrequest import LogRequest + +class DeleteLogsV2Request(LogRequest): + def __init__(self, project: Optional[str] = ..., logstore: Optional[str] = ..., fromTime: Optional[Union[int, str]] = ..., toTime: Optional[Union[int, str]] = ..., query: Optional[str] = ..., rowId: Optional[str] = ...) -> None: ... + def get_logstore(self) -> str: ... + def set_logstore(self, logstore: str) -> None: ... + def get_from(self) -> int: ... + def set_from(self, fromTime: Union[int, str]) -> None: ... + def get_to(self) -> int: ... + def set_to(self, toTime: Union[int, str]) -> None: ... + def get_query(self) -> Optional[str]: ... + def set_query(self, query: Optional[str]) -> None: ... + def get_row_id(self) -> Optional[str]: ... + def set_row_id(self, rowId: Optional[str]) -> None: ... diff --git a/aliyun/log/deletelogsv2response.py b/aliyun/log/deletelogsv2response.py new file mode 100644 index 0000000..b591e88 --- /dev/null +++ b/aliyun/log/deletelogsv2response.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + +from .logresponse import LogResponse + + +class DeleteLogsV2Response(LogResponse): + """The response of the DeleteLogsV2 API from log.""" + + def __init__(self, resp, header): + LogResponse.__init__(self, header, resp) + self.affected_rows = resp.get('affected_rows', 0) + + def get_affected_rows(self): + return self.affected_rows + + def log_print(self): + print('DeleteLogsV2Response:') + print('headers:', self.get_all_headers()) + print('affected_rows:', self.affected_rows) diff --git a/aliyun/log/deletelogsv2response.pyi b/aliyun/log/deletelogsv2response.pyi new file mode 100644 index 0000000..687d594 --- /dev/null +++ b/aliyun/log/deletelogsv2response.pyi @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict + +from .logresponse import LogResponse + +class DeleteLogsV2Response(LogResponse): + def __init__(self, resp: Dict[str, Any], header: Dict[str, Any]) -> None: ... + def get_affected_rows(self) -> int: ... + def log_print(self) -> None: ... diff --git a/aliyun/log/logclient.py b/aliyun/log/logclient.py index 9081c99..d19780b 100644 --- a/aliyun/log/logclient.py +++ b/aliyun/log/logclient.py @@ -24,9 +24,11 @@ from .getlogsrequest import * from .cursor_response import GetCursorResponse from .cursor_time_response import GetCursorTimeResponse -from .gethistogramsresponse import GetHistogramsResponse -from .deletelogssresponse import DeleteLogsResponse -from .getlogsresponse import GetLogsResponse +from .gethistogramsresponse import GetHistogramsResponse +from .deletelogssresponse import DeleteLogsResponse +from .deletelogsv2response import DeleteLogsV2Response +from .updatelogsresponse import UpdateLogsResponse +from .getlogsresponse import GetLogsResponse from .getdeletelogsstatusresponse import GetDeleteLogsStatusResponse from .listdeletelogsstasksresponse import ListDeleteLogsTasksResponse from .getcontextlogsresponse import GetContextLogsResponse @@ -566,9 +568,9 @@ def get_histograms(self, request): (resp, header) = self._send("GET", project, None, resource, params, headers) return GetHistogramsResponse(resp, header) - def delete_logs(self, request): - """ delete logs of requested query from log service. - Unsuccessful operation will cause an LogException. + def delete_logs(self, request): + """ delete logs of requested query from log service. + Unsuccessful operation will cause an LogException. :type request: DeleteLogsRequest :param request: the DeleteLogsRequest request parameters class. @@ -595,12 +597,80 @@ def delete_logs(self, request): resource = "/logstores/" + logstore + "/deletelogtasks" body_str = six.b(json.dumps(params)) headers["x-log-bodyrawsize"] = str(len(body_str)) - (resp, header) = self._send("POST", project, body_str, resource, None, headers) - return DeleteLogsResponse(resp, header) - - def get_delete_logs_status(self, request): - """ Get get_delete_logs_status of requested logstore from log service. - Unsuccessful operation will cause an LogException. + (resp, header) = self._send("POST", project, body_str, resource, None, headers) + return DeleteLogsResponse(resp, header) + + def delete_logs_v2(self, request): + """Delete logs from a logstore. + + Unsuccessful operation will cause an LogException. + + :type request: DeleteLogsV2Request + :param request: the DeleteLogsV2Request request parameters class. + + :return: DeleteLogsV2Response + + :raise: LogException + """ + body = { + 'from': request.get_from(), + 'to': request.get_to() + } + if request.get_query() is not None: + body['query'] = request.get_query() + if request.get_row_id() is not None: + body['rowId'] = request.get_row_id() + + body_str = six.b(json.dumps(body)) + headers = { + 'Content-Type': 'application/json', + 'x-log-bodyrawsize': str(len(body_str)) + } + logstore = request.get_logstore() + project = request.get_project() + resource = "/logstores/" + logstore + "/deletelogs" + (resp, header) = self._send("POST", project, body_str, resource, None, headers) + return DeleteLogsV2Response(resp, header) + + def update_logs(self, request): + """Update logs in a logstore. + + Unsuccessful operation will cause an LogException. + + :type request: UpdateLogsRequest + :param request: the UpdateLogsRequest request parameters class. + + :return: UpdateLogsResponse + + :raise: LogException + """ + body = { + 'from': request.get_from(), + 'to': request.get_to() + } + if request.get_query() is not None: + body['query'] = request.get_query() + if request.get_row_id() is not None: + body['rowId'] = request.get_row_id() + if request.get_update_mode() is not None: + body['updateMode'] = request.get_update_mode() + if request.get_data() is not None: + body['data'] = request.get_data() + + body_str = six.b(json.dumps(body)) + headers = { + 'Content-Type': 'application/json', + 'x-log-bodyrawsize': str(len(body_str)) + } + logstore = request.get_logstore() + project = request.get_project() + resource = "/logstores/" + logstore + "/updatelogs" + (resp, header) = self._send("POST", project, body_str, resource, None, headers) + return UpdateLogsResponse(resp, header) + + def get_delete_logs_status(self, request): + """ Get get_delete_logs_status of requested logstore from log service. + Unsuccessful operation will cause an LogException. :type request: GetDeleteLogsStatusRequest :param request: the GetDeleteLogsStatusRequest request parameters class. diff --git a/aliyun/log/logclient.pyi b/aliyun/log/logclient.pyi index 5abea38..85f0c18 100644 --- a/aliyun/log/logclient.pyi +++ b/aliyun/log/logclient.pyi @@ -9,6 +9,8 @@ from .cursor_time_response import GetCursorTimeResponse from .delete_async_sql_request import DeleteAsyncSqlRequest from .deletelogsrequest import DeleteLogsRequest from .deletelogssresponse import DeleteLogsResponse +from .deletelogsv2request import DeleteLogsV2Request +from .deletelogsv2response import DeleteLogsV2Response from .etl_config_response import CreateEtlResponse, DeleteEtlResponse, GetEtlResponse, ListEtlsResponse, StartEtlResponse, StopEtlResponse, UpdateEtlResponse from .export_response import CreateExportResponse, DeleteExportResponse, GetExportResponse, ListExportResponse, UpdateExportResponse from .external_store_config import ExternalStoreConfigBase @@ -61,6 +63,8 @@ from .store_view import StoreView from .store_view_response import CreateStoreViewResponse, DeleteStoreViewResponse, GetStoreViewResponse, ListStoreViewsResponse, UpdateStoreViewResponse from .substore_config_response import CreateMetricsStoreResponse, CreateSubStoreResponse, DeleteSubStoreResponse, GetSubStoreResponse, GetSubStoreTTLResponse, ListSubStoreResponse, UpdateSubStoreResponse, UpdateSubStoreTTLResponse from .submit_async_sql_request import SubmitAsyncSqlRequest +from .updatelogsrequest import UpdateLogsRequest +from .updatelogsresponse import UpdateLogsResponse from .tag_response import GetResourceTagsResponse from .topostore_params import Topostore, TopostoreNode, TopostoreRelation from .topostore_response import CreateTopostoreNodeResponse, CreateTopostoreRelationResponse, CreateTopostoreResponse, DeleteTopostoreNodeResponse, DeleteTopostoreRelationResponse, DeleteTopostoreResponse, GetTopostoreNodeResponse, GetTopostoreRelationResponse, GetTopostoreResponse, ListTopostoreNodesResponse, ListTopostoreRelationsResponse, ListTopostoresResponse, UpdateTopostoreNodeResponse, UpdateTopostoreRelationResponse, UpdateTopostoreResponse, UpsertTopostoreNodeResponse, UpsertTopostoreRelationResponse @@ -82,6 +86,8 @@ class LogClient(object): def list_topics(self, request: ListTopicsRequest) -> ListTopicsResponse: ... def get_histograms(self, request: GetHistogramsRequest) -> GetHistogramsResponse: ... def delete_logs(self, request: DeleteLogsRequest) -> DeleteLogsResponse: ... + def delete_logs_v2(self, request: DeleteLogsV2Request) -> DeleteLogsV2Response: ... + def update_logs(self, request: UpdateLogsRequest) -> UpdateLogsResponse: ... def get_delete_logs_status(self, request: GetDeleteLogsStatusRequest) -> GetDeleteLogsStatusResponse: ... def list_delete_logs_tasks(self, request: ListDeleteLogsTasksRequest) -> ListDeleteLogsTasksResponse: ... def get_log(self, project: str, logstore: str, from_time: Union[int, str], to_time: Union[int, str], topic: Optional[str] = ..., query: Optional[str] = ..., reverse: bool = ..., offset: int = ..., size: int = ..., power_sql: bool = ..., scan: bool = ..., forward: bool = ..., accurate_query: bool = ..., from_time_nano_part: int = ..., to_time_nano_part: int = ...) -> GetLogsResponse: ... diff --git a/aliyun/log/updatelogsrequest.py b/aliyun/log/updatelogsrequest.py new file mode 100644 index 0000000..cc12692 --- /dev/null +++ b/aliyun/log/updatelogsrequest.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + +from .logrequest import LogRequest +from .util import parse_timestamp + + +class UpdateLogsRequest(LogRequest): + """The request used to update logs in a logstore.""" + + def __init__(self, project=None, logstore=None, fromTime=None, toTime=None, + query=None, rowId=None, updateMode=None, data=None): + LogRequest.__init__(self, project) + self.logstore = logstore + self.fromTime = parse_timestamp(fromTime) + self.toTime = parse_timestamp(toTime) + self.query = query + self.rowId = rowId + self.updateMode = updateMode + self.data = data + + def get_logstore(self): + """Get logstore name. + + :return: string, logstore name. + """ + return self.logstore if self.logstore else '' + + def set_logstore(self, logstore): + """Set logstore name. + + :type logstore: string + :param logstore: logstore name + """ + self.logstore = logstore + + def get_from(self): + """Get begin time. + + :return: int, begin time + """ + return self.fromTime + + def set_from(self, fromTime): + """Set begin time. + + :type fromTime: int/string + :param fromTime: begin time + """ + self.fromTime = parse_timestamp(fromTime) + + def get_to(self): + """Get end time. + + :return: int, end time + """ + return self.toTime + + def set_to(self, toTime): + """Set end time. + + :type toTime: int/string + :param toTime: end time + """ + self.toTime = parse_timestamp(toTime) + + def get_query(self): + """Get user defined query. + + :return: string, user defined query + """ + return self.query + + def set_query(self, query): + """Set user defined query. + + :type query: string + :param query: user defined query + """ + self.query = query + + def get_row_id(self): + """Get row id. + + :return: string, row id + """ + return self.rowId + + def set_row_id(self, rowId): + """Set row id. + + :type rowId: string + :param rowId: row id + """ + self.rowId = rowId + + def get_update_mode(self): + """Get update mode. + + :return: string, update mode + """ + return self.updateMode + + def set_update_mode(self, updateMode): + """Set update mode. + + :type updateMode: string + :param updateMode: update mode + """ + self.updateMode = updateMode + + def get_data(self): + """Get update data. + + :return: string, update data + """ + return self.data + + def set_data(self, data): + """Set update data. + + :type data: string + :param data: update data + """ + self.data = data diff --git a/aliyun/log/updatelogsrequest.pyi b/aliyun/log/updatelogsrequest.pyi new file mode 100644 index 0000000..9e91751 --- /dev/null +++ b/aliyun/log/updatelogsrequest.pyi @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from typing import Optional, Union + +from .logrequest import LogRequest + +class UpdateLogsRequest(LogRequest): + def __init__(self, project: Optional[str] = ..., logstore: Optional[str] = ..., fromTime: Optional[Union[int, str]] = ..., toTime: Optional[Union[int, str]] = ..., query: Optional[str] = ..., rowId: Optional[str] = ..., updateMode: Optional[str] = ..., data: Optional[str] = ...) -> None: ... + def get_logstore(self) -> str: ... + def set_logstore(self, logstore: str) -> None: ... + def get_from(self) -> int: ... + def set_from(self, fromTime: Union[int, str]) -> None: ... + def get_to(self) -> int: ... + def set_to(self, toTime: Union[int, str]) -> None: ... + def get_query(self) -> Optional[str]: ... + def set_query(self, query: Optional[str]) -> None: ... + def get_row_id(self) -> Optional[str]: ... + def set_row_id(self, rowId: Optional[str]) -> None: ... + def get_update_mode(self) -> Optional[str]: ... + def set_update_mode(self, updateMode: Optional[str]) -> None: ... + def get_data(self) -> Optional[str]: ... + def set_data(self, data: Optional[str]) -> None: ... diff --git a/aliyun/log/updatelogsresponse.py b/aliyun/log/updatelogsresponse.py new file mode 100644 index 0000000..f474919 --- /dev/null +++ b/aliyun/log/updatelogsresponse.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# Copyright (C) Alibaba Cloud Computing +# All rights reserved. + +from .logresponse import LogResponse + + +class UpdateLogsResponse(LogResponse): + """The response of the UpdateLogs API from log.""" + + def __init__(self, resp, header): + LogResponse.__init__(self, header, resp) + self.affected_rows = resp.get('affected_rows', 0) + + def get_affected_rows(self): + return self.affected_rows + + def log_print(self): + print('UpdateLogsResponse:') + print('headers:', self.get_all_headers()) + print('affected_rows:', self.affected_rows) diff --git a/aliyun/log/updatelogsresponse.pyi b/aliyun/log/updatelogsresponse.pyi new file mode 100644 index 0000000..56bbcd9 --- /dev/null +++ b/aliyun/log/updatelogsresponse.pyi @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict + +from .logresponse import LogResponse + +class UpdateLogsResponse(LogResponse): + def __init__(self, resp: Dict[str, Any], header: Dict[str, Any]) -> None: ... + def get_affected_rows(self) -> int: ... + def log_print(self) -> None: ... diff --git a/tests/unit/test_logclient_mock.py b/tests/unit/test_logclient_mock.py index 8ed17a0..04ca477 100644 --- a/tests/unit/test_logclient_mock.py +++ b/tests/unit/test_logclient_mock.py @@ -13,7 +13,15 @@ import pytest import responses -from aliyun.log import GetLogsRequest, LogClient, LogException, LogItem, PutLogsRequest +from aliyun.log import ( + DeleteLogsV2Request, + GetLogsRequest, + LogClient, + LogException, + LogItem, + PutLogsRequest, + UpdateLogsRequest, +) from tests._helpers.fakes import error_response, make_client, mock_sls_response @@ -126,3 +134,121 @@ def test_error_response_raises_logexception(): with pytest.raises(LogException) as excinfo: client.get_logs(req) assert excinfo.value.get_error_code() == "ParameterInvalid" + + +@responses.activate +def test_delete_logs_v2_posts_backend_body_and_parses_affected_rows(): + """DeleteLogsV2 posts to /deletelogs and parses affected_rows.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + captured = {} + + def request_callback(request): + captured["headers"] = dict(request.headers) + captured["body"] = json.loads(request.body.decode("utf-8")) + return ( + 200, + { + "x-log-requestid": "mock-request-id", + "Content-Type": "application/json", + }, + json.dumps({"affected_rows": 3}), + ) + + responses.add_callback( + responses.POST, + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores/store-1/deletelogs$"), + callback=request_callback, + ) + + req = DeleteLogsV2Request( + project="mock-proj", + logstore="store-1", + fromTime=1700000000, + toTime=1700000100, + query="level:error", + rowId="row-1", + ) + resp = client.delete_logs_v2(req) + + assert captured["headers"].get("Content-Type") == "application/json" + assert captured["body"] == { + "from": 1700000000, + "to": 1700000100, + "query": "level:error", + "rowId": "row-1", + } + assert resp.get_affected_rows() == 3 + + +@responses.activate +def test_update_logs_posts_backend_body_and_parses_affected_rows(): + """UpdateLogs posts to /updatelogs and leaves data as a string.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + captured = {} + + def request_callback(request): + captured["body"] = json.loads(request.body.decode("utf-8")) + return ( + 200, + { + "x-log-requestid": "mock-request-id", + "Content-Type": "application/json", + }, + json.dumps({"affected_rows": 2}), + ) + + responses.add_callback( + responses.POST, + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores/store-1/updatelogs$"), + callback=request_callback, + ) + + data = '{"level":"warning"}' + req = UpdateLogsRequest( + project="mock-proj", + logstore="store-1", + fromTime=1700000000, + toTime=1700000100, + query="level:error", + rowId="row-1", + updateMode="replace", + data=data, + ) + resp = client.update_logs(req) + + assert captured["body"] == { + "from": 1700000000, + "to": 1700000100, + "query": "level:error", + "rowId": "row-1", + "updateMode": "replace", + "data": data, + } + assert resp.get_affected_rows() == 2 + + +@responses.activate +def test_update_logs_error_response_raises_logexception(): + """A server error envelope from UpdateLogs is converted to LogException.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + mock_sls_response( + responses, + "POST", + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores/store-1/updatelogs$"), + status=400, + body=error_response("ParameterInvalid", "bad update request"), + ) + + req = UpdateLogsRequest( + project="mock-proj", + logstore="store-1", + fromTime=1700000000, + toTime=1700000100, + query="*", + ) + with pytest.raises(LogException) as excinfo: + client.update_logs(req) + assert excinfo.value.get_error_code() == "ParameterInvalid" From 338b15f92e6682a59460426666514368985b7bab Mon Sep 17 00:00:00 2001 From: "shining.wz" Date: Tue, 2 Jun 2026 16:39:08 +0800 Subject: [PATCH 2/2] Align log modification APIs with docs --- aliyun/log/deletelogsv2request.py | 8 +- aliyun/log/deletelogsv2request.pyi | 8 +- aliyun/log/deletelogsv2response.pyi | 1 + aliyun/log/logclient.py | 149 +++++++++++++++++++++------ aliyun/log/logclient.pyi | 7 +- aliyun/log/updatelogsrequest.py | 8 +- aliyun/log/updatelogsrequest.pyi | 8 +- aliyun/log/updatelogsresponse.pyi | 1 + tests/unit/test_logclient_mock.py | 152 ++++++++++++++++++++++++++++ 9 files changed, 292 insertions(+), 50 deletions(-) diff --git a/aliyun/log/deletelogsv2request.py b/aliyun/log/deletelogsv2request.py index 86101f3..b6cdf68 100644 --- a/aliyun/log/deletelogsv2request.py +++ b/aliyun/log/deletelogsv2request.py @@ -33,8 +33,8 @@ class DeleteLogsV2Request(LogRequest): def __init__(self, project=None, logstore=None, fromTime=None, toTime=None, query=None, rowId=None): LogRequest.__init__(self, project) self.logstore = logstore - self.fromTime = parse_timestamp(fromTime) - self.toTime = parse_timestamp(toTime) + self.fromTime = parse_timestamp(fromTime) if fromTime is not None else fromTime + self.toTime = parse_timestamp(toTime) if toTime is not None else toTime self.query = query self.rowId = rowId @@ -66,7 +66,7 @@ def set_from(self, fromTime): :type fromTime: int/string :param fromTime: begin time """ - self.fromTime = parse_timestamp(fromTime) + self.fromTime = parse_timestamp(fromTime) if fromTime is not None else fromTime def get_to(self): """Get end time. @@ -81,7 +81,7 @@ def set_to(self, toTime): :type toTime: int/string :param toTime: end time """ - self.toTime = parse_timestamp(toTime) + self.toTime = parse_timestamp(toTime) if toTime is not None else toTime def get_query(self): """Get user defined query. diff --git a/aliyun/log/deletelogsv2request.pyi b/aliyun/log/deletelogsv2request.pyi index b569ca2..00efe42 100644 --- a/aliyun/log/deletelogsv2request.pyi +++ b/aliyun/log/deletelogsv2request.pyi @@ -7,10 +7,10 @@ class DeleteLogsV2Request(LogRequest): def __init__(self, project: Optional[str] = ..., logstore: Optional[str] = ..., fromTime: Optional[Union[int, str]] = ..., toTime: Optional[Union[int, str]] = ..., query: Optional[str] = ..., rowId: Optional[str] = ...) -> None: ... def get_logstore(self) -> str: ... def set_logstore(self, logstore: str) -> None: ... - def get_from(self) -> int: ... - def set_from(self, fromTime: Union[int, str]) -> None: ... - def get_to(self) -> int: ... - def set_to(self, toTime: Union[int, str]) -> None: ... + def get_from(self) -> Optional[int]: ... + def set_from(self, fromTime: Optional[Union[int, str]]) -> None: ... + def get_to(self) -> Optional[int]: ... + def set_to(self, toTime: Optional[Union[int, str]]) -> None: ... def get_query(self) -> Optional[str]: ... def set_query(self, query: Optional[str]) -> None: ... def get_row_id(self) -> Optional[str]: ... diff --git a/aliyun/log/deletelogsv2response.pyi b/aliyun/log/deletelogsv2response.pyi index 687d594..21cbccf 100644 --- a/aliyun/log/deletelogsv2response.pyi +++ b/aliyun/log/deletelogsv2response.pyi @@ -4,6 +4,7 @@ from typing import Any, Dict from .logresponse import LogResponse class DeleteLogsV2Response(LogResponse): + affected_rows: int def __init__(self, resp: Dict[str, Any], header: Dict[str, Any]) -> None: ... def get_affected_rows(self) -> int: ... def log_print(self) -> None: ... diff --git a/aliyun/log/logclient.py b/aliyun/log/logclient.py index d19780b..2e8594b 100644 --- a/aliyun/log/logclient.py +++ b/aliyun/log/logclient.py @@ -21,12 +21,14 @@ from itertools import cycle from .consumer_group_request import * from .consumer_group_response import * -from .getlogsrequest import * -from .cursor_response import GetCursorResponse -from .cursor_time_response import GetCursorTimeResponse +from .getlogsrequest import * +from .cursor_response import GetCursorResponse +from .cursor_time_response import GetCursorTimeResponse from .gethistogramsresponse import GetHistogramsResponse from .deletelogssresponse import DeleteLogsResponse +from .deletelogsv2request import DeleteLogsV2Request from .deletelogsv2response import DeleteLogsV2Response +from .updatelogsrequest import UpdateLogsRequest from .updatelogsresponse import UpdateLogsResponse from .getlogsresponse import GetLogsResponse from .getdeletelogsstatusresponse import GetDeleteLogsStatusResponse @@ -600,7 +602,22 @@ def delete_logs(self, request): (resp, header) = self._send("POST", project, body_str, resource, None, headers) return DeleteLogsResponse(resp, header) - def delete_logs_v2(self, request): + @staticmethod + def _log_item_to_update_data(log_item): + if log_item is None: + return None + if hasattr(log_item, 'get_contents'): + return json.dumps(dict(log_item.get_contents())) + if isinstance(log_item, dict): + return json.dumps(log_item) + return log_item + + @staticmethod + def _choose_row_id(rowid, row_id): + return rowid if rowid is not None else row_id + + def delete_logs_v2(self, request=None, project=None, logstore=None, from_time=None, to_time=None, + query=None, rowid=None, row_id=None): """Delete logs from a logstore. Unsuccessful operation will cause an LogException. @@ -608,14 +625,40 @@ def delete_logs_v2(self, request): :type request: DeleteLogsV2Request :param request: the DeleteLogsV2Request request parameters class. + :type project: string + :param project: project name + + :type logstore: string + :param logstore: logstore name + + :type from_time: int/string + :param from_time: the begin time + + :type to_time: int/string + :param to_time: the end time + + :type query: string + :param query: user defined query + + :type rowid: string + :param rowid: row id of the log + + :type row_id: string + :param row_id: row id of the log + :return: DeleteLogsV2Response :raise: LogException """ - body = { - 'from': request.get_from(), - 'to': request.get_to() - } + if request is None: + request = DeleteLogsV2Request(project, logstore, from_time, to_time, query, + self._choose_row_id(rowid, row_id)) + + body = {} + if request.get_from() is not None: + body['from'] = request.get_from() + if request.get_to() is not None: + body['to'] = request.get_to() if request.get_query() is not None: body['query'] = request.get_query() if request.get_row_id() is not None: @@ -632,7 +675,8 @@ def delete_logs_v2(self, request): (resp, header) = self._send("POST", project, body_str, resource, None, headers) return DeleteLogsV2Response(resp, header) - def update_logs(self, request): + def update_logs(self, request=None, project=None, logstore=None, from_time=None, to_time=None, + query=None, rowid=None, row_id=None, log_item=None, update_mode=None, data=None): """Update logs in a logstore. Unsuccessful operation will cause an LogException. @@ -640,14 +684,51 @@ def update_logs(self, request): :type request: UpdateLogsRequest :param request: the UpdateLogsRequest request parameters class. + :type project: string + :param project: project name + + :type logstore: string + :param logstore: logstore name + + :type from_time: int/string + :param from_time: the begin time + + :type to_time: int/string + :param to_time: the end time + + :type query: string + :param query: user defined query + + :type rowid: string + :param rowid: row id of the log + + :type row_id: string + :param row_id: row id of the log + + :type log_item: LogItem/dict + :param log_item: fields to update + + :type update_mode: string + :param update_mode: update mode + + :type data: string + :param data: update data + :return: UpdateLogsResponse :raise: LogException """ - body = { - 'from': request.get_from(), - 'to': request.get_to() - } + if request is None: + if data is None: + data = self._log_item_to_update_data(log_item) + request = UpdateLogsRequest(project, logstore, from_time, to_time, query, + self._choose_row_id(rowid, row_id), update_mode, data) + + body = {} + if request.get_from() is not None: + body['from'] = request.get_from() + if request.get_to() is not None: + body['to'] = request.get_to() if request.get_query() is not None: body['query'] = request.get_query() if request.get_row_id() is not None: @@ -1540,9 +1621,9 @@ def pull_log_dump(self, project_name, logstore_name, from_time, to_time, file_pa batch_size=batch_size, compress=compress, encodings=encodings, shard_list=shard_list, no_escape=no_escape, query=query, processor=processor) - def create_logstore(self, project_name, logstore_name, - ttl=30, - shard_count=2, + def create_logstore(self, project_name, logstore_name, + ttl=30, + shard_count=2, enable_tracking=False, append_meta=False, auto_split=True, @@ -1550,10 +1631,11 @@ def create_logstore(self, project_name, logstore_name, preserve_storage=False, encrypt_conf=None, telemetry_type='', - hot_ttl=-1, - mode = None, - infrequent_access_ttl=-1 - ): + hot_ttl=-1, + mode = None, + infrequent_access_ttl=-1, + enable_modify=False + ): """ create log store Unsuccessful operation will cause an LogException. @@ -1605,12 +1687,15 @@ def create_logstore(self, project_name, logstore_name, :type infrequent_access_ttl: int :param infrequent_access_ttl: infrequent access storage time - :type hot_ttl: int - :param hot_ttl: the life cycle of hot storage,[0-hot_ttl]is hot storage, (hot_ttl-ttl] is warm storage, if hot_ttl=-1, it means [0-ttl]is all hot storage - - :return: CreateLogStoreResponse - - :raise: LogException + :type hot_ttl: int + :param hot_ttl: the life cycle of hot storage,[0-hot_ttl]is hot storage, (hot_ttl-ttl] is warm storage, if hot_ttl=-1, it means [0-ttl]is all hot storage + + :type enable_modify: bool + :param enable_modify: enable log modification and deletion, default is False + + :return: CreateLogStoreResponse + + :raise: LogException """ if preserve_storage: ttl = 3650 @@ -1625,11 +1710,13 @@ def create_logstore(self, project_name, logstore_name, "enable_tracking": enable_tracking, "autoSplit": auto_split, "maxSplitShard": max_split_shard, - "appendMeta": append_meta, - "telemetryType": telemetry_type - } - if hot_ttl !=-1: - body['hot_ttl'] = hot_ttl + "appendMeta": append_meta, + "telemetryType": telemetry_type + } + if enable_modify: + body["enableModify"] = enable_modify + if hot_ttl !=-1: + body['hot_ttl'] = hot_ttl if encrypt_conf != None: body["encrypt_conf"] = encrypt_conf if mode != None: diff --git a/aliyun/log/logclient.pyi b/aliyun/log/logclient.pyi index 85f0c18..fd3bdb3 100644 --- a/aliyun/log/logclient.pyi +++ b/aliyun/log/logclient.pyi @@ -33,6 +33,7 @@ from .listlogstoresrequest import ListLogstoresRequest from .listlogstoresresponse import ListLogstoresResponse from .listtopicsrequest import ListTopicsRequest from .listtopicsresponse import ListTopicsResponse +from .logitem import LogItem from .logclient_operator import ResourceUsageResponse, copy_project, list_more, query_more, pull_log_dump, copy_logstore, copy_data, get_resource_usage, arrange_shard, transform_data, copy_dashboard, copy_alert from .logexception import LogException from .logresponse import LogResponse @@ -86,8 +87,8 @@ class LogClient(object): def list_topics(self, request: ListTopicsRequest) -> ListTopicsResponse: ... def get_histograms(self, request: GetHistogramsRequest) -> GetHistogramsResponse: ... def delete_logs(self, request: DeleteLogsRequest) -> DeleteLogsResponse: ... - def delete_logs_v2(self, request: DeleteLogsV2Request) -> DeleteLogsV2Response: ... - def update_logs(self, request: UpdateLogsRequest) -> UpdateLogsResponse: ... + def delete_logs_v2(self, request: Optional[DeleteLogsV2Request] = ..., project: Optional[str] = ..., logstore: Optional[str] = ..., from_time: Optional[Union[int, str]] = ..., to_time: Optional[Union[int, str]] = ..., query: Optional[str] = ..., rowid: Optional[str] = ..., row_id: Optional[str] = ...) -> DeleteLogsV2Response: ... + def update_logs(self, request: Optional[UpdateLogsRequest] = ..., project: Optional[str] = ..., logstore: Optional[str] = ..., from_time: Optional[Union[int, str]] = ..., to_time: Optional[Union[int, str]] = ..., query: Optional[str] = ..., rowid: Optional[str] = ..., row_id: Optional[str] = ..., log_item: Optional[Union[LogItem, Dict[str, Any], str]] = ..., update_mode: Optional[str] = ..., data: Optional[str] = ...) -> UpdateLogsResponse: ... def get_delete_logs_status(self, request: GetDeleteLogsStatusRequest) -> GetDeleteLogsStatusResponse: ... def list_delete_logs_tasks(self, request: ListDeleteLogsTasksRequest) -> ListDeleteLogsTasksResponse: ... def get_log(self, project: str, logstore: str, from_time: Union[int, str], to_time: Union[int, str], topic: Optional[str] = ..., query: Optional[str] = ..., reverse: bool = ..., offset: int = ..., size: int = ..., power_sql: bool = ..., scan: bool = ..., forward: bool = ..., accurate_query: bool = ..., from_time_nano_part: int = ..., to_time_nano_part: int = ...) -> GetLogsResponse: ... @@ -109,7 +110,7 @@ class LogClient(object): def pull_logs(self, project_name: str, logstore_name: str, shard_id: int, cursor: str, count: Optional[int] = ..., end_cursor: Optional[str] = ..., compress: Optional[bool] = ..., query: Optional[str] = ..., accept_compress_type: Optional[str] = ..., processor: Optional[str] = ...) -> PullLogResponse: ... def pull_log(self, project_name: str, logstore_name: str, shard_id: int, from_time: Union[int, str], to_time: Union[int, str], batch_size: Optional[int] = ..., compress: Optional[bool] = ..., query: Optional[str] = ..., accept_compress_type: Optional[str] = ..., processor: Optional[str] = ...) -> Iterator[PullLogResponse]: ... def pull_log_dump(self, project_name: str, logstore_name: str, from_time: Union[int, str], to_time: Union[int, str], file_path: str, batch_size: Optional[int] = ..., compress: Optional[bool] = ..., encodings: Optional[List[str]] = ..., shard_list: Optional[Union[str, List[str]]] = ..., no_escape: Optional[bool] = ..., query: Optional[str] = ..., processor: Optional[str] = ...) -> LogResponse: ... - def create_logstore(self, project_name: str, logstore_name: str, ttl: int = ..., shard_count: int = ..., enable_tracking: bool = ..., append_meta: bool = ..., auto_split: bool = ..., max_split_shard: int = ..., preserve_storage: bool = ..., encrypt_conf: Optional[Dict[str, Any]] = ..., telemetry_type: str = ..., hot_ttl: int = ..., mode: Optional[str] = ..., infrequent_access_ttl: int = ...) -> CreateLogStoreResponse: ... + def create_logstore(self, project_name: str, logstore_name: str, ttl: int = ..., shard_count: int = ..., enable_tracking: bool = ..., append_meta: bool = ..., auto_split: bool = ..., max_split_shard: int = ..., preserve_storage: bool = ..., encrypt_conf: Optional[Dict[str, Any]] = ..., telemetry_type: str = ..., hot_ttl: int = ..., mode: Optional[str] = ..., infrequent_access_ttl: int = ..., enable_modify: bool = ...) -> CreateLogStoreResponse: ... def delete_logstore(self, project_name: str, logstore_name: str) -> DeleteLogStoreResponse: ... def get_logstore(self, project_name: str, logstore_name: str) -> GetLogStoreResponse: ... def update_logstore(self, project_name: str, logstore_name: str, ttl: Optional[int] = ..., enable_tracking: Optional[bool] = ..., shard_count: Optional[int] = ..., append_meta: Optional[bool] = ..., auto_split: Optional[bool] = ..., max_split_shard: Optional[int] = ..., preserve_storage: Optional[bool] = ..., encrypt_conf: Optional[Dict[str, Any]] = ..., hot_ttl: int = ..., mode: Optional[str] = ..., telemetry_type: Optional[str] = ..., infrequent_access_ttl: int = ...) -> UpdateLogStoreResponse: ... diff --git a/aliyun/log/updatelogsrequest.py b/aliyun/log/updatelogsrequest.py index cc12692..821f9c5 100644 --- a/aliyun/log/updatelogsrequest.py +++ b/aliyun/log/updatelogsrequest.py @@ -15,8 +15,8 @@ def __init__(self, project=None, logstore=None, fromTime=None, toTime=None, query=None, rowId=None, updateMode=None, data=None): LogRequest.__init__(self, project) self.logstore = logstore - self.fromTime = parse_timestamp(fromTime) - self.toTime = parse_timestamp(toTime) + self.fromTime = parse_timestamp(fromTime) if fromTime is not None else fromTime + self.toTime = parse_timestamp(toTime) if toTime is not None else toTime self.query = query self.rowId = rowId self.updateMode = updateMode @@ -50,7 +50,7 @@ def set_from(self, fromTime): :type fromTime: int/string :param fromTime: begin time """ - self.fromTime = parse_timestamp(fromTime) + self.fromTime = parse_timestamp(fromTime) if fromTime is not None else fromTime def get_to(self): """Get end time. @@ -65,7 +65,7 @@ def set_to(self, toTime): :type toTime: int/string :param toTime: end time """ - self.toTime = parse_timestamp(toTime) + self.toTime = parse_timestamp(toTime) if toTime is not None else toTime def get_query(self): """Get user defined query. diff --git a/aliyun/log/updatelogsrequest.pyi b/aliyun/log/updatelogsrequest.pyi index 9e91751..22a7323 100644 --- a/aliyun/log/updatelogsrequest.pyi +++ b/aliyun/log/updatelogsrequest.pyi @@ -7,10 +7,10 @@ class UpdateLogsRequest(LogRequest): def __init__(self, project: Optional[str] = ..., logstore: Optional[str] = ..., fromTime: Optional[Union[int, str]] = ..., toTime: Optional[Union[int, str]] = ..., query: Optional[str] = ..., rowId: Optional[str] = ..., updateMode: Optional[str] = ..., data: Optional[str] = ...) -> None: ... def get_logstore(self) -> str: ... def set_logstore(self, logstore: str) -> None: ... - def get_from(self) -> int: ... - def set_from(self, fromTime: Union[int, str]) -> None: ... - def get_to(self) -> int: ... - def set_to(self, toTime: Union[int, str]) -> None: ... + def get_from(self) -> Optional[int]: ... + def set_from(self, fromTime: Optional[Union[int, str]]) -> None: ... + def get_to(self) -> Optional[int]: ... + def set_to(self, toTime: Optional[Union[int, str]]) -> None: ... def get_query(self) -> Optional[str]: ... def set_query(self, query: Optional[str]) -> None: ... def get_row_id(self) -> Optional[str]: ... diff --git a/aliyun/log/updatelogsresponse.pyi b/aliyun/log/updatelogsresponse.pyi index 56bbcd9..29c2b7b 100644 --- a/aliyun/log/updatelogsresponse.pyi +++ b/aliyun/log/updatelogsresponse.pyi @@ -4,6 +4,7 @@ from typing import Any, Dict from .logresponse import LogResponse class UpdateLogsResponse(LogResponse): + affected_rows: int def __init__(self, resp: Dict[str, Any], header: Dict[str, Any]) -> None: ... def get_affected_rows(self) -> int: ... def log_print(self) -> None: ... diff --git a/tests/unit/test_logclient_mock.py b/tests/unit/test_logclient_mock.py index 04ca477..b51fd8d 100644 --- a/tests/unit/test_logclient_mock.py +++ b/tests/unit/test_logclient_mock.py @@ -179,6 +179,41 @@ def request_callback(request): "rowId": "row-1", } assert resp.get_affected_rows() == 3 + assert resp.affected_rows == 3 + + +@responses.activate +def test_delete_logs_v2_accepts_documented_keyword_arguments(): + """DeleteLogsV2 supports the documented project/logstore/rowid call shape.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + captured = {} + + def request_callback(request): + captured["body"] = json.loads(request.body.decode("utf-8")) + return ( + 200, + { + "x-log-requestid": "mock-request-id", + "Content-Type": "application/json", + }, + json.dumps({"affected_rows": 1}), + ) + + responses.add_callback( + responses.POST, + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores/store-1/deletelogs$"), + callback=request_callback, + ) + + resp = client.delete_logs_v2( + project="mock-proj", + logstore="store-1", + rowid="row-1", + ) + + assert captured["body"] == {"rowId": "row-1"} + assert resp.affected_rows == 1 @responses.activate @@ -229,6 +264,88 @@ def request_callback(request): assert resp.get_affected_rows() == 2 +@responses.activate +def test_update_logs_accepts_documented_log_item_keyword_arguments(): + """UpdateLogs supports the documented LogItem keyword call shape.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + captured = {} + + def request_callback(request): + captured["body"] = json.loads(request.body.decode("utf-8")) + return ( + 200, + { + "x-log-requestid": "mock-request-id", + "Content-Type": "application/json", + }, + json.dumps({"affected_rows": 1}), + ) + + responses.add_callback( + responses.POST, + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores/store-1/updatelogs$"), + callback=request_callback, + ) + + resp = client.update_logs( + project="mock-proj", + logstore="store-1", + rowid="row-1", + log_item=LogItem(contents=[("status", "REFUNDED")]), + ) + + assert captured["body"] == { + "rowId": "row-1", + "data": '{"status": "REFUNDED"}', + } + assert resp.affected_rows == 1 + + +@responses.activate +def test_update_logs_accepts_documented_dict_keyword_arguments(): + """UpdateLogs supports dict log_item and row_id aliases from the docs.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + captured = {} + + def request_callback(request): + captured["body"] = json.loads(request.body.decode("utf-8")) + return ( + 200, + { + "x-log-requestid": "mock-request-id", + "Content-Type": "application/json", + }, + json.dumps({"affected_rows": 2}), + ) + + responses.add_callback( + responses.POST, + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores/store-1/updatelogs$"), + callback=request_callback, + ) + + resp = client.update_logs( + project="mock-proj", + logstore="store-1", + from_time=1700000000, + to_time=1700000100, + query='order_id: 12345 and status: "PENDING"', + row_id="", + log_item={"status": "REFUNDED", "refund_at": "2026-05-25T10:00:00Z"}, + ) + + assert captured["body"] == { + "from": 1700000000, + "to": 1700000100, + "query": 'order_id: 12345 and status: "PENDING"', + "rowId": "", + "data": '{"status": "REFUNDED", "refund_at": "2026-05-25T10:00:00Z"}', + } + assert resp.affected_rows == 2 + + @responses.activate def test_update_logs_error_response_raises_logexception(): """A server error envelope from UpdateLogs is converted to LogException.""" @@ -252,3 +369,38 @@ def test_update_logs_error_response_raises_logexception(): with pytest.raises(LogException) as excinfo: client.update_logs(req) assert excinfo.value.get_error_code() == "ParameterInvalid" + + +@responses.activate +def test_create_logstore_supports_enable_modify(): + """CreateLogstore includes enableModify when requested by docs.""" + client = make_client(endpoint="cn-mock.example.com", project="mock-proj") + + captured = {} + + def request_callback(request): + captured["body"] = json.loads(request.body.decode("utf-8")) + return ( + 200, + { + "x-log-requestid": "mock-request-id", + "Content-Type": "application/json", + }, + "{}", + ) + + responses.add_callback( + responses.POST, + re.compile(r"https?://mock-proj\.cn-mock\.example\.com.*?/logstores$"), + callback=request_callback, + ) + + client.create_logstore( + project_name="mock-proj", + logstore_name="store-1", + ttl=30, + shard_count=1, + enable_modify=True, + ) + + assert captured["body"]["enableModify"] is True