Mirror of https://github.com/roostorg/osprey github.com/roostorg/osprey
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add an output sink writing results to minio, pluggify storage option (#15)

authored by

Chenyu and committed by
GitHub
2f80e4ee c498c35a

+482 -109
+8
docker-compose.yaml
··· 98 98 condition: service_completed_successfully 99 99 minio: 100 100 condition: service_healthy 101 + minio-bucket-init: 102 + condition: service_completed_successfully 101 103 ports: 102 104 - "5001:5000" 103 105 command: ["osprey-worker"] ··· 117 119 - DD_DOGSTATSD_DISABLE=True 118 120 - OSPREY_RULES_SINK_NUM_WORKERS=1 119 121 - BIGTABLE_EMULATOR_HOST=bigtable:8361 122 + - OSPREY_EXECUTION_RESULT_STORAGE_BACKEND=minio 123 + - OSPREY_MINIO_ENDPOINT=minio:9000 124 + - OSPREY_MINIO_ACCESS_KEY=minioadmin 125 + - OSPREY_MINIO_SECRET_KEY=minioadmin123 126 + - OSPREY_MINIO_SECURE=false 127 + - OSPREY_MINIO_EXECUTION_RESULTS_BUCKET=execution-output 120 128 - SNOWFLAKE_API_ENDPOINT=http://snowflake:8080 121 129 - OSPREY_RULES_PATH=./example_rules 122 130 volumes:
+3 -1
example_rules/main.sml
··· 1 - Require(rule='rules/post_contains_hello.sml') 1 + Import(rules=['models/base.sml']) 2 + 3 + Require(rule='rules/post_contains_hello.sml')
+3
example_rules/models/base.sml
··· 9 9 path='$.event_type', 10 10 coerce_type=True 11 11 ) 12 + 13 + ActionName=GetActionName() 14 +
+2 -1
osprey_worker/src/osprey/worker/_stdlibplugin/__init__.py
··· 1 1 # Imports needed to work with the plugin system - if you modify this make sure to also modify the pyproject.toml file 2 2 from osprey.worker._stdlibplugin.sink_register import register_output_sinks 3 + from osprey.worker._stdlibplugin.storage_register import register_execution_result_store 3 4 from osprey.worker._stdlibplugin.udf_register import register_udfs 4 5 from osprey.worker._stdlibplugin.validator_regsiter import register_ast_validators 5 6 6 - __all__ = ['register_output_sinks', 'register_udfs', 'register_ast_validators'] 7 + __all__ = ['register_output_sinks', 'register_udfs', 'register_ast_validators', 'register_execution_result_store']
+5 -1
osprey_worker/src/osprey/worker/_stdlibplugin/sink_register.py
··· 1 1 from typing import List, Sequence 2 2 3 3 from kafka import KafkaProducer 4 - from osprey.worker.adaptor.plugin_manager import hookimpl_osprey 4 + from osprey.worker.adaptor.plugin_manager import hookimpl_osprey, bootstrap_execution_result_store 5 5 from osprey.worker.lib.config import Config 6 6 from osprey.worker.sinks.sink.kafka_output_sink import KafkaOutputSink 7 7 from osprey.worker.sinks.sink.output_sink import BaseOutputSink, StdoutOutputSink 8 + from osprey.worker.sinks.sink.stored_execution_result_output_sink import StoredExecutionResultOutputSink 8 9 9 10 10 11 @hookimpl_osprey ··· 22 23 kafka_producer=KafkaProducer(bootstrap_servers=bootstrap_servers, client_id=client_id), 23 24 ) 24 25 ) 26 + execution_result_store = bootstrap_execution_result_store(config=config) 27 + if execution_result_store is not None: 28 + sinks.append(StoredExecutionResultOutputSink()) 25 29 return sinks
+16
osprey_worker/src/osprey/worker/_stdlibplugin/storage_register.py
··· 1 + from osprey.worker.adaptor.plugin_manager import hookimpl_osprey 2 + from osprey.worker.lib.config import Config 3 + from osprey.worker.lib.storage.stored_execution_result import ExecutionResultStore, StoredExecutionResultMinIO 4 + 5 + 6 + @hookimpl_osprey 7 + def register_execution_result_store(config: Config) -> ExecutionResultStore: 8 + endpoint = config.get_str('OSPREY_MINIO_ENDPOINT', 'minio:9000') 9 + access_key = config.get_str('OSPREY_MINIO_ACCESS_KEY', 'minioadmin') 10 + secret_key = config.get_str('OSPREY_MINIO_SECRET_KEY', 'minioadmin123') 11 + secure = config.get_bool('OSPREY_MINIO_SECURE', False) 12 + bucket_name = config.get_str('OSPREY_MINIO_EXECUTION_RESULTS_BUCKET', 'execution-output') 13 + 14 + return StoredExecutionResultMinIO( 15 + endpoint=endpoint, access_key=access_key, secret_key=secret_key, secure=secure, bucket_name=bucket_name 16 + )
+7
osprey_worker/src/osprey/worker/adaptor/hookspecs/osprey_hooks.py
··· 13 13 14 14 if TYPE_CHECKING: 15 15 from osprey.worker.lib.config import Config 16 + from osprey.worker.lib.storage.stored_execution_result import ExecutionResultStore 16 17 from osprey.worker.sinks.sink.output_sink import BaseOutputSink 17 18 18 19 hookspec: pluggy.HookspecMarker = pluggy.HookspecMarker(OSPREY_ADAPTOR) ··· 45 46 @hookspec(firstresult=True) 46 47 def register_input_stream(config: Config) -> BaseInputStream[BaseAckingContext[Action]]: 47 48 raise NotImplementedError('register_input_stream must be implemented by the plugin') 49 + 50 + 51 + @hookspec(firstresult=True) 52 + def register_execution_result_store(config: Config) -> ExecutionResultStore: 53 + """Register an execution result storage backend instance.""" 54 + raise NotImplementedError('register_execution_result_store must be implemented by the plugin')
+11
osprey_worker/src/osprey/worker/adaptor/plugin_manager.py
··· 91 91 return stream 92 92 else: 93 93 return None 94 + 95 + 96 + def bootstrap_execution_result_store(config: Config): 97 + """Get the execution result storage backend from plugins.""" 98 + load_all_osprey_plugins() 99 + 100 + try: 101 + store = plugin_manager.hook.register_execution_result_store(config=config) 102 + return store 103 + except Exception: 104 + return None
+2 -1
osprey_worker/src/osprey/worker/lib/acls/definitions/super_user.json
··· 41 41 "name": "CAN_VIEW_EVENTS_BY_ACTION", 42 42 "allow_all": true 43 43 } 44 - ] 44 + ], 45 + "ability_groups": ["CAN_VIEW_BASIC_USER_DATA"] 45 46 }
osprey_worker/src/osprey/worker/lib/acls/groups/_empty.py

This is a binary file and will not be displayed.

+13
osprey_worker/src/osprey/worker/lib/acls/groups/can_view_basic_user_data.json
··· 1 + { 2 + "description": "Grants ability to view basic user data.", 3 + "acl": [ 4 + { 5 + "name": "CAN_VIEW_ACTION_DATA", 6 + "allow_all": true 7 + }, 8 + { 9 + "name": "CAN_VIEW_FEATURE_DATA", 10 + "allow_all": true 11 + } 12 + ] 13 + }
+7
osprey_worker/src/osprey/worker/lib/snowflake.py
··· 32 32 epoch = CONFIG.instance().get_int('SNOWFLAKE_EPOCH', 0) 33 33 return ((self.id >> 22) + epoch) / 1000.0 34 34 35 + def to_key_prefix(self) -> str: 36 + timestamp_portion = self.id >> 22 37 + # reverse the last 4 characters of the timestamp to create a 38 + # uniformly distributed prefix space. 39 + key_prefix = str(timestamp_portion)[:-5:-1] 40 + return key_prefix 41 + 35 42 def __str__(self) -> str: 36 43 return self.to_str() 37 44
+231 -91
osprey_worker/src/osprey/worker/lib/storage/stored_execution_result.py
··· 2 2 3 3 import gzip 4 4 import json 5 + from abc import ABC, abstractmethod 5 6 from datetime import datetime 6 7 from typing import TYPE_CHECKING, Any, Dict, List, Optional, Sequence 7 8 8 9 import gevent 9 10 import google.cloud.storage as storage 11 + from osprey.worker.lib.snowflake import Snowflake 10 12 import pytz 11 13 from google.api_core import retry 12 14 from google.cloud.bigtable import row_filters 13 15 from google.cloud.bigtable.row import Row 16 + from io import BytesIO 17 + from minio import Minio 18 + from minio.error import S3Error 14 19 from osprey.engine.executor.execution_context import ExecutionResult 15 20 from osprey.worker.lib.instruments import metrics 16 21 from osprey.worker.lib.osprey_shared.logging import get_logger ··· 23 28 if TYPE_CHECKING: 24 29 from osprey.worker.ui_api.osprey.lib.abilities import DataCensorAbility 25 30 26 - SCYLLA_CONCURRENCY_LIMIT = 100 27 31 BIGTABLE_CONCURRENCY_LIMIT = 100 28 32 GCS_CONCURRENCY_LIMIT = 100 33 + MINIO_CONCURRENCY_LIMIT = 100 34 + 35 + 36 + class ExecutionResultStore(ABC): 37 + """Abstract base class for execution result storage backends.""" 38 + 39 + @abstractmethod 40 + def select_one(self, action_id: int) -> Optional[Dict[str, Any]]: 41 + """Retrieve a single execution result by action ID.""" 42 + pass 43 + 44 + @abstractmethod 45 + def select_many(self, action_ids: List[int]) -> List[Dict[str, Any]]: 46 + """Retrieve multiple execution results by action IDs.""" 47 + pass 48 + 49 + @abstractmethod 50 + def insert( 51 + self, 52 + action_id: int, 53 + extracted_features_json: str, 54 + error_traces_json: str, 55 + timestamp: datetime, 56 + action_data_json: str, 57 + ) -> None: 58 + """Insert an execution result.""" 59 + pass 29 60 30 61 31 62 class ErrorTrace(BaseModel): ··· 35 66 36 67 class StoredExecutionResult(BaseModel): 37 68 """ 38 - Stores the execution result in GCS and BigTable, reads result from GCS with BigTable fallback. 69 + Represents a stored execution result with methods to persist and retrieve it using a storage backend. 39 70 """ 40 71 41 72 # NOTE: These fields must match the database column names exactly. ··· 46 77 action_data: Optional[Dict[str, Any]] = None 47 78 48 79 @classmethod 49 - def persist_from_execution_result(cls, execution_result: ExecutionResult) -> None: 50 - """Persist execution result to GCS and BigTable.""" 51 - StoredExecutionResultGCS.insert( 52 - action_id=execution_result.action.action_id, 53 - extracted_features_json=execution_result.extracted_features_json, 54 - error_traces_json=execution_result.error_traces_json, 55 - action_data_json=execution_result.action.data_json, 56 - timestamp=execution_result.action.timestamp, 57 - ) 58 - StoredExecutionResultBigTable.insert( 80 + def persist_from_execution_result( 81 + cls, execution_result: ExecutionResult, storage_backend: ExecutionResultStore 82 + ) -> None: 83 + """Persist execution result using the provided storage backend.""" 84 + storage_backend.insert( 59 85 action_id=execution_result.action.action_id, 60 86 extracted_features_json=execution_result.extracted_features_json, 61 87 error_traces_json=execution_result.error_traces_json, ··· 65 91 66 92 @classmethod 67 93 def get_one_with_action_data( 68 - cls, event_record_id: int, data_censor_abilities: Sequence[Optional[DataCensorAbility[Any, Any]]] = () 94 + cls, 95 + event_record_id: int, 96 + storage_backend: ExecutionResultStore, 97 + data_censor_abilities: Sequence[Optional[DataCensorAbility[Any, Any]]] = (), 69 98 ) -> Optional['StoredExecutionResult']: 70 - """Get execution result from GCS, fallback to BigTable if not found.""" 71 - gcs_event_record = StoredExecutionResultGCS.select_one(event_record_id) 72 - if gcs_event_record: 73 - return StoredExecutionResult.parse_from_query_result(gcs_event_record, data_censor_abilities) 74 - 75 - bigtable_event_record = StoredExecutionResultBigTable.select_one(event_record_id) 76 - if not bigtable_event_record: 77 - return None 78 - return StoredExecutionResult.parse_from_query_result(bigtable_event_record, data_censor_abilities) 99 + """Get execution result from the provided storage backend.""" 100 + result = storage_backend.select_one(event_record_id) 101 + if result: 102 + return StoredExecutionResult.parse_from_query_result(result, data_censor_abilities) 103 + return None 79 104 80 105 @classmethod 81 106 def get_many( 82 - cls, action_ids: List[int], data_censor_abilities: Sequence[Optional[DataCensorAbility[Any, Any]]] = () 107 + cls, 108 + action_ids: List[int], 109 + storage_backend: ExecutionResultStore, 110 + data_censor_abilities: Sequence[Optional[DataCensorAbility[Any, Any]]] = (), 83 111 ) -> List['StoredExecutionResult']: 84 - gcs_results = StoredExecutionResultGCS.select_many(action_ids) 85 - found_action_ids = {result['id'] for result in gcs_results} 112 + """Get execution results from the provided storage backend.""" 113 + results = storage_backend.select_many(action_ids) 86 114 87 - missing_action_ids = [aid for aid in action_ids if aid not in found_action_ids] 88 - bigtable_results = [] 89 - if missing_action_ids: 90 - bigtable_results = StoredExecutionResultBigTable.select_many(missing_action_ids) 91 - 92 - combined_results = gcs_results + bigtable_results 93 115 return sorted( 94 - [ 95 - StoredExecutionResult.parse_from_query_result(result, data_censor_abilities) 96 - for result in combined_results 97 - ], 116 + [StoredExecutionResult.parse_from_query_result(result, data_censor_abilities) for result in results], 98 117 key=lambda r: pytz.utc.localize(r.timestamp) if r.timestamp.tzinfo is None else r.timestamp, 99 118 reverse=True, 100 119 ) ··· 159 178 160 179 161 180 # TODO: Add tests 162 - class StoredExecutionResultBigTable: 181 + class StoredExecutionResultBigTable(ExecutionResultStore): 163 182 retry_policy = retry.Retry(initial=1.0, maximum=2.0, multiplier=1.25, deadline=120.0) 164 183 165 - @classmethod 166 - def select_one(cls, action_id: int) -> Optional[Dict[str, Any]]: 184 + def select_one(self, action_id: int) -> Optional[Dict[str, Any]]: 167 185 row = osprey_bigtable.table('stored_execution_result').read_row( 168 - cls._encode_action_id(action_id), row_filters.CellsColumnLimitFilter(1) 186 + StoredExecutionResultBigTable._encode_action_id(action_id), row_filters.CellsColumnLimitFilter(1) 169 187 ) 170 188 if not row: 171 189 return None 172 190 173 - return cls._execution_result_dict_from_row(row) 191 + return StoredExecutionResultBigTable._execution_result_dict_from_row(row) 174 192 175 193 # TODO: Add `select_*_minimal` methods 176 194 177 - @classmethod 178 - def select_many(cls, action_ids: List[int]) -> List[Dict[str, Any]]: 195 + def select_many(self, action_ids: List[int]) -> List[Dict[str, Any]]: 179 196 return [ 180 197 row 181 - for row in gevent.pool.Pool(BIGTABLE_CONCURRENCY_LIMIT).imap(cls.select_one, action_ids) 198 + for row in gevent.pool.Pool(BIGTABLE_CONCURRENCY_LIMIT).imap(self.select_one, action_ids) 182 199 if row is not None 183 200 ] 184 201 185 - @classmethod 186 202 def insert( 187 - cls, 203 + self, 188 204 action_id: int, 189 205 extracted_features_json: str, 190 206 error_traces_json: str, 191 207 timestamp: datetime, 192 208 action_data_json: str, 193 209 ) -> None: 194 - row = osprey_bigtable.table('stored_execution_result').row(cls._encode_action_id(action_id)) 210 + row = osprey_bigtable.table('stored_execution_result').row( 211 + StoredExecutionResultBigTable._encode_action_id(action_id) 212 + ) 195 213 row.set_cell('execution_result', b'extracted_features', extracted_features_json.encode(), timestamp=timestamp) 196 214 row.set_cell('execution_result', b'error_traces', error_traces_json.encode(), timestamp=timestamp) 197 215 row.set_cell('execution_result', b'timestamp', timestamp.isoformat().encode(), timestamp=timestamp) 198 216 row.set_cell('execution_result', b'action_data', action_data_json.encode(), timestamp=timestamp) 199 - osprey_bigtable.table('stored_execution_result').mutate_rows([row], retry=cls.retry_policy) 217 + osprey_bigtable.table('stored_execution_result').mutate_rows([row], retry=self.retry_policy) 200 218 201 - @classmethod 202 - def _encode_action_id(cls, action_id_snowflake: int) -> bytes: 219 + @staticmethod 220 + def _encode_action_id(action_id_snowflake: int) -> bytes: 203 221 """Constructs a bigtable key for a given snowflake.""" 204 - timestamp_portion = action_id_snowflake >> 22 205 - # reverse the last 4 characters of the timestamp to create a 206 - # uniformly distributed prefix space. 207 - key_prefix = str(timestamp_portion)[:-5:-1] 222 + key_prefix = Snowflake(action_id_snowflake).to_key_prefix() 208 223 return f'{key_prefix}:{action_id_snowflake}'.encode() 209 224 210 - @classmethod 211 - def _decode_action_id(cls, bigtable_key: bytes) -> int: 225 + @staticmethod 226 + def _decode_action_id(bigtable_key: bytes) -> int: 212 227 """Extracts the snowflake portion of a bigtable key produced by `to_bigtable_key`""" 213 228 _prefix, _, snowflake = bigtable_key.decode('utf-8').partition(':') 214 229 return int(snowflake) 215 230 216 - @classmethod 217 - def _execution_result_dict_from_row(cls, row: Row) -> Dict[str, Any]: 231 + @staticmethod 232 + def _execution_result_dict_from_row(row: Row) -> Dict[str, Any]: 218 233 # row.cells doesn't have the right type information setup (at least in this version of bt), so its ignored here. 219 234 extracted_features = row.cells['execution_result'][b'extracted_features'][0].value.decode('utf-8') # type: ignore[attr-defined] 220 235 error_traces = row.cells['execution_result'][b'error_traces'][0].value.decode('utf-8') # type: ignore[attr-defined] ··· 222 237 timestamp = row.cells['execution_result'][b'timestamp'][0].timestamp # type: ignore[attr-defined] 223 238 224 239 execution_result_dict = { 225 - 'id': cls._decode_action_id(row.row_key), 240 + 'id': StoredExecutionResultBigTable._decode_action_id(row.row_key), 226 241 'extracted_features': extracted_features, 227 242 'error_traces': error_traces, 228 243 'timestamp': timestamp, ··· 236 251 return execution_result_dict 237 252 238 253 239 - class StoredExecutionResultGCS: 240 - _gcs_client: storage.Client | None = None 241 - _bucket_name: str | None = None 254 + class StoredExecutionResultGCS(ExecutionResultStore): 255 + def __init__(self): 256 + self._gcs_client: storage.Client | None = None 257 + self._bucket_name: str | None = None 242 258 243 - @classmethod 244 - def _get_gcs_client(cls) -> storage.Client: 245 - if cls._gcs_client is None: 259 + def _get_gcs_client(self) -> storage.Client: 260 + if self._gcs_client is None: 246 261 from osprey.worker.lib.singletons import CONFIG 247 262 248 263 config = CONFIG.instance() 249 264 project_id = config.get_str('OSPREY_GCP_PROJECT_ID', 'osprey-dev') 250 - cls._gcs_client = storage.Client(project=project_id) 251 - return cls._gcs_client 265 + self._gcs_client = storage.Client(project=project_id) 266 + return self._gcs_client 252 267 253 - @classmethod 254 - def _get_bucket_name(cls) -> str: 255 - if cls._bucket_name is None: 268 + def _get_bucket_name(self) -> str: 269 + if self._bucket_name is None: 256 270 from osprey.worker.lib.singletons import CONFIG 257 271 258 272 config = CONFIG.instance() 259 - cls._bucket_name = config.get_str('OSPREY_GCS_EXECUTION_RESULTS_BUCKET', 'osprey-execution-results-stg') 260 - return cls._bucket_name 273 + self._bucket_name = config.get_str('OSPREY_GCS_EXECUTION_RESULTS_BUCKET', 'osprey-execution-results-stg') 274 + return self._bucket_name 261 275 262 - @classmethod 263 - def select_one(cls, action_id: int) -> Optional[Dict[str, Any]]: 276 + def select_one(self, action_id: int) -> Optional[Dict[str, Any]]: 264 277 try: 265 278 with metrics.timed('gcs_stored_execution_result.get_one'): 266 - object_name = cls._encode_action_id(action_id) 267 - bucket = cls._get_gcs_client().bucket(cls._get_bucket_name()) 279 + object_name = StoredExecutionResultGCS._encode_action_id(action_id) 280 + bucket = self._get_gcs_client().bucket(self._get_bucket_name()) 268 281 blob = bucket.get_blob(object_name) 269 282 if not blob: 270 283 metrics.increment( ··· 275 288 raw_data = blob.download_as_bytes() 276 289 data = json.loads(raw_data.decode('utf-8')) 277 290 278 - result = cls._execution_result_dict_from_gcs_data(data) 291 + result = StoredExecutionResultGCS._execution_result_dict_from_gcs_data(data) 279 292 return result 280 293 except Exception as e: 281 294 logger.error(f'Failed to retrieve execution result from GCS for action_id {action_id}: {e}') 282 295 return None 283 296 284 - @classmethod 285 - def select_many(cls, action_ids: List[int]) -> List[Dict[str, Any]]: 297 + def select_many(self, action_ids: List[int]) -> List[Dict[str, Any]]: 286 298 results = [ 287 299 result 288 - for result in gevent.pool.Pool(GCS_CONCURRENCY_LIMIT).imap(cls.select_one, action_ids) 300 + for result in gevent.pool.Pool(GCS_CONCURRENCY_LIMIT).imap(self.select_one, action_ids) 289 301 if result is not None 290 302 ] 291 303 292 304 return results 293 305 294 - @classmethod 295 306 def insert( 296 - cls, 307 + self, 297 308 action_id: int, 298 309 extracted_features_json: str, 299 310 error_traces_json: str, ··· 302 313 ) -> None: 303 314 try: 304 315 with metrics.timed('gcs_stored_execution_result.insert'): 305 - object_name = cls._encode_action_id(action_id) 316 + object_name = StoredExecutionResultGCS._encode_action_id(action_id) 306 317 data = { 307 318 'id': action_id, 308 319 'extracted_features': extracted_features_json, ··· 314 325 json_data = json.dumps(data) 315 326 compressed_data = gzip.compress(json_data.encode('utf-8')) 316 327 317 - bucket = cls._get_gcs_client().bucket(cls._get_bucket_name()) 328 + bucket = self._get_gcs_client().bucket(self._get_bucket_name()) 318 329 blob = bucket.blob(object_name) 319 330 320 331 blob.content_encoding = 'gzip' ··· 324 335 except Exception as e: 325 336 logger.error(f'Failed to insert execution result into GCS for action_id {action_id}: {e}') 326 337 327 - @classmethod 328 - def _encode_action_id(cls, action_id_snowflake: int) -> str: 338 + @staticmethod 339 + def _encode_action_id(action_id_snowflake: int) -> str: 329 340 """Constructs a GCS object key for a given snowflake using the same distribution logic as BigTable.""" 330 - timestamp_portion = action_id_snowflake >> 22 331 - # reverse the last 4 characters of the timestamp to create a 332 - # uniformly distributed prefix space. 333 - key_prefix = str(timestamp_portion)[:-5:-1] 341 + key_prefix = Snowflake(action_id_snowflake).to_key_prefix() 342 + return f'{key_prefix}:{action_id_snowflake}.json' 343 + 344 + @staticmethod 345 + def _execution_result_dict_from_gcs_data(data: Dict[str, Any]) -> Dict[str, Any]: 346 + execution_result_dict = { 347 + 'id': data['id'], 348 + 'extracted_features': data['extracted_features'], 349 + 'error_traces': data['error_traces'], 350 + 'timestamp': datetime.fromisoformat(data['timestamp']), 351 + 'action_data': None, 352 + } 353 + 354 + action_data = data.get('action_data') 355 + if action_data: 356 + execution_result_dict['action_data'] = action_data 357 + 358 + return execution_result_dict 359 + 360 + 361 + class StoredExecutionResultMinIO(ExecutionResultStore): 362 + def __init__(self, endpoint: str, access_key: str, secret_key: str, secure: bool, bucket_name: str): 363 + self._minio_client = Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=secure) 364 + self._bucket_name = bucket_name 365 + 366 + def select_one(self, action_id: int) -> Optional[Dict[str, Any]]: 367 + try: 368 + with metrics.timed('minio_stored_execution_result.get_one'): 369 + object_name = StoredExecutionResultMinIO._encode_action_id(action_id) 370 + 371 + try: 372 + response = self._minio_client.get_object(self._bucket_name, object_name) 373 + raw_data = response.read() 374 + response.close() 375 + response.release_conn() 376 + 377 + data = json.loads(raw_data.decode('utf-8')) 378 + result = StoredExecutionResultMinIO._execution_result_dict_from_minio_data(data) 379 + return result 380 + 381 + except S3Error as e: 382 + if e.code == 'NoSuchKey': 383 + metrics.increment( 384 + 'minio_stored_execution_result.select_one.not_found', tags=[f'action_id:{action_id}'] 385 + ) 386 + return None 387 + raise 388 + 389 + except Exception as e: 390 + logger.error(f'Failed to retrieve execution result from MinIO for action_id {action_id}: {e}') 391 + return None 392 + 393 + def select_many(self, action_ids: List[int]) -> List[Dict[str, Any]]: 394 + results = [ 395 + result 396 + for result in gevent.pool.Pool(MINIO_CONCURRENCY_LIMIT).imap(self.select_one, action_ids) 397 + if result is not None 398 + ] 399 + return results 400 + 401 + def insert( 402 + self, 403 + action_id: int, 404 + extracted_features_json: str, 405 + error_traces_json: str, 406 + timestamp: datetime, 407 + action_data_json: str, 408 + ) -> None: 409 + try: 410 + with metrics.timed('minio_stored_execution_result.insert'): 411 + object_name = StoredExecutionResultMinIO._encode_action_id(action_id) 412 + data = { 413 + 'id': action_id, 414 + 'extracted_features': extracted_features_json, 415 + 'error_traces': error_traces_json, 416 + 'timestamp': timestamp.isoformat(), 417 + 'action_data': action_data_json, 418 + } 419 + 420 + json_data = json.dumps(data) 421 + 422 + data_stream = BytesIO(json_data.encode('utf-8')) 423 + 424 + self._minio_client.put_object( 425 + self._bucket_name, 426 + object_name, 427 + data_stream, 428 + length=len(json_data.encode('utf-8')), 429 + content_type='application/json', 430 + ) 431 + 432 + except Exception as e: 433 + logger.error(f'Failed to insert execution result into MinIO for action_id {action_id}: {e}') 434 + 435 + @staticmethod 436 + def _encode_action_id(action_id_snowflake: int) -> str: 437 + """Constructs a MinIO object key for a given snowflake using the same distribution logic as BigTable.""" 438 + key_prefix = Snowflake(action_id_snowflake).to_key_prefix() 334 439 return f'{key_prefix}:{action_id_snowflake}.json' 335 440 336 - @classmethod 337 - def _execution_result_dict_from_gcs_data(cls, data: Dict[str, Any]) -> Dict[str, Any]: 441 + @staticmethod 442 + def _execution_result_dict_from_minio_data(data: Dict[str, Any]) -> Dict[str, Any]: 338 443 execution_result_dict = { 339 444 'id': data['id'], 340 445 'extracted_features': data['extracted_features'], ··· 348 453 execution_result_dict['action_data'] = action_data 349 454 350 455 return execution_result_dict 456 + 457 + 458 + class ExecutionResultStorageService: 459 + """Service class that provides execution result operations with a configured backend.""" 460 + 461 + def __init__(self, storage_backend: ExecutionResultStore): 462 + self._storage_backend = storage_backend 463 + 464 + def persist_from_execution_result(self, execution_result: ExecutionResult) -> None: 465 + """Persist execution result using the configured storage backend.""" 466 + StoredExecutionResult.persist_from_execution_result(execution_result, self._storage_backend) 467 + 468 + def get_one_with_action_data( 469 + self, event_record_id: int, data_censor_abilities: Sequence[Optional[DataCensorAbility[Any, Any]]] = () 470 + ) -> Optional[StoredExecutionResult]: 471 + """Get execution result from the configured storage backend.""" 472 + return StoredExecutionResult.get_one_with_action_data( 473 + event_record_id, self._storage_backend, data_censor_abilities 474 + ) 475 + 476 + def get_many( 477 + self, action_ids: List[int], data_censor_abilities: Sequence[Optional[DataCensorAbility[Any, Any]]] = () 478 + ) -> List[StoredExecutionResult]: 479 + """Get execution results from the configured storage backend.""" 480 + return StoredExecutionResult.get_many(action_ids, self._storage_backend, data_censor_abilities) 481 + 482 + 483 + def bootstrap_execution_result_storage_service() -> ExecutionResultStorageService: 484 + """Create an ExecutionResultStorageService with the configured storage backend.""" 485 + from osprey.worker.adaptor.plugin_manager import bootstrap_execution_result_store 486 + from osprey.worker.lib.singletons import CONFIG 487 + 488 + config = CONFIG.instance() 489 + storage_backend = bootstrap_execution_result_store(config) 490 + return ExecutionResultStorageService(storage_backend)
+19
osprey_worker/src/osprey/worker/sinks/sink/stored_execution_result_output_sink.py
··· 1 + from osprey.engine.executor.execution_context import ExecutionResult 2 + from osprey.worker.lib.storage.stored_execution_result import bootstrap_execution_result_storage_service 3 + from osprey.worker.sinks.sink.output_sink import BaseOutputSink 4 + 5 + 6 + class StoredExecutionResultOutputSink(BaseOutputSink): 7 + """An output sink that persists the execution result to an EventRecord.""" 8 + 9 + def __init__(self): 10 + self._service = bootstrap_execution_result_storage_service() 11 + 12 + def will_do_work(self, result: ExecutionResult) -> bool: 13 + return True 14 + 15 + def push(self, result: ExecutionResult) -> None: 16 + self._service.persist_from_execution_result(result) 17 + 18 + def stop(self) -> None: 19 + pass
+6 -2
osprey_worker/src/osprey/worker/ui_api/osprey/cli.py
··· 10 10 import click # noqa: E402 11 11 import simplejson as json 12 12 from osprey.worker.lib.osprey_shared.logging import get_logger 13 - from osprey.worker.lib.storage.stored_execution_result import StoredExecutionResult 13 + from osprey.worker.lib.storage.stored_execution_result import ( 14 + StoredExecutionResult, 15 + bootstrap_execution_result_storage_service, 16 + ) 14 17 from osprey.worker.lib.utils.json import CustomJSONEncoder 15 18 from osprey.worker.ui_api.osprey.app import create_app 16 19 from osprey.worker.ui_api.osprey.lib.druid import PaginatedScanDruidQuery, PaginatedScanResult ··· 79 82 d = event.dict() 80 83 return {'id': d['id'], 'timestamp': d['timestamp'], 'action_data': d['action_data']} 81 84 85 + storage_service = bootstrap_execution_result_storage_service() 82 86 druid_result = query_druid() 83 87 while druid_result.action_ids: 84 - events = StoredExecutionResult.get_many(action_ids=druid_result.action_ids) 88 + events = storage_service.get_many(action_ids=druid_result.action_ids) 85 89 86 90 for event in events: 87 91 output.writelines([json.dumps(row(event), cls=CustomJSONEncoder), '\n'])
+9 -5
osprey_worker/src/osprey/worker/ui_api/osprey/views/events.py
··· 5 5 from typing import Any, Dict, List, Optional, Set 6 6 7 7 from flask import Blueprint, Response, abort, jsonify 8 - from osprey.worker.lib.storage.stored_execution_result import StoredExecutionResult 8 + from osprey.worker.lib.storage.stored_execution_result import ( 9 + bootstrap_execution_result_storage_service, 10 + ) 9 11 from osprey.worker.ui_api.osprey.lib.abilities import ( 10 12 CanBulkLabel, 11 13 CanBulkLabelWithNoLimit, ··· 128 130 129 131 action_data_censor_ability = get_current_user().get_ability(CanViewActionData) 130 132 feature_data_censor_ability = get_current_user().get_ability(CanViewFeatureData) 131 - events = StoredExecutionResult.get_many( 133 + storage_service = bootstrap_execution_result_storage_service() 134 + events = storage_service.get_many( 135 + action_ids=paginated_scan_results.action_ids, 132 136 data_censor_abilities=[action_data_censor_ability, feature_data_censor_ability], 133 - action_ids=paginated_scan_results.action_ids, 134 137 ) 135 138 136 139 return ScanQueryResult( ··· 226 229 def get_event_data(event_id: int) -> Any: 227 230 action_data_censor_ability = get_current_user().get_ability(CanViewActionData) 228 231 feature_data_censor_ability = get_current_user().get_ability(CanViewFeatureData) 229 - execution_result = StoredExecutionResult.get_one_with_action_data( 230 - event_id, data_censor_abilities=[action_data_censor_ability, feature_data_censor_ability] 232 + storage_service = bootstrap_execution_result_storage_service() 233 + execution_result = storage_service.get_one_with_action_data( 234 + event_id, [action_data_censor_ability, feature_data_censor_ability] 231 235 ) 232 236 if not execution_result: 233 237 return abort(Response(response='Unknown action id', status=NOT_FOUND, mimetype='application/json'))
+2 -1
pyproject.toml
··· 78 78 "mdit-py-plugins==0.4.2", 79 79 "mdurl==0.1.2", 80 80 "memray==1.17.2", 81 + "minio>=7.2.16", 81 82 "mmh3==3.0.0", 82 83 "msgpack==1.0.8", 83 84 "mypy==1.13.0", ··· 288 289 implicit_reexport = true 289 290 290 291 [[tool.mypy.overrides]] 291 - module = "jsonpath_rw" 292 + module = ["jsonpath_rw", "minio.*"] 292 293 ignore_missing_imports = true 293 294 294 295 # third party packages we don't care about
+138 -6
uv.lock
··· 2 2 revision = 3 3 3 requires-python = ">=3.11" 4 4 resolution-markers = [ 5 - "python_full_version >= '3.13' and platform_machine == 'x86_64'", 5 + "python_full_version >= '3.14' and platform_machine == 'x86_64'", 6 + "python_full_version == '3.13.*' and platform_machine == 'x86_64'", 6 7 "python_full_version == '3.12.*' and platform_machine == 'x86_64'", 7 8 "python_full_version < '3.12' and platform_machine == 'x86_64'", 8 - "python_full_version >= '3.13' and platform_machine == 'aarch64'", 9 + "python_full_version >= '3.14' and platform_machine == 'aarch64'", 10 + "python_full_version == '3.13.*' and platform_machine == 'aarch64'", 9 11 "python_full_version == '3.12.*' and platform_machine == 'aarch64'", 10 12 "python_full_version < '3.12' and platform_machine == 'aarch64'", 11 - "python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 13 + "python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 14 + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 12 15 "python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 13 16 "python_full_version < '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 14 17 ] ··· 97 100 { name = "mdit-py-plugins", specifier = "==0.4.2" }, 98 101 { name = "mdurl", specifier = "==0.1.2" }, 99 102 { name = "memray", specifier = "==1.17.2" }, 103 + { name = "minio", specifier = ">=7.2.16" }, 100 104 { name = "mmh3", specifier = "==3.0.0" }, 101 105 { name = "msgpack", specifier = "==1.0.8" }, 102 106 { name = "mypy", specifier = "==1.13.0" }, ··· 211 215 ] 212 216 213 217 [[package]] 218 + name = "argon2-cffi" 219 + version = "25.1.0" 220 + source = { registry = "https://pypi.org/simple" } 221 + dependencies = [ 222 + { name = "argon2-cffi-bindings", version = "21.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" }, 223 + { name = "argon2-cffi-bindings", version = "25.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" }, 224 + ] 225 + sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } 226 + wheels = [ 227 + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, 228 + ] 229 + 230 + [[package]] 231 + name = "argon2-cffi-bindings" 232 + version = "21.2.0" 233 + source = { registry = "https://pypi.org/simple" } 234 + resolution-markers = [ 235 + "python_full_version >= '3.14' and platform_machine == 'x86_64'", 236 + "python_full_version >= '3.14' and platform_machine == 'aarch64'", 237 + "python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 238 + ] 239 + dependencies = [ 240 + { name = "cffi", marker = "python_full_version >= '3.14'" }, 241 + ] 242 + sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } 243 + wheels = [ 244 + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, 245 + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, 246 + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, 247 + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, 248 + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, 249 + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, 250 + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, 251 + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, 252 + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, 253 + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, 254 + ] 255 + 256 + [[package]] 257 + name = "argon2-cffi-bindings" 258 + version = "25.1.0" 259 + source = { registry = "https://pypi.org/simple" } 260 + resolution-markers = [ 261 + "python_full_version == '3.13.*' and platform_machine == 'x86_64'", 262 + "python_full_version == '3.12.*' and platform_machine == 'x86_64'", 263 + "python_full_version < '3.12' and platform_machine == 'x86_64'", 264 + "python_full_version == '3.13.*' and platform_machine == 'aarch64'", 265 + "python_full_version == '3.12.*' and platform_machine == 'aarch64'", 266 + "python_full_version < '3.12' and platform_machine == 'aarch64'", 267 + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 268 + "python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 269 + "python_full_version < '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 270 + ] 271 + dependencies = [ 272 + { name = "cffi", marker = "python_full_version < '3.14'" }, 273 + ] 274 + sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } 275 + wheels = [ 276 + { url = "https://files.pythonhosted.org/packages/60/97/3c0a35f46e52108d4707c44b95cfe2afcafc50800b5450c197454569b776/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3d3f05610594151994ca9ccb3c771115bdb4daef161976a266f0dd8aa9996b8f", size = 54393, upload-time = "2025-07-30T10:01:40.97Z" }, 277 + { url = "https://files.pythonhosted.org/packages/9d/f4/98bbd6ee89febd4f212696f13c03ca302b8552e7dbf9c8efa11ea4a388c3/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8b8efee945193e667a396cbc7b4fb7d357297d6234d30a489905d96caabde56b", size = 29328, upload-time = "2025-07-30T10:01:41.916Z" }, 278 + { url = "https://files.pythonhosted.org/packages/43/24/90a01c0ef12ac91a6be05969f29944643bc1e5e461155ae6559befa8f00b/argon2_cffi_bindings-25.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c6702abc36bf3ccba3f802b799505def420a1b7039862014a65db3205967f5a", size = 31269, upload-time = "2025-07-30T10:01:42.716Z" }, 279 + { url = "https://files.pythonhosted.org/packages/d4/d3/942aa10782b2697eee7af5e12eeff5ebb325ccfb86dd8abda54174e377e4/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c70058c6ab1e352304ac7e3b52554daadacd8d453c1752e547c76e9c99ac44", size = 86558, upload-time = "2025-07-30T10:01:43.943Z" }, 280 + { url = "https://files.pythonhosted.org/packages/0d/82/b484f702fec5536e71836fc2dbc8c5267b3f6e78d2d539b4eaa6f0db8bf8/argon2_cffi_bindings-25.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb", size = 92364, upload-time = "2025-07-30T10:01:44.887Z" }, 281 + { url = "https://files.pythonhosted.org/packages/c9/c1/a606ff83b3f1735f3759ad0f2cd9e038a0ad11a3de3b6c673aa41c24bb7b/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4f9665de60b1b0e99bcd6be4f17d90339698ce954cfd8d9cf4f91c995165a92", size = 85637, upload-time = "2025-07-30T10:01:46.225Z" }, 282 + { url = "https://files.pythonhosted.org/packages/44/b4/678503f12aceb0262f84fa201f6027ed77d71c5019ae03b399b97caa2f19/argon2_cffi_bindings-25.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ba92837e4a9aa6a508c8d2d7883ed5a8f6c308c89a4790e1e447a220deb79a85", size = 91934, upload-time = "2025-07-30T10:01:47.203Z" }, 283 + { url = "https://files.pythonhosted.org/packages/f0/c7/f36bd08ef9bd9f0a9cff9428406651f5937ce27b6c5b07b92d41f91ae541/argon2_cffi_bindings-25.1.0-cp314-cp314t-win32.whl", hash = "sha256:84a461d4d84ae1295871329b346a97f68eade8c53b6ed9a7ca2d7467f3c8ff6f", size = 28158, upload-time = "2025-07-30T10:01:48.341Z" }, 284 + { url = "https://files.pythonhosted.org/packages/b3/80/0106a7448abb24a2c467bf7d527fe5413b7fdfa4ad6d6a96a43a62ef3988/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b55aec3565b65f56455eebc9b9f34130440404f27fe21c3b375bf1ea4d8fbae6", size = 32597, upload-time = "2025-07-30T10:01:49.112Z" }, 285 + { url = "https://files.pythonhosted.org/packages/05/b8/d663c9caea07e9180b2cb662772865230715cbd573ba3b5e81793d580316/argon2_cffi_bindings-25.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:87c33a52407e4c41f3b70a9c2d3f6056d88b10dad7695be708c5021673f55623", size = 28231, upload-time = "2025-07-30T10:01:49.92Z" }, 286 + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, 287 + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, 288 + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, 289 + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, 290 + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, 291 + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, 292 + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, 293 + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, 294 + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, 295 + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, 296 + ] 297 + 298 + [[package]] 214 299 name = "asttokens" 215 300 version = "3.0.0" 216 301 source = { registry = "https://pypi.org/simple" } ··· 264 349 ] 265 350 sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } 266 351 wheels = [ 352 + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, 353 + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, 354 + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, 355 + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, 356 + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, 357 + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, 358 + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, 359 + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, 360 + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, 361 + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, 267 362 { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, 268 363 { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, 364 + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, 365 + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, 366 + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, 367 + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, 368 + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, 369 + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, 370 + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, 371 + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, 372 + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, 269 373 { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, 270 374 { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, 375 + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, 376 + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, 377 + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, 378 + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, 379 + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, 380 + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, 381 + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, 382 + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, 383 + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, 271 384 { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, 272 385 { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, 273 386 ] ··· 874 987 version = "1.49.1" 875 988 source = { registry = "https://pypi.org/simple" } 876 989 resolution-markers = [ 877 - "python_full_version >= '3.13' and platform_machine == 'x86_64'", 990 + "python_full_version >= '3.14' and platform_machine == 'x86_64'", 991 + "python_full_version == '3.13.*' and platform_machine == 'x86_64'", 878 992 "python_full_version == '3.12.*' and platform_machine == 'x86_64'", 879 993 "python_full_version < '3.12' and platform_machine == 'x86_64'", 880 994 ] ··· 895 1009 version = "1.53.2" 896 1010 source = { registry = "https://pypi.org/simple" } 897 1011 resolution-markers = [ 898 - "python_full_version >= '3.13' and platform_machine == 'aarch64'", 1012 + "python_full_version >= '3.14' and platform_machine == 'aarch64'", 1013 + "python_full_version == '3.13.*' and platform_machine == 'aarch64'", 899 1014 "python_full_version == '3.12.*' and platform_machine == 'aarch64'", 900 1015 "python_full_version < '3.12' and platform_machine == 'aarch64'", 901 - "python_full_version >= '3.13' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 1016 + "python_full_version >= '3.14' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 1017 + "python_full_version == '3.13.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 902 1018 "python_full_version == '3.12.*' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 903 1019 "python_full_version < '3.12' and platform_machine != 'aarch64' and platform_machine != 'x86_64'", 904 1020 ] ··· 1338 1454 { url = "https://files.pythonhosted.org/packages/a6/74/9f5fb772cdb7b203ece295dc429507a28e0ecda9fef6c3dc2fdf02c7f1cd/memray-1.17.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48fe99afa391bdc67f2415b968b31c1ace5d045a0af91312a8e24cfa4ff577c6", size = 7956125, upload-time = "2025-05-09T02:02:59.351Z" }, 1339 1455 { url = "https://files.pythonhosted.org/packages/b9/69/06c3b2776c90257354176d6a3caf3c4a74f2d2920c96c456b149109a6456/memray-1.17.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e871af5a88006f9c0d16cb234e9d18244ffeb28f5c8aa17bd6babc8418fdda1d", size = 8235642, upload-time = "2025-05-09T02:03:00.913Z" }, 1340 1456 { url = "https://files.pythonhosted.org/packages/a6/d4/7adf788b3bb1652131bdd086434ec4e337c46fc8df9809a8d0887aebd400/memray-1.17.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:63f0a8d5d9d179648e7e336f8a185e8f4a1b70f28ec62476e193c024aa992313", size = 8183003, upload-time = "2025-05-09T02:03:02.792Z" }, 1457 + ] 1458 + 1459 + [[package]] 1460 + name = "minio" 1461 + version = "7.2.16" 1462 + source = { registry = "https://pypi.org/simple" } 1463 + dependencies = [ 1464 + { name = "argon2-cffi" }, 1465 + { name = "certifi" }, 1466 + { name = "pycryptodome" }, 1467 + { name = "typing-extensions" }, 1468 + { name = "urllib3" }, 1469 + ] 1470 + sdist = { url = "https://files.pythonhosted.org/packages/f4/a0/33ea2e18d5169817950edc13eba58cd781cedefe9f6696cae26aa2d75882/minio-7.2.16.tar.gz", hash = "sha256:81e365c8494d591d8204a63ee7596bfdf8a7d06ad1b1507d6b9c1664a95f299a", size = 139149, upload-time = "2025-07-21T20:11:15.911Z" } 1471 + wheels = [ 1472 + { url = "https://files.pythonhosted.org/packages/89/a3/00260f8df72b51afa1f182dd609533c77fa2407918c4c2813d87b4a56725/minio-7.2.16-py3-none-any.whl", hash = "sha256:9288ab988ca57c181eb59a4c96187b293131418e28c164392186c2b89026b223", size = 95750, upload-time = "2025-07-21T20:11:14.139Z" }, 1341 1473 ] 1342 1474 1343 1475 [[package]]