olas-operate-middleware 0.14.2__py3-none-any.whl → 0.14.4__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: olas-operate-middleware
3
- Version: 0.14.2
3
+ Version: 0.14.4
4
4
  Summary:
5
5
  License-File: LICENSE
6
6
  Author: David Vilela
@@ -17,11 +17,11 @@ Requires-Dist: deepdiff (>=8.6.1,<9.0.0)
17
17
  Requires-Dist: fastapi (==0.110.3)
18
18
  Requires-Dist: halo (==0.0.31)
19
19
  Requires-Dist: multiaddr (==0.0.9)
20
- Requires-Dist: open-aea-cli-ipfs (>=2.0.6,<3.0.0)
21
- Requires-Dist: open-aea-ledger-cosmos (>=2.0.6,<3.0.0)
22
- Requires-Dist: open-aea-ledger-ethereum (>=2.0.6,<3.0.0)
23
- Requires-Dist: open-aea-ledger-ethereum-flashbots (>=2.0.6,<3.0.0)
24
- Requires-Dist: open-autonomy (>=0.21.5,<0.22.0)
20
+ Requires-Dist: open-aea-cli-ipfs (>=2.0.8,<3.0.0)
21
+ Requires-Dist: open-aea-ledger-cosmos (>=2.0.8,<3.0.0)
22
+ Requires-Dist: open-aea-ledger-ethereum (>=2.0.8,<3.0.0)
23
+ Requires-Dist: open-aea-ledger-ethereum-flashbots (>=2.0.8,<3.0.0)
24
+ Requires-Dist: open-autonomy (>=0.21.8,<0.22.0)
25
25
  Requires-Dist: psutil (>=5.9.8,<6.0.0)
26
26
  Requires-Dist: pyinstaller (>=6.8.0,<7.0.0)
27
27
  Requires-Dist: requests-mock (>=1.12.1,<2.0.0)
@@ -6,8 +6,8 @@ operate/bridge/providers/lifi_provider.py,sha256=UzAeEnX9FGpnCYYml5lcICeEZeHHqNR
6
6
  operate/bridge/providers/native_bridge_provider.py,sha256=vAx0MtVPIAxIdQ5OKSUDhnGurYVkC8tKVJRFK9NkIdk,25088
7
7
  operate/bridge/providers/provider.py,sha256=E5d3yFe7WYZd_IbQIgDVf6F5MMJ8vMOm1o-qeqJJSCk,16981
8
8
  operate/bridge/providers/relay_provider.py,sha256=QU9H9mCAUgQx-qMKXkCGfFNimi1WGoxmKO30F2K9erw,17628
9
- operate/cli.py,sha256=2Rg4pIRZMMK7U2xgMuX3CEueOXi280cUzk-avZ3E1aw,69786
10
- operate/constants.py,sha256=oBxZEnhETCd96GWz2QDUZd-0-ofV-1deKNvFPl4-mmM,3933
9
+ operate/cli.py,sha256=ZvbG2UZyi0oyvyXMRTMtKyeDcQxlrVEhBAGlZ0nEEww,72187
10
+ operate/constants.py,sha256=ek69ACPZ8Q3Bdiuducd8zjTZOMHerh5L_xdJ5K430Ew,4001
11
11
  operate/data/README.md,sha256=jGPyZTvg2LCGdllvmYxmFMkkkiXb6YWatbqIkcX3kv4,879
12
12
  operate/data/__init__.py,sha256=ttC51Yqk9c4ehpIgs1Qbe7aJvzkrbbdZ1ClaCxJYByE,864
13
13
  operate/data/contracts/__init__.py,sha256=_th54_WvL0ibGy-b6St0Ne9DX-fyjsh-tNOKDn-cWrg,809
@@ -55,22 +55,22 @@ operate/data/contracts/uniswap_v2_erc20/tests/__init__.py,sha256=3Arw8dsCsJz6hVO
55
55
  operate/data/contracts/uniswap_v2_erc20/tests/test_contract.py,sha256=z4GfybA_oZUA6-sl61qaJ78rXdbcW_rk4APzgMzQD38,13456
56
56
  operate/keys.py,sha256=qfT3-ZS1R2jG_t7BdqUdgrAYqHnj6dNrtK5c0-_AODU,6179
57
57
  operate/ledger/__init__.py,sha256=c41SqGLSpLBvG_q1cPyDh1aunByopsIIC5Mfk1vdmQE,6207
58
- operate/ledger/profiles.py,sha256=xeHRM_qGAj9Ksl1DaMrAmn5GENmsVgpiI8s3QDBrLyM,15463
58
+ operate/ledger/profiles.py,sha256=e37WUpS48E3icEYqp8sBwbuSKRzhiQappF7JjF1br-U,15788
59
59
  operate/migration.py,sha256=hdZlhhdkoPPzkOD0CFyNYAp-eqUrVu_PJnw8_PoxpWk,21015
60
60
  operate/operate_http/__init__.py,sha256=MTS1tMZ5qnA_WzaoeLxlK9IJToMGIpkNr7_vyBeAqZ8,4680
61
61
  operate/operate_http/exceptions.py,sha256=4UFzrn-GyDD71RhkaOyFPBynL6TrrtP3eywaaU3o4fc,1339
62
- operate/operate_types.py,sha256=tsGL1vC0dSZidseG1A_2oAPEpxEHXM7YhtGVcuifBDw,13855
62
+ operate/operate_types.py,sha256=czaqIsF46bZn2ePpGNKPZ487d-5wYsab9Z-6DPJoips,14458
63
63
  operate/pearl.py,sha256=yrTpSXLu_ML3qT-uNxq3kScOyo31JyxBujiSMfMUbcg,1690
64
64
  operate/quickstart/analyse_logs.py,sha256=cAeAL2iUy0Po8Eor70tq54-Ibg-Dn8rkuaS167yjE_I,4198
65
65
  operate/quickstart/claim_staking_rewards.py,sha256=K7X1Yq0mxe3qWmFLb1Xu9-Jghhml95lS_LpM_BXii0o,3533
66
66
  operate/quickstart/reset_configs.py,sha256=DVPM4mh6Djunwq16hf8lD9-nGkkm7wVtwr2JUXr1if8,3380
67
67
  operate/quickstart/reset_password.py,sha256=jEBk2ROR1q8PkTIHlqum7E8PRQtXHwrauiy0_bik3RQ,2394
68
68
  operate/quickstart/reset_staking.py,sha256=SB5LZq9EctG4SYn2M6oPZ7R7ARHSFLRGzAqfKkpRcy0,5111
69
- operate/quickstart/run_service.py,sha256=zn-TC7xlRc036C9tXMKIDKPB4GDXMWI2pRG3AeziLWk,30355
69
+ operate/quickstart/run_service.py,sha256=Xag5daVvMBVY8aiAez3ZmYhcz5ZNLWaD5G87QDNatWU,30483
70
70
  operate/quickstart/stop_service.py,sha256=a3-1vVyZma2UtFUPKMvVrOso1Iwpz5Rzpus9VAI4qOc,2169
71
71
  operate/quickstart/terminate_on_chain_service.py,sha256=5ENU8_mkj06i80lKUX-v1QbLU0YzKeOZDUL1e_jzySE,2914
72
72
  operate/quickstart/utils.py,sha256=i1juhJCPkzB7ZKgSk5tNiRmYxGcx8MG-dTjVyC5sKys,9287
73
- operate/resource.py,sha256=0KeQkWojN737er4USSJMVC9tQEmWtXWdXj8xPEfaHqI,3954
73
+ operate/resource.py,sha256=Z0P1weLWiNRan_3gJw3etvptnBnHvBEKBZ2kBjGMuhw,4254
74
74
  operate/serialization.py,sha256=mf2uJCj1WULaTIJSYQ-XuaW3bdwc0vn-cxk2q0IJQf4,3885
75
75
  operate/services/__init__.py,sha256=isrThS-Ccu5Sc15JZgkN4uTAVaSg-NwUUSDeTyJEqLk,855
76
76
  operate/services/agent_runner.py,sha256=JGjyrzA5hX4Nuh79h81-dl2hdt74ZkC63t7UsGXY6Rw,7500
@@ -79,7 +79,7 @@ operate/services/funding_manager.py,sha256=pURhGwo2-3c7_EiQE_BuECLQsD7t_86KgL27b
79
79
  operate/services/health_checker.py,sha256=dARikrgzU1jEuK4NUqlZ7N0DQq4Ah1ZiRKHmrlh8v-A,11472
80
80
  operate/services/manage.py,sha256=9dTjXhBq6tgcFWfhU61JQL67L2h3_b_7p8OICimWWB4,113292
81
81
  operate/services/protocol.py,sha256=afq9x0MbWV0ry2h7DusVTxFWUVe6Q3pVAZGhzb2RznQ,71310
82
- operate/services/service.py,sha256=YOU0rlPe5PjDnFRlAcThYLvr5MrPOftBDKZHMRNzZ78,46647
82
+ operate/services/service.py,sha256=IIgDS2y72CxM8km1msVSDcdHXARunPx4wC5pTDNBOt4,50420
83
83
  operate/services/utils/__init__.py,sha256=TvioaZ1mfTRUSCtrQoLNAp4WMVXyqEJqFJM4PxSQCRU,24
84
84
  operate/services/utils/mech.py,sha256=98gNw8pMNvv_O34V1blr7JUwenqxFeeyFuXLuSYv10w,3864
85
85
  operate/services/utils/tendermint.py,sha256=M4zjF97SOJomhmj97bWKIphnia30lbDie65fs_vy_q8,25686
@@ -91,8 +91,8 @@ operate/utils/ssl.py,sha256=O5DrDoZD4T4qQuHP8GLwWUVxQ-1qXeefGp6uDJiF2lM,4308
91
91
  operate/wallet/__init__.py,sha256=NGiozD3XhvkBi7_FaOWQ8x1thZPK4uGpokJaeDY_o2w,813
92
92
  operate/wallet/master.py,sha256=019VvWsMfzAV3fUienhcYVLB12BcSVvub3weAeMOlSA,33808
93
93
  operate/wallet/wallet_recovery_manager.py,sha256=kZIKBCIVb-ufntUoCE0IqAJ-Q2YUIl7955UYY6sp8Os,19856
94
- olas_operate_middleware-0.14.2.dist-info/METADATA,sha256=RMbhPtaO2PcCJJJxdgyi2N0ihdzE0Rt1M83ETVeLltM,1492
95
- olas_operate_middleware-0.14.2.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
96
- olas_operate_middleware-0.14.2.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
97
- olas_operate_middleware-0.14.2.dist-info/licenses/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
98
- olas_operate_middleware-0.14.2.dist-info/RECORD,,
94
+ olas_operate_middleware-0.14.4.dist-info/METADATA,sha256=PQtCwNBLxX-1tjPEuSA4IguTXqOD_eLPPjFS1G4FwzU,1492
95
+ olas_operate_middleware-0.14.4.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
96
+ olas_operate_middleware-0.14.4.dist-info/entry_points.txt,sha256=dM1g2I7ODApKQFcgl5J4NGA7pfBTo6qsUTXM-j2OLlw,44
97
+ olas_operate_middleware-0.14.4.dist-info/licenses/LICENSE,sha256=mdBDB-mWKV5Cz4ejBzBiKqan6Z8zVLAh9xwM64O2FW4,11339
98
+ olas_operate_middleware-0.14.4.dist-info/RECORD,,
operate/cli.py CHANGED
@@ -37,7 +37,7 @@ from types import FrameType
37
37
  import autonomy.chain.tx
38
38
  from aea.helpers.logging import setup_logger
39
39
  from clea import group, params, run
40
- from fastapi import FastAPI, Request
40
+ from fastapi import FastAPI, Query, Request
41
41
  from fastapi.middleware.cors import CORSMiddleware
42
42
  from fastapi.responses import JSONResponse
43
43
  from typing_extensions import Annotated
@@ -1066,6 +1066,68 @@ def create_app( # pylint: disable=too-many-locals, unused-argument, too-many-st
1066
1066
  deployment_json["healthcheck"] = service.get_latest_healthcheck()
1067
1067
  return JSONResponse(content=deployment_json)
1068
1068
 
1069
+ @app.get("/api/v2/service/{service_config_id}/achievements")
1070
+ async def _get_service_achievements(
1071
+ request: Request, include_acknowledged: bool = Query(False) # noqa: B008
1072
+ ) -> JSONResponse:
1073
+ """Get the service achievements."""
1074
+ service_config_id = request.path_params["service_config_id"]
1075
+
1076
+ if not operate.service_manager().exists(service_config_id=service_config_id):
1077
+ return service_not_found_error(service_config_id=service_config_id)
1078
+
1079
+ service = operate.service_manager().load(service_config_id=service_config_id)
1080
+
1081
+ achievements_json = service.get_achievements_notifications(
1082
+ include_acknowledged=include_acknowledged,
1083
+ )
1084
+
1085
+ return JSONResponse(content=achievements_json)
1086
+
1087
+ @app.post(
1088
+ "/api/v2/service/{service_config_id}/achievement/{achievement_id}/acknowledge"
1089
+ )
1090
+ async def _acknowledge_achievement(request: Request) -> JSONResponse:
1091
+ """Update a service."""
1092
+ if operate.password is None:
1093
+ return USER_NOT_LOGGED_IN_ERROR
1094
+
1095
+ service_config_id = request.path_params["service_config_id"]
1096
+ manager = operate.service_manager()
1097
+
1098
+ if not manager.exists(service_config_id=service_config_id):
1099
+ return service_not_found_error(service_config_id=service_config_id)
1100
+
1101
+ service = operate.service_manager().load(service_config_id=service_config_id)
1102
+
1103
+ achievement_id = request.path_params["achievement_id"]
1104
+
1105
+ try:
1106
+ service.acknowledge_achievement(
1107
+ achievement_id=achievement_id,
1108
+ )
1109
+ except KeyError:
1110
+ return JSONResponse(
1111
+ content={
1112
+ "error": f"Achievement {achievement_id} does not exist for service {service_config_id}."
1113
+ },
1114
+ status_code=HTTPStatus.NOT_FOUND,
1115
+ )
1116
+ except ValueError:
1117
+ return JSONResponse(
1118
+ content={
1119
+ "error": f"Achievement {achievement_id} was already acknowledged for service {service_config_id}."
1120
+ },
1121
+ status_code=HTTPStatus.BAD_REQUEST,
1122
+ )
1123
+
1124
+ return JSONResponse(
1125
+ content={
1126
+ "error": None,
1127
+ "message": f"Acknowledged achievement_id {achievement_id} for service {service_config_id} successfully.",
1128
+ }
1129
+ )
1130
+
1069
1131
  @app.get("/api/v2/service/{service_config_id}/agent_performance")
1070
1132
  async def _get_agent_performance(request: Request) -> JSONResponse:
1071
1133
  """Get the service refill requirements."""
operate/constants.py CHANGED
@@ -35,6 +35,7 @@ DEPLOYMENT_JSON = "deployment.json"
35
35
  CONFIG_JSON = "config.json"
36
36
  USER_JSON = "user.json"
37
37
  HEALTHCHECK_JSON = "healthcheck.json"
38
+ ACHIEVEMENTS_NOTIFICATIONS_JSON = "achievements_notifications.json"
38
39
  VERSION_FILE = "operate.version"
39
40
  SETTINGS_JSON = "settings.json"
40
41
  FUNDING_REQUIREMENTS_JSON = "funding_requirements.json"
@@ -102,6 +102,8 @@ STAKING: t.Dict[Chain, t.Dict[str, str]] = {
102
102
  "quickstart_beta_mech_marketplace_expert_8": "0x168aED532a0CD8868c22Fc77937Af78b363652B1",
103
103
  "quickstart_beta_mech_marketplace_expert_9": "0xdDa9cD214F12e7C2D58E871404A0A3B1177065C8",
104
104
  "quickstart_beta_mech_marketplace_expert_10": "0x53a38655B4e659eF4C7F88A26fbF5c67932C7156",
105
+ "quickstart_beta_mech_marketplace_expert_11": "0x1eaDe40561C61fa7AcC5D816b1FC55a8d9B58519",
106
+ "quickstart_beta_mech_marketplace_expert_12": "0x99Fe6B5C9980Fc3A44b1Dc32A76Db6aDfcf4c75e",
105
107
  "mech_marketplace": "0x998dEFafD094817EF329f6dc79c703f1CF18bC90",
106
108
  "marketplace_supply_alpha": "0xCAbD0C941E54147D40644CF7DA7e36d70DF46f44",
107
109
  "marketplace_demand_alpha_1": "0x9d6e7aB0B5B48aE5c146936147C639fEf4575231",
@@ -157,6 +159,10 @@ DEFAULT_PRIORITY_MECH = { # maps mech marketplace address to its default priori
157
159
  "0xC05e7412439bD7e91730a6880E18d5D5873F632C",
158
160
  2182,
159
161
  ),
162
+ "0x343F2B005cF6D70bA610CD9F1F1927049414B582": (
163
+ "0x45F25db135E83d7a010b05FFc1202F8473E3ae7D",
164
+ 25,
165
+ ),
160
166
  }
161
167
 
162
168
 
operate/operate_types.py CHANGED
@@ -34,7 +34,11 @@ from autonomy.chain.config import LedgerType as LedgerTypeOA
34
34
  from cryptography.fernet import Fernet
35
35
  from typing_extensions import TypedDict
36
36
 
37
- from operate.constants import FERNET_KEY_LENGTH, NO_STAKING_PROGRAM_ID
37
+ from operate.constants import (
38
+ ACHIEVEMENTS_NOTIFICATIONS_JSON,
39
+ FERNET_KEY_LENGTH,
40
+ NO_STAKING_PROGRAM_ID,
41
+ )
38
42
  from operate.resource import LocalResource
39
43
  from operate.serialization import BigInt, serialize
40
44
 
@@ -220,6 +224,30 @@ class OnChainData(LocalResource):
220
224
  user_params: OnChainUserParams
221
225
 
222
226
 
227
+ @dataclass
228
+ class AchievementNotification(LocalResource):
229
+ """AchievementNotification"""
230
+
231
+ achievement_id: str
232
+ acknowledged: bool
233
+ acknowledgement_timestamp: int
234
+
235
+ @classmethod
236
+ def from_json(cls, obj: t.Dict) -> "ChainConfig":
237
+ """Load the chain config."""
238
+ return super().from_json(obj) # type: ignore
239
+
240
+
241
+ @dataclass
242
+ class AchievementsNotifications(LocalResource):
243
+ """AchievementsNotifications"""
244
+
245
+ path: Path
246
+ notifications: t.Dict[str, AchievementNotification]
247
+
248
+ _file = ACHIEVEMENTS_NOTIFICATIONS_JSON
249
+
250
+
223
251
  @dataclass
224
252
  class ChainConfig(LocalResource):
225
253
  """Chain config."""
@@ -109,6 +109,8 @@ QS_STAKING_PROGRAMS: t.Dict[Chain, t.Dict[str, str]] = {
109
109
  "quickstart_beta_mech_marketplace_expert_8": "trader",
110
110
  "quickstart_beta_mech_marketplace_expert_9": "trader",
111
111
  "quickstart_beta_mech_marketplace_expert_10": "trader",
112
+ "quickstart_beta_mech_marketplace_expert_11": "trader",
113
+ "quickstart_beta_mech_marketplace_expert_12": "trader",
112
114
  "mech_marketplace": "mech",
113
115
  "marketplace_supply_alpha": "mech",
114
116
  },
operate/resource.py CHANGED
@@ -65,6 +65,16 @@ class LocalResource:
65
65
  kwargs[pname] = deserialize(obj=obj[pname], otype=ptype)
66
66
  return cls(**kwargs)
67
67
 
68
+ @classmethod
69
+ def exists_at(cls, path: Path) -> bool:
70
+ """Verifies if local resource exists at specified path."""
71
+ file = (
72
+ path / cls._file
73
+ if cls._file is not None and path.name != cls._file
74
+ else path
75
+ )
76
+ return file.exists()
77
+
68
78
  @classmethod
69
79
  def load(cls, path: Path) -> "LocalResource":
70
80
  """Load local resource."""
@@ -80,6 +80,8 @@ from operate.ledger import get_default_ledger_api, get_default_rpc
80
80
  from operate.ledger.profiles import WRAPPED_NATIVE_ASSET
81
81
  from operate.operate_http.exceptions import NotAllowed
82
82
  from operate.operate_types import (
83
+ AchievementNotification,
84
+ AchievementsNotifications,
83
85
  AgentRelease,
84
86
  Chain,
85
87
  ChainAmounts,
@@ -1028,13 +1030,107 @@ class Service(LocalResource):
1028
1030
  if isinstance(data, dict):
1029
1031
  agent_performance.update(data)
1030
1032
  except (json.JSONDecodeError, OSError) as e:
1031
- # Keep default values if file is invalid
1032
- print(
1033
- f"Error reading file 'agent_performance.json': {e}"
1034
- ) # TODO Use logger
1033
+ logger.warning(f"Cannot read file 'agent_performance.json': {e}")
1035
1034
 
1036
1035
  return dict(sorted(agent_performance.items()))
1037
1036
 
1037
+ def _load_achievements_notifications(
1038
+ self,
1039
+ ) -> t.Tuple[AchievementsNotifications, t.Dict[str, t.Any]]:
1040
+ if not AchievementsNotifications.exists_at(self.path):
1041
+ AchievementsNotifications(
1042
+ path=self.path,
1043
+ notifications={},
1044
+ ).store()
1045
+
1046
+ achievements_notifications: AchievementsNotifications = t.cast(
1047
+ AchievementsNotifications, AchievementsNotifications.load(self.path)
1048
+ )
1049
+
1050
+ agent_achievements_json_path = (
1051
+ Path(
1052
+ self.env_variables.get(
1053
+ AGENT_PERSISTENT_STORAGE_ENV_VAR, {"value": "."}
1054
+ ).get("value", ".")
1055
+ )
1056
+ / "achievements.json"
1057
+ )
1058
+
1059
+ agent_achievements: t.Dict[str, t.Any] = {}
1060
+ if agent_achievements_json_path.exists():
1061
+ try:
1062
+ with open(agent_achievements_json_path, "r", encoding="utf-8") as f:
1063
+ agent_achievements = json.load(f)
1064
+ except (json.JSONDecodeError, OSError) as e:
1065
+ print(f"Error reading file 'achievements.json': {e}")
1066
+
1067
+ save_changes = False
1068
+ for achievement_id in agent_achievements:
1069
+ if achievement_id not in achievements_notifications.notifications:
1070
+ achievements_notifications.notifications[achievement_id] = (
1071
+ AchievementNotification(
1072
+ achievement_id=achievement_id,
1073
+ acknowledged=False,
1074
+ acknowledgement_timestamp=0,
1075
+ )
1076
+ )
1077
+ save_changes = True
1078
+
1079
+ if save_changes:
1080
+ achievements_notifications.store()
1081
+
1082
+ return achievements_notifications, agent_achievements
1083
+
1084
+ def get_achievements_notifications(
1085
+ self, include_acknowledged: bool
1086
+ ) -> t.List[t.Dict]:
1087
+ """Return the achievements notifications"""
1088
+
1089
+ achievements_notifications, agent_achievements = (
1090
+ self._load_achievements_notifications()
1091
+ )
1092
+
1093
+ output: t.Dict[str, t.Dict] = {}
1094
+
1095
+ for (
1096
+ achievement_id,
1097
+ achievement_notification,
1098
+ ) in achievements_notifications.notifications.items():
1099
+ acknowledged = achievement_notification.acknowledged
1100
+ if not acknowledged or (acknowledged and include_acknowledged):
1101
+ if achievement_id in agent_achievements:
1102
+ output[achievement_id] = achievement_notification.json
1103
+ output[achievement_id].update(agent_achievements[achievement_id])
1104
+ else:
1105
+ logger.warning(
1106
+ f"Achievement {achievement_id} from notifications database is not present in agent achievements file (Corrupted file?)."
1107
+ )
1108
+
1109
+ return list(output.values())
1110
+
1111
+ def acknowledge_achievement(self, achievement_id: str) -> None:
1112
+ """Acknowledge an achievement id"""
1113
+
1114
+ achievements_notifications, _ = self._load_achievements_notifications()
1115
+
1116
+ if achievement_id not in achievements_notifications.notifications:
1117
+ raise KeyError(
1118
+ f"Achievement {achievement_id} does not exist for service {self.service_config_id}."
1119
+ )
1120
+
1121
+ achievement_notification = achievements_notifications.notifications[
1122
+ achievement_id
1123
+ ]
1124
+
1125
+ if achievement_notification.acknowledged:
1126
+ raise ValueError(
1127
+ f"Achievement {achievement_id} was already acknowledged for service {self.service_config_id}."
1128
+ )
1129
+
1130
+ achievement_notification.acknowledgement_timestamp = int(time.time())
1131
+ achievement_notification.acknowledged = True
1132
+ achievements_notifications.store()
1133
+
1038
1134
  def update(
1039
1135
  self,
1040
1136
  service_template: ServiceTemplate,