diff --git a/api.md b/api.md index dff57591c..ae12f3f15 100644 --- a/api.md +++ b/api.md @@ -714,6 +714,64 @@ Update service configuration `service_config_id` with the provided template. --- +### `PATCH /api/v2/service/{service_config_id}` + +Partial update service configuration `service_config_id` with the provided (partial) template. + +![Partial updates](docs/images/partial_update_examples.png) + +
+ Request + +```json + { + "configurations": {...}, + "description": "Trader agent for omen prediction markets", + "env_variables": {...}, + "hash": "bafybeibpseosblmaw6sk6zsnic2kfxfsijrnfluuhkwboyqhx7ma7zw2me", + "image": "https://operate.olas.network/_next/image?url=%2Fimages%2Fprediction-agent.png&w=3840&q=75", + "home_chain": "gnosis", + "name": "valory/trader_omen_gnosis", + "service_version": "v0.19.0" + } +``` + +
+ +
+ Response + +- If the update is successful, the response contains the updated service configuration: + + ```json + { + "chain_configs": {...}, + "description": "Trader agent for omen prediction markets", + "env_variables": {...}, + "hash": "bafybeidicxsruh3r4a2xarawzan6ocwyvpn3ofv42po5kxf7x6ck7kn22u", + "hash_history": {"1731487112": "bafybeidicxsruh3r4a2xarawzan6ocwyvpn3ofv42po5kxf7x6ck7kn22u", "1731490000": "bafybeibpseosblmaw6sk6zsnic2kfxfsijrnfluuhkwboyqhx7ma7zw2me"}, + "home_chain": "gnosis", + "keys": [...], + "name": "valory/trader_omen_gnosis", + "service_config_id": "sc-85a7a12a-8c6b-46b8-919a-b8a3b8e3ad39", + "service_path": "/home/user/.operate/services/sc-85a7a12a-8c6b-46b8-919a-b8a3b8e3ad39/trader_omen_gnosis" + } + + ``` + +- If the update is not successful: + + ```json + { + "error": "Error message", + "traceback": "Traceback message" + } + ``` + +
+ +--- + ### `POST /api/service/{service_config_id}/stop` Stop service with service configuration `service_configuration_id`. diff --git a/docs/images/partial_update_examples.png b/docs/images/partial_update_examples.png new file mode 100644 index 000000000..732ef3613 Binary files /dev/null and b/docs/images/partial_update_examples.png differ diff --git a/frontend/client/types.ts b/frontend/client/types.ts index 7c8113167..ccfd2c36d 100644 --- a/frontend/client/types.ts +++ b/frontend/client/types.ts @@ -39,8 +39,8 @@ export type ChainData = { nft: string; staking_program_id: StakingProgramId; threshold: number; - use_mech_marketplace: true; - use_staking: true; + use_mech_marketplace: boolean; + use_staking: boolean; }; }; diff --git a/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx b/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx index 66d47ea22..3d347bc33 100644 --- a/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx +++ b/frontend/components/MainPage/header/AgentButton/AgentNotRunningButton.tsx @@ -249,14 +249,9 @@ const useServiceDeployment = () => { if (service.hash !== serviceTemplate.hash) { await ServicesService.updateService({ serviceConfigId: service.service_config_id, - stakingProgramId: selectedStakingProgramId, - // chainId: selectedAgentConfig.evmHomeChainId, - serviceTemplate, - deploy: false, // TODO: deprecated will remove - useMechMarketplace: - STAKING_PROGRAMS[selectedAgentConfig.evmHomeChainId][ - selectedStakingProgramId - ].mechType === MechType.Marketplace, + partialServiceTemplate: { + hash: serviceTemplate.hash, + }, }); } } diff --git a/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx b/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx index 21de15047..52c051158 100644 --- a/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx +++ b/frontend/components/ManageStakingPage/StakingContractSection/MigrateButton.tsx @@ -20,6 +20,7 @@ import { } from '@/hooks/useStakingContractDetails'; import { useStakingProgram } from '@/hooks/useStakingProgram'; import { ServicesService } from '@/service/Services'; +import { DeepPartial } from '@/types/Util'; import { CountdownUntilMigration } from './CountdownUntilMigration'; import { CantMigrateReason, useMigrate } from './useMigrate'; @@ -45,11 +46,10 @@ export const MigrateButton = ({ const { service } = useService(serviceConfigId); const serviceTemplate = useMemo( () => - service - ? getServiceTemplate(service.hash) - : SERVICE_TEMPLATES.find( - (template) => template.agentType === selectedAgentType, - ), + (service && getServiceTemplate(service.hash)) ?? + SERVICE_TEMPLATES.find( + (template) => template.agentType === selectedAgentType, + ), [selectedAgentType, service], ); @@ -128,23 +128,39 @@ export const MigrateButton = ({ overrideSelectedServiceStatus(MiddlewareDeploymentStatus.DEPLOYING); goto(Pages.Main); - const serviceConfigParams = { - stakingProgramId: stakingProgramIdToMigrateTo, - serviceTemplate, - deploy: true, - useMechMarketplace: - stakingProgramIdToMigrateTo === - StakingProgramId.PearlBetaMechMarketplace, - }; - if (selectedService) { // update service await ServicesService.updateService({ - ...serviceConfigParams, serviceConfigId, + partialServiceTemplate: { + configurations: { + ...Object.entries(serviceTemplate.configurations).reduce( + (acc, [middlewareChain]) => { + acc[middlewareChain] = { + staking_program_id: stakingProgramIdToMigrateTo, + use_mech_marketplace: + stakingProgramIdToMigrateTo === + StakingProgramId.PearlBetaMechMarketplace, + }; + return acc; + }, + {} as DeepPartial, + ), + }, + }, }); } else { // create service if it doesn't exist + + const serviceConfigParams = { + stakingProgramId: stakingProgramIdToMigrateTo, + serviceTemplate, + deploy: true, + useMechMarketplace: + stakingProgramIdToMigrateTo === + StakingProgramId.PearlBetaMechMarketplace, + }; + await ServicesService.createService(serviceConfigParams); } diff --git a/frontend/service/Services.ts b/frontend/service/Services.ts index 32bc21965..86b13ed38 100644 --- a/frontend/service/Services.ts +++ b/frontend/service/Services.ts @@ -9,6 +9,7 @@ import { CONTENT_TYPE_JSON_UTF8 } from '@/constants/headers'; import { BACKEND_URL_V2 } from '@/constants/urls'; import { StakingProgramId } from '@/enums/StakingProgram'; import { Address } from '@/types/Address'; +import { DeepPartial } from '@/types/Util'; import { asEvmChainId } from '@/utils/middlewareHelpers'; /** @@ -92,44 +93,19 @@ const createService = async ({ /** * Updates a service - * @param serviceTemplate + * @param partialServiceTemplate * @returns Promise */ const updateService = async ({ - deploy, - serviceTemplate, + partialServiceTemplate, serviceConfigId, - stakingProgramId, - useMechMarketplace = false, }: { - deploy: boolean; - serviceTemplate: ServiceTemplate; + partialServiceTemplate: DeepPartial; serviceConfigId: string; - stakingProgramId: StakingProgramId; - useMechMarketplace?: boolean; }): Promise => fetch(`${BACKEND_URL_V2}/service/${serviceConfigId}`, { - method: 'PUT', - body: JSON.stringify({ - ...serviceTemplate, - deploy, - configurations: { - ...serviceTemplate.configurations, - // overwrite defaults with chain-specific configurations - ...Object.entries(serviceTemplate.configurations).reduce( - (acc, [middlewareChain, config]) => { - acc[middlewareChain] = { - ...config, - rpc: CHAIN_CONFIG[asEvmChainId(middlewareChain)].rpc, - staking_program_id: stakingProgramId, - use_mech_marketplace: useMechMarketplace, - }; - return acc; - }, - {} as typeof serviceTemplate.configurations, - ), - }, - }), + method: 'PATCH', + body: JSON.stringify({ ...partialServiceTemplate }), headers: { ...CONTENT_TYPE_JSON_UTF8 }, }).then((response) => { if (response.ok) { diff --git a/frontend/types/Util.ts b/frontend/types/Util.ts index e4bed10e6..b39a88304 100644 --- a/frontend/types/Util.ts +++ b/frontend/types/Util.ts @@ -4,6 +4,10 @@ export type Optional = T | undefined; export type Maybe = Nullable>; +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; + /** * function to strip off the null or undefined types from a type by making an assertion. * @note This function should be used if you are confident that the value will never ever be null or undefined. diff --git a/operate/cli.py b/operate/cli.py index 3cf562455..6f156f5ff 100644 --- a/operate/cli.py +++ b/operate/cli.py @@ -268,7 +268,7 @@ def pause_all_services_on_exit(signum: int, frame: t.Optional[FrameType]) -> Non app.add_middleware( CORSMiddleware, allow_origins=["*"], - allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], ) def with_retries(f: t.Callable) -> t.Callable: @@ -756,6 +756,7 @@ def _fn() -> None: ) @app.put("/api/v2/service/{service_config_id}") + @app.patch("/api/v2/service/{service_config_id}") @with_retries async def _update_service(request: Request) -> JSONResponse: """Update a service.""" @@ -765,7 +766,6 @@ async def _update_service(request: Request) -> JSONResponse: service_config_id = request.path_params["service_config_id"] manager = operate.service_manager() - print(service_config_id) if not manager.exists(service_config_id=service_config_id): return service_not_found_error(service_config_id=service_config_id) @@ -773,10 +773,21 @@ async def _update_service(request: Request) -> JSONResponse: allow_different_service_public_id = template.get( "allow_different_service_public_id", False ) + + if request.method == "PUT": + partial_update = False + else: + partial_update = True + + logger.info( + f"_update_service {partial_update=} {allow_different_service_public_id=}" + ) + output = manager.update( service_config_id=service_config_id, service_template=template, allow_different_service_public_id=allow_different_service_public_id, + partial_update=partial_update, ) return JSONResponse(content=output.json) diff --git a/operate/operate_types.py b/operate/operate_types.py index 9a6838377..b6b502c1b 100644 --- a/operate/operate_types.py +++ b/operate/operate_types.py @@ -246,7 +246,7 @@ class EnvVariableAttributes(TypedDict): EnvVariables = t.Dict[str, EnvVariableAttributes] -class ServiceTemplate(TypedDict): +class ServiceTemplate(TypedDict, total=False): """Service template.""" name: str diff --git a/operate/services/manage.py b/operate/services/manage.py index 8840f9f43..f728ee3e2 100644 --- a/operate/services/manage.py +++ b/operate/services/manage.py @@ -585,8 +585,37 @@ def _deploy_service_onchain_from_safe( # pylint: disable=too-many-statements,to ): # TODO: This is possibly not a good idea: we are setting up a computed variable based on # the value passed in the template. - db_path = service.path / "persistent_data/memeooorr.db" - cookies_path = service.path / "persistent_data/twikit_cookies.json" + db_path = ( + service.path + / "persistent_data" + / service.env_variables["TWIKIT_USERNAME"]["value"] + / "memeooorr.db" + ) + cookies_path = ( + service.path + / "persistent_data" + / service.env_variables["TWIKIT_USERNAME"]["value"] + / "twikit_cookies.json" + ) + + # Patch: Move existing configurations to the new location + old_db_path = service.path / "persistent_data" / "memeooorr.db" + old_cookies_path = service.path / "persistent_data" / "twikit_cookies.json" + + for old_path, new_path in [ + (old_db_path, db_path), + (old_cookies_path, cookies_path), + ]: + if old_path.exists(): + self.logger.info(f"Moving {old_path} -> {new_path}") + new_path.parent.mkdir(parents=True, exist_ok=True) + try: + os.rename(old_path, new_path) + except OSError: + self.logger.info("Fallback to shutil.move") + shutil.move(str(old_path), str(new_path)) + time.sleep(3) + # End patch env_var_to_value.update( { @@ -1689,12 +1718,17 @@ def update( service_config_id: str, service_template: ServiceTemplate, allow_different_service_public_id: bool = False, + partial_update: bool = True, ) -> Service: """Update a service.""" self.logger.info(f"Updating {service_config_id=}") service = self.load(service_config_id=service_config_id) - service.update(service_template, allow_different_service_public_id) + service.update( + service_template=service_template, + allow_different_service_public_id=allow_different_service_public_id, + partial_update=partial_update, + ) return service def update_all_matching( diff --git a/operate/services/service.py b/operate/services/service.py index ec6ca340f..8a41859eb 100644 --- a/operate/services/service.py +++ b/operate/services/service.py @@ -98,7 +98,7 @@ SERVICE_CONFIG_VERSION = 4 SERVICE_CONFIG_PREFIX = "sc-" -DUMMY_MULTISIG = "0xm" +NON_EXISTENT_MULTISIG = "0xm" NON_EXISTENT_TOKEN = -1 DEFAULT_TRADER_ENV_VARS = { @@ -683,7 +683,7 @@ class Service(LocalResource): _file = "config.json" @classmethod - def migrate_format(cls, path: Path) -> bool: + def migrate_format(cls, path: Path) -> bool: # pylint: disable=too-many-statements """Migrate the JSON file format if needed.""" if not path.is_dir(): @@ -897,7 +897,7 @@ def new( # pylint: disable=too-many-locals chain_data = OnChainData( instances=[], token=NON_EXISTENT_TOKEN, - multisig=DUMMY_MULTISIG, + multisig=NON_EXISTENT_MULTISIG, staked=False, on_chain_state=OnChainState.NON_EXISTENT, user_params=OnChainUserParams.from_json(config), # type: ignore @@ -981,47 +981,65 @@ def update( self, service_template: ServiceTemplate, allow_different_service_public_id: bool = False, + partial_update: bool = False, ) -> None: """Update service.""" - target_hash = service_template["hash"] - target_service_public_id = Service.get_service_public_id(target_hash, self.path) - - if not allow_different_service_public_id and ( - self.service_public_id() != target_service_public_id - ): - raise ValueError( - f"Trying to update a service with a different public id: {self.service_public_id()=} {self.hash=} {target_service_public_id=} {target_hash=}." + target_hash = service_template.get("hash") + if target_hash: + target_service_public_id = Service.get_service_public_id( + target_hash, self.path ) + if not allow_different_service_public_id and ( + self.service_public_id() != target_service_public_id + ): + raise ValueError( + f"Trying to update a service with a different public id: {self.service_public_id()=} {self.hash=} {target_service_public_id=} {target_hash=}." + ) + + self.hash = service_template.get("hash", self.hash) + + # hash_history - Only update if latest inserted hash is different + if self.hash_history[max(self.hash_history.keys())] != self.hash: + current_timestamp = int(time.time()) + self.hash_history[current_timestamp] = self.hash + + self.home_chain = service_template.get("home_chain", self.home_chain) + self.description = service_template.get("description", self.description) + self.name = service_template.get("name", self.name) + shutil.rmtree(self.service_path) service_path = Path( IPFSTool().download( - hash_id=service_template["hash"], + hash_id=self.hash, target_dir=self.path, ) ) self.service_path = service_path - self.name = service_template["name"] - self.hash = service_template["hash"] - self.description = service_template["description"] - - # TODO temporarily disable update env variables - hotfix for Memeooorr - # self.env_variables = service_template["env_variables"] - - # Only update hash_history if latest inserted hash is different - if self.hash_history[max(self.hash_history.keys())] != service_template["hash"]: - current_timestamp = int(time.time()) - self.hash_history[current_timestamp] = service_template["hash"] - self.home_chain = service_template["home_chain"] + # env_variables + if partial_update: + for var, attrs in service_template.get("env_variables", {}).items(): + self.env_variables.setdefault(var, {}).update(attrs) + else: + self.env_variables = service_template["env_variables"] + # chain_configs + # TODO support remove chains for non-partial updates + # TODO ensure all and only existing chains are passed for non-partial updates ledger_configs = ServiceHelper(path=self.service_path).ledger_configs() - for chain, config in service_template["configurations"].items(): + for chain, new_config in service_template.get("configurations", {}).items(): if chain in self.chain_configs: # The template is providing a chain configuration that already # exists in this service - update only the user parameters. # This is to avoid losing on-chain data like safe, token, etc. + if partial_update: + config = self.chain_configs[chain].chain_data.user_params.json + config.update(new_config) + else: + config = new_config + self.chain_configs[ chain ].chain_data.user_params = OnChainUserParams.from_json( @@ -1032,15 +1050,15 @@ def update( # not currently exist in this service - copy all config as # when creating a new service. ledger_config = ledger_configs[chain] - ledger_config.rpc = config["rpc"] + ledger_config.rpc = new_config["rpc"] chain_data = OnChainData( instances=[], token=NON_EXISTENT_TOKEN, - multisig=DUMMY_MULTISIG, + multisig=NON_EXISTENT_MULTISIG, staked=False, on_chain_state=OnChainState.NON_EXISTENT, - user_params=OnChainUserParams.from_json(config), # type: ignore + user_params=OnChainUserParams.from_json(new_config), # type: ignore ) self.chain_configs[chain] = ChainConfig( diff --git a/tests/test_services_manage.py b/tests/test_services_manage.py new file mode 100644 index 000000000..4f840ceb7 --- /dev/null +++ b/tests/test_services_manage.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2024 Valory AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Tests for services.service module.""" + +import random +import string +import typing as t +from pathlib import Path + +import pytest +from deepdiff import DeepDiff + +from operate.cli import OperateApp +from operate.operate_types import ServiceTemplate + +from .test_services_service import DEFAULT_CONFIG_KWARGS + + +ROOT_PATH = Path(__file__).resolve().parent +OPERATE_HOME = ROOT_PATH / ".operate_test" + + +@pytest.fixture +def random_string() -> str: + """random_string""" + length = 8 + chars = string.ascii_letters + string.digits + return "".join(random.choices(chars, k=length)) # nosec B311 + + +def get_template(**kwargs: t.Any) -> ServiceTemplate: + """get_template""" + + return { + "name": kwargs.get("name"), + "hash": kwargs.get("hash"), + "description": kwargs.get("description"), + "image": "https://image_url", + "service_version": "", + "home_chain": "gnosis", + "configurations": { + "gnosis": { + "staking_program_id": kwargs.get("staking_program_id"), + "nft": kwargs.get("nft"), + "rpc": "http://localhost:8545", + "threshold": kwargs.get("threshold"), + "agent_id": kwargs.get("agent_id"), + "use_staking": kwargs.get("use_staking"), + "use_mech_marketplace": kwargs.get("use_mech_marketplace"), + "cost_of_bond": kwargs.get("cost_of_bond"), + "fund_requirements": { + "agent": kwargs.get("fund_requirements_agent"), + "safe": kwargs.get("fund_requirements_safe"), + }, + "fallback_chain_params": {}, + } + }, + "env_variables": { + "VAR1": { + "name": "var1_name", + "description": "var1_description", + "value": "var1_value", + "provision_type": "var1_provision_type", + }, + "VAR2": { + "name": "var2_name", + "description": "var2_description", + "value": "var2_value", + "provision_type": "var2_provision_type", + }, + }, + } + + +class TestServiceManager: + """Tests for services.manager.ServiceManager class.""" + + @pytest.mark.parametrize("update_new_var", [True, False]) + @pytest.mark.parametrize("update_update_var", [True, False]) + @pytest.mark.parametrize("update_name", [True, False]) + @pytest.mark.parametrize("update_description", [True, False]) + @pytest.mark.parametrize("update_hash", [True, False]) + def test_service_manager_partial_update( + self, + update_new_var: bool, + update_update_var: bool, + update_name: bool, + update_description: bool, + update_hash: bool, + tmp_path: Path, + random_string: str, + ) -> None: + """Test operate.service_manager().update()""" + + operate = OperateApp( + home=tmp_path / ".operate_test", + ) + operate.setup() + password = random_string + operate.create_user_account(password=password) + operate.password = password + service_manager = operate.service_manager() + service_template = get_template(**DEFAULT_CONFIG_KWARGS) + service = service_manager.create(service_template) + service_config_id = service.service_config_id + service_json = service_manager.load(service_config_id).json + + new_hash = "bafybeicts6zhavxzz2rxahz3wzs2pzamoq64n64wp4q4cdanfuz7id6c2q" + VAR2_updated_attributes = { + "name": "var2_name_updated", + "description": "var2_description_updated", + "value": "var2_value_updated", + "provision_type": "var2_provision_type_updated", + "extra_attr": "extra_val", + } + + VAR3_attributes = { + "name": "var3_name", + "description": "var3_description", + "value": "var3_value", + "provision_type": "var3_provision_type", + } + + # Partial update + update_template: t.Dict = {} + expected_service_json = service_json.copy() + + if update_new_var: + update_template["env_variables"] = update_template.get("env_variables", {}) + update_template["env_variables"]["VAR3"] = VAR3_attributes + expected_service_json["env_variables"]["VAR3"] = VAR3_attributes + + if update_update_var: + update_template["env_variables"] = update_template.get("env_variables", {}) + update_template["env_variables"]["VAR2"] = VAR2_updated_attributes + expected_service_json["env_variables"]["VAR2"] = VAR2_updated_attributes + + if update_name: + update_template["name"] = "name_updated" + expected_service_json["name"] = "name_updated" + + if update_description: + update_template["description"] = "description_updated" + expected_service_json["description"] = "description_updated" + + if update_hash: + update_template["hash"] = new_hash + expected_service_json["hash"] = new_hash + + service_manager.update( + service_config_id=service_config_id, + service_template=update_template, + allow_different_service_public_id=False, + partial_update=True, + ) + service_json = service_manager.load(service_config_id).json + + if update_hash: + timestamp = max(service_json["hash_history"].keys()) + expected_service_json["hash_history"][timestamp] = new_hash + + diff = DeepDiff(service_json, expected_service_json) + if diff: + print(diff) + + assert not diff, "Updated service does not match expected service." + + @pytest.mark.parametrize("update_new_var", [True, False]) + @pytest.mark.parametrize("update_update_var", [True, False]) + @pytest.mark.parametrize("update_delete_var", [True, False]) + @pytest.mark.parametrize("update_name", [True, False]) + @pytest.mark.parametrize("update_description", [True, False]) + @pytest.mark.parametrize("update_hash", [True, False]) + def test_service_manager_update( + self, + update_new_var: bool, + update_update_var: bool, + update_delete_var: bool, + update_name: bool, + update_description: bool, + update_hash: bool, + tmp_path: Path, + random_string: str, + ) -> None: + """Test operate.service_manager().update()""" + + operate = OperateApp( + home=tmp_path / ".operate_test", + ) + operate.setup() + password = random_string + operate.create_user_account(password=password) + operate.password = password + service_manager = operate.service_manager() + service_template = get_template(**DEFAULT_CONFIG_KWARGS) + service = service_manager.create(service_template) + service_config_id = service.service_config_id + service_json = service_manager.load(service_config_id).json + + new_hash = "bafybeicts6zhavxzz2rxahz3wzs2pzamoq64n64wp4q4cdanfuz7id6c2q" + VAR2_updated_attributes = { + "name": "var2_name_updated", + "description": "var2_description_updated", + "value": "var2_value_updated", + "provision_type": "var2_provision_type_updated", + "extra_attr": "extra_val", + } + + VAR3_attributes = { + "name": "var3_name", + "description": "var3_description", + "value": "var3_value", + "provision_type": "var3_provision_type", + } + + # Partial update + update_template: t.Dict = service_template.copy() + expected_service_json = service_json.copy() + + if update_new_var: + update_template["env_variables"] = update_template.get("env_variables", {}) + update_template["env_variables"]["VAR3"] = VAR3_attributes + expected_service_json["env_variables"]["VAR3"] = VAR3_attributes + + if update_update_var: + update_template["env_variables"] = update_template.get("env_variables", {}) + update_template["env_variables"]["VAR2"] = VAR2_updated_attributes + expected_service_json["env_variables"]["VAR2"] = VAR2_updated_attributes + + if update_delete_var: + update_template["env_variables"] = update_template.get("env_variables", {}) + del update_template["env_variables"]["VAR1"] + del expected_service_json["env_variables"]["VAR1"] + + if update_name: + update_template["name"] = "name_updated" + expected_service_json["name"] = "name_updated" + + if update_description: + update_template["description"] = "description_updated" + expected_service_json["description"] = "description_updated" + + if update_hash: + update_template["hash"] = new_hash + expected_service_json["hash"] = new_hash + + service_manager.update( + service_config_id=service_config_id, + service_template=update_template, + allow_different_service_public_id=False, + partial_update=False, + ) + service_json = service_manager.load(service_config_id).json + + if update_hash: + timestamp = max(service_json["hash_history"].keys()) + expected_service_json["hash_history"][timestamp] = new_hash + + diff = DeepDiff(service_json, expected_service_json) + if diff: + print(diff) + + assert not diff, "Updated service does not match expected service." diff --git a/tox.ini b/tox.ini index f294bf30c..27d456c3b 100644 --- a/tox.ini +++ b/tox.ini @@ -230,6 +230,9 @@ ignore_missing_imports = True [mypy-deepdiff.*] ignore_missing_imports = True +[mypy-twikit.*] +ignore_missing_imports = True + [testenv:unit-tests] deps = pytest==7.2.1