diff --git a/stochss_compute/client/server.py b/stochss_compute/client/server.py index a381e44c..ae7b6bb5 100644 --- a/stochss_compute/client/server.py +++ b/stochss_compute/client/server.py @@ -21,7 +21,7 @@ from abc import ABC, abstractmethod import requests from stochss_compute.client.endpoint import Endpoint -from stochss_compute.core.messages import Request, Response +from stochss_compute.core.messages.base import Request class Server(ABC): ''' diff --git a/stochss_compute/cloud/ec2.py b/stochss_compute/cloud/ec2.py index 08d09b2f..5d458513 100644 --- a/stochss_compute/cloud/ec2.py +++ b/stochss_compute/cloud/ec2.py @@ -23,7 +23,7 @@ from secrets import token_hex from stochss_compute.client.server import Server from stochss_compute.cloud.ec2_config import EC2LocalConfig, EC2RemoteConfig -from stochss_compute.core.messages import SourceIpRequest, SourceIpResponse +from stochss_compute.core.messages.source_ip import SourceIpRequest, SourceIpResponse from stochss_compute.cloud.exceptions import EC2ImportException, ResourceException, EC2Exception from stochss_compute.client.endpoint import Endpoint try: diff --git a/stochss_compute/core/exceptions.py b/stochss_compute/core/exceptions.py new file mode 100644 index 00000000..40a88219 --- /dev/null +++ b/stochss_compute/core/exceptions.py @@ -0,0 +1,24 @@ +''' +stochss_compute.core.exceptions +''' +# StochSS-Compute is a tool for running and caching GillesPy2 simulations remotely. +# Copyright (C) 2019-2023 GillesPy2 and StochSS developers. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +class PRNGCollision(Exception): + ''' + ...Lucky??? + ''' + \ No newline at end of file diff --git a/stochss_compute/core/log_config.py b/stochss_compute/core/log_config.py new file mode 100644 index 00000000..3aeb7afe --- /dev/null +++ b/stochss_compute/core/log_config.py @@ -0,0 +1,36 @@ +''' +stochss_compute.core.log_config + +Global Logging Configuration +''' + +from logging import getLogger + +def init_logging(name): + ''' + Call after import to initialize logs in a module file. + To follow convention, use predefined __name__. + + Like so: + + from stochss_compute.core.log_config import init_logs + log = init_logs(__name__) + + :param name: Name for the logger. Use the dot-separated module path string. + :type name: str + + :returns: A module specific logger. + :rtype: logging.Logger + ''' + logger = getLogger(name) + return logger + +def set_global_log_level(level): + ''' + Sets the root logger log level. + + :param level: NOTSET:0, DEBUG:10, INFO:20, WARNING:30, ERROR:40, CRITICAL:50, etc. + :type level: int | logging._Level + ''' + getLogger(__name__.split('.', maxsplit=1)[0]).setLevel(level) + \ No newline at end of file diff --git a/stochss_compute/core/messages.py b/stochss_compute/core/messages.py deleted file mode 100644 index 28d833e1..00000000 --- a/stochss_compute/core/messages.py +++ /dev/null @@ -1,388 +0,0 @@ -''' -stochss_compute.core.messages -''' -# StochSS-Compute is a tool for running and caching GillesPy2 simulations remotely. -# Copyright (C) 2019-2023 GillesPy2 and StochSS developers. - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from abc import ABC, abstractmethod -from enum import Enum -from hashlib import md5 -from gillespy2 import Model, Results -from tornado.escape import json_encode, json_decode - - -class SimStatus(Enum): - ''' - Status describing a remote simulation. - ''' - PENDING = 'The simulation is pending.' - RUNNING = 'The simulation is still running.' - READY = 'Simulation is done and results exist in the cache.' - ERROR = 'The Simulation has encountered an error.' - DOES_NOT_EXIST = 'There is no evidence of this simulation either running or on disk.' - - @staticmethod - def from_str(name): - ''' - Convert str to Enum. - ''' - if name == 'PENDING': - return SimStatus.PENDING - if name == 'RUNNING': - return SimStatus.RUNNING - if name == 'READY': - return SimStatus.READY - if name == 'ERROR': - return SimStatus.ERROR - if name == 'DOES_NOT_EXIST': - return SimStatus.DOES_NOT_EXIST - -class Request(ABC): - ''' - Base class. - ''' - @abstractmethod - def encode(self): - ''' - Encode self for http. - ''' - @staticmethod - @abstractmethod - def parse(raw_request): - ''' - Parse http for python. - ''' - -class Response(ABC): - ''' - Base class. - ''' - @abstractmethod - def encode(self): - ''' - Encode self for http. - ''' - @staticmethod - @abstractmethod - def parse(raw_response): - ''' - Parse http for python. - ''' - - -class SimulationRunRequest(Request): - ''' - A simulation request. - - :param model: A model to run. - :type model: gillespy2.Model - - :param kwargs: kwargs for the model.run() call. - :type kwargs: dict[str, Any] - ''' - def __init__(self, model, **kwargs): - self.model = model - self.kwargs = kwargs - - def encode(self): - ''' - JSON-encode model and then encode self to dict. - ''' - return {'model': self.model.to_json(), - 'kwargs': self.kwargs} - - @staticmethod - def parse(raw_request): - ''' - Parse HTTP request. - - :param raw_request: The request. - :type raw_request: dict[str, str] - - :returns: The decoded object. - :rtype: SimulationRunRequest - ''' - request_dict = json_decode(raw_request) - model = Model.from_json(request_dict['model']) - kwargs = request_dict['kwargs'] - return SimulationRunRequest(model, **kwargs) - - def hash(self): - ''' - Generate a unique hash of this simulation request. - Does not include number_of_trajectories in this calculation. - - :returns: md5 hex digest. - :rtype: str - ''' - anon_model_string = self.model.to_anon().to_json(encode_private=False) - popped_kwargs = {kw:self.kwargs[kw] for kw in self.kwargs if kw!='number_of_trajectories'} - kwargs_string = json_encode(popped_kwargs) - request_string = f'{anon_model_string}{kwargs_string}' - _hash = md5(str.encode(request_string)).hexdigest() - return _hash - -class SimulationRunResponse(Response): - ''' - A response from the server regarding a SimulationRunRequest. - - :param status: The status of the simulation. - :type status: SimStatus - - :param error_message: Possible error message. - :type error_message: str | None - - :param results_id: Hash of the simulation request. Identifies the results. - :type results_id: str | None - - :param results: JSON-Encoded gillespy2.Results - :type results: str | None - ''' - def __init__(self, status, error_message = None, results_id = None, results = None, task_id = None): - self.status = status - self.error_message = error_message - self.results_id = results_id - self.results = results - self.task_id = task_id - - def encode(self): - ''' - Encode self to dict. - ''' - return {'status': self.status.name, - 'error_message': self.error_message or '', - 'results_id': self.results_id or '', - 'results': self.results or '', - 'task_id': self.task_id or '',} - - @staticmethod - def parse(raw_response): - ''' - Parse HTTP response. - - :param raw_response: The response. - :type raw_response: dict[str, str] - - :returns: The decoded object. - :rtype: SimulationRunResponse - ''' - response_dict = json_decode(raw_response) - status = SimStatus.from_str(response_dict['status']) - results_id = response_dict['results_id'] - error_message = response_dict['error_message'] - task_id = response_dict['task_id'] - if response_dict['results'] != '': - results = Results.from_json(response_dict['results']) - else: - results = None - return SimulationRunResponse(status, error_message, results_id, results, task_id) - -class StatusRequest(Request): - ''' - A request for simulation status. - - :param results_id: Hash of the SimulationRunRequest - :type results_id: str - ''' - def __init__(self, results_id): - self.results_id = results_id - def encode(self): - ''' - :returns: self.__dict__ - :rtype: dict - ''' - return self.__dict__ - - @staticmethod - def parse(raw_request): - ''' - Parse HTTP request. - - :param raw_request: The request. - :type raw_request: dict[str, str] - - :returns: The decoded object. - :rtype: StatusRequest - ''' - request_dict = json_decode(raw_request) - return StatusRequest(request_dict['results_id']) - -class StatusResponse(Response): - ''' - A response from the server about simulation status. - - :param status: Status of the simulation - :type status: SimStatus - - :param message: Possible error message or otherwise - :type message: str - ''' - def __init__(self, status, message = None): - self.status = status - self.message = message - - def encode(self): - ''' - Encodes self. - :returns: self as dict - :rtype: dict[str, str] - ''' - return {'status': self.status.name, - 'message': self.message or ''} - - @staticmethod - def parse(raw_response): - ''' - Parse HTTP response. - - :param raw_response: The response. - :type raw_response: dict[str, str] - - :returns: The decoded object. - :rtype: StatusResponse - ''' - response_dict = json_decode(raw_response) - status = SimStatus.from_str(response_dict['status']) - message = response_dict['message'] - if not message: - return StatusResponse(status) - else: - return StatusResponse(status, message) - -class ResultsRequest(Request): - ''' - Request results from the server. - - :param results_id: Hash of the SimulationRunRequest - :type results_id: str - ''' - def __init__(self, results_id): - self.results_id = results_id - def encode(self): - ''' - :returns: self.__dict__ - :rtype: dict - ''' - return self.__dict__ - @staticmethod - def parse(raw_request): - ''' - Parse HTTP request. - - :param raw_request: The request. - :type raw_request: dict[str, str] - - :returns: The decoded object. - :rtype: ResultsRequest - ''' - request_dict = json_decode(raw_request) - return ResultsRequest(request_dict['results_id']) - -class ResultsResponse(Response): - ''' - A response from the server about the Results. - - :param results: The requested Results from the cache. (JSON) - :type results: str - - ''' - def __init__(self, results = None): - self.results = results - - def encode(self): - ''' - :returns: self.__dict__ - :rtype: dict - ''' - return {'results': self.results or ''} - - @staticmethod - def parse(raw_response): - ''' - Parse HTTP response. - - :param raw_response: The response. - :type raw_response: dict[str, str] - - :returns: The decoded object. - :rtype: ResultsResponse - ''' - response_dict = json_decode(raw_response) - if response_dict['results'] != '': - results = Results.from_json(response_dict['results']) - else: - results = None - return ResultsResponse(results) - -class SourceIpRequest(Request): - ''' - Restrict server access. - - :param cloud_key: Random key generated locally during launch. - :type cloud_key: str - ''' - def __init__(self, cloud_key): - self.cloud_key = cloud_key - def encode(self): - ''' - :returns: self.__dict__ - :rtype: dict - ''' - return self.__dict__ - @staticmethod - def parse(raw_request): - ''' - Parse HTTP request. - - :param raw_request: The request. - :type raw_request: dict[str, str] - - :returns: The decoded object. - :rtype: SourceIpRequest - ''' - request_dict = json_decode(raw_request) - return SourceIpRequest(request_dict['cloud_key']) - -class SourceIpResponse(Response): - ''' - Response from server containing IP address of the source. - - :param source_ip: IP address of the client. - :type source_ip: str - ''' - def __init__(self, source_ip): - self.source_ip = source_ip - - def encode(self): - ''' - :returns: self.__dict__ - :rtype: dict - ''' - return self.__dict__ - - @staticmethod - def parse(raw_response): - ''' - Parses a http response and returns a python object. - - :param raw_response: A raw http SourceIpResponse from the server. - :type raw_response: str - - :returns: The decoded object. - :rtype: SourceIpResponse - ''' - response_dict = json_decode(raw_response) - return SourceIpResponse(response_dict['source_ip']) diff --git a/stochss_compute/core/messages/__init__.py b/stochss_compute/core/messages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/stochss_compute/core/messages/base.py b/stochss_compute/core/messages/base.py new file mode 100644 index 00000000..d7779114 --- /dev/null +++ b/stochss_compute/core/messages/base.py @@ -0,0 +1,52 @@ +''' +stochss_compute.core.messages +''' +# StochSS-Compute is a tool for running and caching GillesPy2 simulations remotely. +# Copyright (C) 2019-2023 GillesPy2 and StochSS developers. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from abc import ABC, abstractmethod + +class Request(ABC): + ''' + Base class. + ''' + @abstractmethod + def encode(self): + ''' + Encode self for http. + ''' + @staticmethod + @abstractmethod + def parse(raw_request): + ''' + Parse http for python. + ''' + +class Response(ABC): + ''' + Base class. + ''' + @abstractmethod + def encode(self): + ''' + Encode self for http. + ''' + @staticmethod + @abstractmethod + def parse(raw_response): + ''' + Parse http for python. + ''' diff --git a/stochss_compute/core/messages/results.py b/stochss_compute/core/messages/results.py new file mode 100644 index 00000000..30c6a5d5 --- /dev/null +++ b/stochss_compute/core/messages/results.py @@ -0,0 +1,72 @@ +''' +stochss_compute.core.messages.results +''' +from tornado.escape import json_decode +from gillespy2 import Results + +from stochss_compute.core.messages.base import Request, Response + +class ResultsRequest(Request): + ''' + Request results from the server. + + :param results_id: Hash of the SimulationRunRequest + :type results_id: str + ''' + def __init__(self, results_id): + self.results_id = results_id + def encode(self): + ''' + :returns: self.__dict__ + :rtype: dict + ''' + return self.__dict__ + @staticmethod + def parse(raw_request): + ''' + Parse HTTP request. + + :param raw_request: The request. + :type raw_request: dict[str, str] + + :returns: The decoded object. + :rtype: ResultsRequest + ''' + request_dict = json_decode(raw_request) + return ResultsRequest(request_dict['results_id']) + +class ResultsResponse(Response): + ''' + A response from the server about the Results. + + :param results: The requested Results from the cache. (JSON) + :type results: str + + ''' + def __init__(self, results = None): + self.results = results + + def encode(self): + ''' + :returns: self.__dict__ + :rtype: dict + ''' + return {'results': self.results or ''} + + @staticmethod + def parse(raw_response): + ''' + Parse HTTP response. + + :param raw_response: The response. + :type raw_response: dict[str, str] + + :returns: The decoded object. + :rtype: ResultsResponse + ''' + response_dict = json_decode(raw_response) + if response_dict['results'] != '': + results = Results.from_json(response_dict['results']) + else: + results = None + return ResultsResponse(results) \ No newline at end of file diff --git a/stochss_compute/core/messages/simulation_run.py b/stochss_compute/core/messages/simulation_run.py new file mode 100644 index 00000000..707dd226 --- /dev/null +++ b/stochss_compute/core/messages/simulation_run.py @@ -0,0 +1,119 @@ +''' +stochss_compute.core.messages.simulation_run +''' +from hashlib import md5 +from tornado.escape import json_decode, json_encode +from gillespy2 import Model, Results +from stochss_compute.core.messages.base import Request, Response +from stochss_compute.core.messages.status import SimStatus + +class SimulationRunRequest(Request): + ''' + A simulation request. + + :param model: A model to run. + :type model: gillespy2.Model + + :param kwargs: kwargs for the model.run() call. + :type kwargs: dict[str, Any] + ''' + def __init__(self, model,**kwargs): + self.model = model + self.kwargs = kwargs + + def encode(self): + ''' + JSON-encode model and then encode self to dict. + ''' + return {'model': self.model.to_json(), + 'kwargs': self.kwargs, + } + + @staticmethod + def parse(raw_request): + ''' + Parse HTTP request. + + :param raw_request: The request. + :type raw_request: dict[str, str] + + :returns: The decoded object. + :rtype: SimulationRunRequest + ''' + request_dict = json_decode(raw_request) + model = Model.from_json(request_dict['model']) + kwargs = request_dict['kwargs'] + return SimulationRunRequest(model, **kwargs) + + def hash(self): + ''' + Generate a unique hash of this simulation request. + Does not include number_of_trajectories in this calculation. + + :returns: md5 hex digest. + :rtype: str + ''' + anon_model_string = self.model.to_anon().to_json(encode_private=False) + popped_kwargs = {kw:self.kwargs[kw] for kw in self.kwargs if kw!='number_of_trajectories'} + # Explanation of line above: + # Take 'self.kwargs' (a dict), and add all entries to a new dictionary, + # EXCEPT the 'number_of_trajectories' key/value pair. + kwargs_string = json_encode(popped_kwargs) + request_string = f'{anon_model_string}{kwargs_string}' + _hash = md5(str.encode(request_string)).hexdigest() + return _hash + +class SimulationRunResponse(Response): + ''' + A response from the server regarding a SimulationRunRequest. + + :param status: The status of the simulation. + :type status: SimStatus + + :param error_message: Possible error message. + :type error_message: str | None + + :param results_id: Hash of the simulation request. Identifies the results. + :type results_id: str | None + + :param results: JSON-Encoded gillespy2.Results + :type results: str | None + ''' + def __init__(self, status, error_message = None, results_id = None, results = None, task_id = None): + self.status = status + self.error_message = error_message + self.results_id = results_id + self.results = results + self.task_id = task_id + + def encode(self): + ''' + Encode self to dict. + ''' + return {'status': self.status.name, + 'error_message': self.error_message or '', + 'results_id': self.results_id or '', + 'results': self.results or '', + 'task_id': self.task_id or '',} + + @staticmethod + def parse(raw_response): + ''' + Parse HTTP response. + + :param raw_response: The response. + :type raw_response: dict[str, str] + + :returns: The decoded object. + :rtype: SimulationRunResponse + ''' + response_dict = json_decode(raw_response) + status = SimStatus.from_str(response_dict['status']) + results_id = response_dict['results_id'] + error_message = response_dict['error_message'] + task_id = response_dict['task_id'] + if response_dict['results'] != '': + results = Results.from_json(response_dict['results']) + else: + results = None + return SimulationRunResponse(status, error_message, results_id, results, task_id) diff --git a/stochss_compute/core/messages/simulation_run_unique.py b/stochss_compute/core/messages/simulation_run_unique.py new file mode 100644 index 00000000..9429c6a6 --- /dev/null +++ b/stochss_compute/core/messages/simulation_run_unique.py @@ -0,0 +1,89 @@ +''' +stochss_compute.core.messages.simulation_run_unique +''' +from secrets import token_hex +from tornado.escape import json_decode +from gillespy2 import Model +from stochss_compute.core.messages.base import Request, Response +from stochss_compute.core.messages.status import SimStatus + +class SimulationRunUniqueRequest(Request): + ''' + A one-off simulation request identifiable by a unique key. + + :param model: A model to run. + :type model: gillespy2.Model + + :param kwargs: kwargs for the model.run() call. + :type kwargs: dict[str, Any] + ''' + def __init__(self, model, **kwargs): + self.model = model + self.kwargs = kwargs + self.unique_key = token_hex(7) + + def encode(self): + ''' + JSON-encode model and then encode self to dict. + ''' + return {'model': self.model.to_json(), + 'kwargs': self.kwargs, + 'unique_key': self.unique_key, + } + + @staticmethod + def parse(raw_request): + ''' + Parse raw HTTP request. Done server-side. + + :param raw_request: The request. + :type raw_request: dict[str, str] + + :returns: The decoded object. + :rtype: SimulationRunUniqueRequest + ''' + request_dict = json_decode(raw_request) + model = Model.from_json(request_dict['model']) + kwargs = request_dict['kwargs'] + _ = SimulationRunUniqueRequest(model, **kwargs) + _.unique_key = request_dict['unique_key'] # apply correct token (from raw request) after object construction. + return _ + +class SimulationRunUniqueResponse(Response): + ''' + A response from the server regarding a SimulationRunUniqueRequest. + + :param status: The status of the simulation. + :type status: SimStatus + + :param error_message: Possible error message. + :type error_message: str | None + + ''' + def __init__(self, status, error_message = None): + self.status = status + self.error_message = error_message + + def encode(self): + ''' + Encode self to dict. + ''' + return {'status': self.status.name, + 'error_message': self.error_message or '', + } + + @staticmethod + def parse(raw_response): + ''' + Parse HTTP response. + + :param raw_response: The response. + :type raw_response: dict[str, str] + + :returns: The decoded object. + :rtype: SimulationRunResponse + ''' + response_dict = json_decode(raw_response) + status = SimStatus.from_str(response_dict['status']) + error_message = response_dict['error_message'] + return SimulationRunUniqueResponse(status, error_message) diff --git a/stochss_compute/core/messages/source_ip.py b/stochss_compute/core/messages/source_ip.py new file mode 100644 index 00000000..71f8b789 --- /dev/null +++ b/stochss_compute/core/messages/source_ip.py @@ -0,0 +1,68 @@ +''' +stochss_compute.core.messages.source_ip +''' +from tornado.escape import json_decode +from stochss_compute.core.messages.base import Request, Response + +class SourceIpRequest(Request): + ''' + Restrict server access. + + :param cloud_key: Random key generated locally during launch. + :type cloud_key: str + ''' + + def __init__(self, cloud_key): + self.cloud_key = cloud_key + + def encode(self): + ''' + :returns: self.__dict__ + :rtype: dict + ''' + return self.__dict__ + + @staticmethod + def parse(raw_request): + ''' + Parse HTTP request. + + :param raw_request: The request. + :type raw_request: dict[str, str] + + :returns: The decoded object. + :rtype: SourceIpRequest + ''' + request_dict = json_decode(raw_request) + return SourceIpRequest(request_dict['cloud_key']) + +class SourceIpResponse(Response): + ''' + Response from server containing IP address of the source. + + :param source_ip: IP address of the client. + :type source_ip: str + ''' + def __init__(self, source_ip): + self.source_ip = source_ip + + def encode(self): + ''' + :returns: self.__dict__ + :rtype: dict + ''' + return self.__dict__ + + @staticmethod + def parse(raw_response): + ''' + Parses a http response and returns a python object. + + :param raw_response: A raw http SourceIpResponse from the server. + :type raw_response: str + + :returns: The decoded object. + :rtype: SourceIpResponse + ''' + response_dict = json_decode(raw_response) + return SourceIpResponse(response_dict['source_ip']) diff --git a/stochss_compute/core/messages/status.py b/stochss_compute/core/messages/status.py new file mode 100644 index 00000000..a8b5df61 --- /dev/null +++ b/stochss_compute/core/messages/status.py @@ -0,0 +1,107 @@ +''' +stochss_compute.core.messages.status +''' +from enum import Enum +from tornado.escape import json_decode +from stochss_compute.core.messages.base import Request, Response + +class SimStatus(Enum): + ''' + Status describing a remote simulation. + ''' + PENDING = 'The simulation is pending.' + RUNNING = 'The simulation is still running.' + READY = 'Simulation is done and results exist in the cache.' + ERROR = 'The Simulation has encountered an error.' + DOES_NOT_EXIST = 'There is no evidence of this simulation either running or on disk.' + + @staticmethod + def from_str(name): + ''' + Convert str to Enum. + ''' + if name == 'PENDING': + return SimStatus.PENDING + if name == 'RUNNING': + return SimStatus.RUNNING + if name == 'READY': + return SimStatus.READY + if name == 'ERROR': + return SimStatus.ERROR + if name == 'DOES_NOT_EXIST': + return SimStatus.DOES_NOT_EXIST + # pylint: disable=no-member + raise ValueError(f'Not a valid status.\n{SimStatus._member_names_}') + # pylint: enable=no-member + +class StatusRequest(Request): + ''' + A request for simulation status. + + :param results_id: Hash of the SimulationRunRequest + :type results_id: str + ''' + def __init__(self, results_id): + self.results_id = results_id + def encode(self): + ''' + :returns: self.__dict__ + :rtype: dict + ''' + return self.__dict__ + + @staticmethod + def parse(raw_request): + ''' + Parse HTTP request. + + :param raw_request: The request. + :type raw_request: dict[str, str] + + :returns: The decoded object. + :rtype: StatusRequest + ''' + request_dict = json_decode(raw_request) + return StatusRequest(request_dict['results_id']) + +class StatusResponse(Response): + ''' + A response from the server about simulation status. + + :param status: Status of the simulation + :type status: SimStatus + + :param message: Possible error message or otherwise + :type message: str + ''' + def __init__(self, status, message = None): + self.status = status + self.message = message + + def encode(self): + ''' + Encodes self. + :returns: self as dict + :rtype: dict[str, str] + ''' + return {'status': self.status.name, + 'message': self.message or ''} + + @staticmethod + def parse(raw_response): + ''' + Parse HTTP response. + + :param raw_response: The response. + :type raw_response: dict[str, str] + + :returns: The decoded object. + :rtype: StatusResponse + ''' + response_dict = json_decode(raw_response) + status = SimStatus.from_str(response_dict['status']) + message = response_dict['message'] + if not message: + return StatusResponse(status) + else: + return StatusResponse(status, message) \ No newline at end of file diff --git a/stochss_compute/core/remote_results.py b/stochss_compute/core/remote_results.py index dc7dce01..a81a8808 100644 --- a/stochss_compute/core/remote_results.py +++ b/stochss_compute/core/remote_results.py @@ -21,7 +21,8 @@ from gillespy2 import Results from stochss_compute.client.endpoint import Endpoint from stochss_compute.core.errors import RemoteSimulationError -from stochss_compute.core.messages import ResultsResponse, SimStatus, StatusResponse +from stochss_compute.core.messages.results import ResultsResponse +from stochss_compute.core.messages.status import StatusResponse, SimStatus class RemoteResults(Results): ''' @@ -48,8 +49,10 @@ class RemoteResults(Results): n_traj = None task_id = None + # pylint:disable=super-init-not-called def __init__(self, data = None): self._data = data + # pylint:enable=super-init-not-called @property def data(self): @@ -122,13 +125,14 @@ def _resolve(self): if status in (SimStatus.DOES_NOT_EXIST, SimStatus.ERROR): raise RemoteSimulationError(status_response.message) - if status == SimStatus.READY: print('Results ready. Fetching.......') - response_raw = self.server.get(Endpoint.SIMULATION_GILLESPY2, f"/{self.id}/{self.n_traj}/results") + if self.id == self.task_id: + response_raw = self.server.get(Endpoint.SIMULATION_GILLESPY2, f"/{self.id}/results") + else: + response_raw = self.server.get(Endpoint.SIMULATION_GILLESPY2, f"/{self.id}/{self.n_traj}/results") if not response_raw.ok: raise RemoteSimulationError(response_raw.reason) response = ResultsResponse.parse(response_raw.text) self._data = response.results.data - diff --git a/stochss_compute/core/remote_simulation.py b/stochss_compute/core/remote_simulation.py index 562b909d..11a4a7fd 100644 --- a/stochss_compute/core/remote_simulation.py +++ b/stochss_compute/core/remote_simulation.py @@ -1,5 +1,5 @@ ''' -RemoteSimulation +stochss_compute.core.remote_simulation ''' # StochSS-Compute is a tool for running and caching GillesPy2 simulations remotely. # Copyright (C) 2019-2023 GillesPy2 and StochSS developers. @@ -18,7 +18,9 @@ # along with this program. If not, see . from stochss_compute.client.endpoint import Endpoint -from stochss_compute.core.messages import SimulationRunRequest, SimulationRunResponse, SimStatus +from stochss_compute.core.messages.simulation_run import SimulationRunRequest, SimulationRunResponse +from stochss_compute.core.messages.simulation_run_unique import SimulationRunUniqueRequest, SimulationRunUniqueResponse +from stochss_compute.core.messages.status import SimStatus from stochss_compute.core.errors import RemoteSimulationError from stochss_compute.core.remote_results import RemoteResults @@ -97,12 +99,15 @@ def is_cached(self, **params): results_dummy.n_traj = params.get('number_of_trajectories', 1) return results_dummy.is_ready - def run(self, **params): + def run(self, ignore_cache=False, **params): + # pylint:disable=line-too-long """ Simulate the Model on the target ComputeServer, returning the results or a handle to a running simulation. - See `here `_. + :param unique: When True, ignore cache completely and return always new results. + :type unique: bool + :param params: Arguments to pass directly to the Model#run call on the server. :type params: dict[str, Any] @@ -111,6 +116,7 @@ def run(self, **params): :raises RemoteSimulationError: In the case of SimStatus.ERROR """ + # pylint:enable=line-too-long if "solver" in params: if hasattr(params['solver'], 'is_instantiated'): @@ -119,9 +125,19 @@ def run(self, **params): params["solver"] = f"{params['solver'].__module__}.{params['solver'].__qualname__}" if self.solver is not None: params["solver"] = f"{self.solver.__module__}.{self.solver.__qualname__}" - - sim_request = SimulationRunRequest(model=self.model, **params) - response_raw = self.server.post(Endpoint.SIMULATION_GILLESPY2, sub="/run", request=sim_request) + if ignore_cache is True: + sim_request = SimulationRunUniqueRequest(self.model, **params) + return self._run_unique(sim_request) + if ignore_cache is False: + sim_request = SimulationRunRequest(self.model, **params) + return self._run(sim_request) + + def _run(self, request): + ''' + :param request: Request to send to the server. Contains Model and related arguments. + :type request: SimulationRunRequest + ''' + response_raw = self.server.post(Endpoint.SIMULATION_GILLESPY2, sub="/run", request=request) if not response_raw.ok: raise Exception(response_raw.reason) @@ -136,7 +152,30 @@ def run(self, **params): remote_results.id = sim_response.results_id remote_results.server = self.server - remote_results.n_traj = params.get('number_of_trajectories', 1) + remote_results.n_traj = request.kwargs.get('number_of_trajectories', 1) remote_results.task_id = sim_response.task_id return remote_results + + def _run_unique(self, request): + ''' + Ignores the cache. Gives each simulation request a unique identifier. + + :param request: Request to send to the server. Contains Model and related arguments. + :type request: SimulationRunUniqueRequest + ''' + response_raw = self.server.post(Endpoint.SIMULATION_GILLESPY2, sub="/run/unique", request=request) + + if not response_raw.ok: + raise Exception(response_raw.reason) + sim_response = SimulationRunUniqueResponse.parse(response_raw.text) + if not sim_response.status is SimStatus.RUNNING: + raise Exception(sim_response.error_message) + # non-conforming object creation ... possible refactor needed to solve, so left in. + remote_results = RemoteResults() + remote_results.id = request.unique_key + remote_results.task_id = request.unique_key + remote_results.server = self.server + remote_results.n_traj = request.kwargs.get('number_of_trajectories', 1) + + return remote_results diff --git a/stochss_compute/server/api.py b/stochss_compute/server/api.py index 6c8d4428..38236e9a 100644 --- a/stochss_compute/server/api.py +++ b/stochss_compute/server/api.py @@ -20,22 +20,31 @@ import os import asyncio import subprocess +from logging import INFO from tornado.web import Application from stochss_compute.server.is_cached import IsCachedHandler from stochss_compute.server.run import RunHandler +from stochss_compute.server.run_unique import SimulationRunUniqueHandler +from stochss_compute.server.results_unique import ResultsUniqueHandler from stochss_compute.server.sourceip import SourceIpHandler from stochss_compute.server.status import StatusHandler from stochss_compute.server.results import ResultsHandler +from stochss_compute.core.log_config import init_logging, set_global_log_level +log = init_logging(__name__) def _make_app(dask_host, dask_scheduler_port, cache): scheduler_address = f'{dask_host}:{dask_scheduler_port}' return Application([ (r"/api/v2/simulation/gillespy2/run", RunHandler, {'scheduler_address': scheduler_address, 'cache_dir': cache}), + (r"/api/v2/simulation/gillespy2/run/unique", SimulationRunUniqueHandler, + {'scheduler_address': scheduler_address, 'cache_dir': cache}), (r"/api/v2/simulation/gillespy2/(?P.*?)/(?P[1-9]\d*?)/(?P.*?)/status", StatusHandler, {'scheduler_address': scheduler_address, 'cache_dir': cache}), (r"/api/v2/simulation/gillespy2/(?P.*?)/(?P[1-9]\d*?)/results", ResultsHandler, {'cache_dir': cache}), + (r"/api/v2/simulation/gillespy2/(?P.*?)/results", + ResultsUniqueHandler, {'cache_dir': cache}), (r"/api/v2/cache/gillespy2/(?P.*?)/(?P[1-9]\d*?)/is_cached", IsCachedHandler, {'cache_dir': cache}), (r"/api/v2/cloud/sourceip", SourceIpHandler), @@ -47,6 +56,7 @@ async def start_api( dask_host = 'localhost', dask_scheduler_port = 8786, rm = False, + logging_level = INFO, ): """ Start the REST API with the following arguments. @@ -65,22 +75,33 @@ async def start_api( :param rm: Delete the cache when exiting this program. :type rm: bool + + :param logging_level: Set log level for stochss_compute. + :type debug: logging._Level """ - # clean up lock files here + + set_global_log_level(logging_level) + # TODO clean up lock files here + cache_path = os.path.abspath(cache) app = _make_app(dask_host, dask_scheduler_port, cache) app.listen(port) - print(f'StochSS-Compute listening on: :{port}') - print(f'Cache directory: {cache_path}') - print(f'Connecting to Dask scheduler at: {dask_host}:{dask_scheduler_port}\n') + msg=''' +========================================================================= + StochSS-Compute listening on port: %(port)d + Cache directory: %(cache_path)s + Connecting to Dask scheduler at: %(dask_host)s:%(dask_scheduler_port)d +========================================================================= +''' + log.info(msg, locals()) try: await asyncio.Event().wait() except asyncio.exceptions.CancelledError as error: - print(error) + log.error(error) finally: if rm and os.path.exists(cache_path): - print('Removing cache...', end='') + log.info('Removing cache...') subprocess.Popen(['rm', '-r', cache_path]) - print('OK') + log.info('Cache Removed OK') \ No newline at end of file diff --git a/stochss_compute/server/cache.py b/stochss_compute/server/cache.py index 10f99c86..32c007f2 100644 --- a/stochss_compute/server/cache.py +++ b/stochss_compute/server/cache.py @@ -33,10 +33,14 @@ class Cache: :param results_id: Simulation hash. :type results_id: str ''' - def __init__(self, cache_dir, results_id): + def __init__(self, cache_dir, results_id, unique=False): + if unique is True: + while cache_dir.endswith('/'): + cache_dir = cache_dir[:-1] + cache_dir = cache_dir + '/unique/' self.results_path = os.path.join(cache_dir, f'{results_id}.results') if not os.path.exists(cache_dir): - os.mkdir(cache_dir) + os.makedirs(cache_dir) def create(self) -> None: ''' @@ -71,7 +75,7 @@ def is_empty(self) -> bool: return filesize == 0 return True - def is_ready(self, n_traj_wanted) -> bool: + def is_ready(self, n_traj_wanted=0) -> bool: ''' Check if the results are ready to be retrieved from the cache. diff --git a/stochss_compute/server/is_cached.py b/stochss_compute/server/is_cached.py index 8e5726fb..83d68931 100644 --- a/stochss_compute/server/is_cached.py +++ b/stochss_compute/server/is_cached.py @@ -20,7 +20,7 @@ from datetime import datetime from tornado.web import RequestHandler from stochss_compute.core.errors import RemoteSimulationError -from stochss_compute.core.messages import SimStatus, StatusResponse +from stochss_compute.core.messages.status import SimStatus, StatusResponse from stochss_compute.server.cache import Cache class IsCachedHandler(RequestHandler): diff --git a/stochss_compute/server/results.py b/stochss_compute/server/results.py index 4f43f72f..537d565d 100644 --- a/stochss_compute/server/results.py +++ b/stochss_compute/server/results.py @@ -20,13 +20,19 @@ from datetime import datetime from tornado.web import RequestHandler from stochss_compute.core.errors import RemoteSimulationError -from stochss_compute.core.messages import ResultsResponse +from stochss_compute.core.messages.results import ResultsResponse from stochss_compute.server.cache import Cache class ResultsHandler(RequestHandler): ''' Endpoint for Results objects. ''' + def __init__(self, application, request, **kwargs): + self.cache_dir = None + super().__init__(application, request, **kwargs) + + def data_received(self, chunk: bytes): + raise NotImplementedError() def initialize(self, cache_dir): ''' @@ -47,7 +53,7 @@ async def get(self, results_id = None, n_traj = None): :param n_traj: Number of trajectories in the request. :type n_traj: str ''' - if '' in (results_id, n_traj): + if '' in (results_id, n_traj) or '/' in results_id or '/' in n_traj: self.set_status(404, reason=f'Malformed request: {self.request.uri}') self.finish() raise RemoteSimulationError(f'Malformed request | <{self.request.remote_ip}>') diff --git a/stochss_compute/server/results_unique.py b/stochss_compute/server/results_unique.py new file mode 100644 index 00000000..63ebac39 --- /dev/null +++ b/stochss_compute/server/results_unique.py @@ -0,0 +1,72 @@ +''' +stochss_compute.server.results +''' +# StochSS-Compute is a tool for running and caching GillesPy2 simulations remotely. +# Copyright (C) 2019-2023 GillesPy2 and StochSS developers. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tornado.web import RequestHandler +from stochss_compute.core.errors import RemoteSimulationError +from stochss_compute.core.messages.results import ResultsResponse +from stochss_compute.server.cache import Cache + +from stochss_compute.core.log_config import init_logging +log = init_logging(__name__) + +class ResultsUniqueHandler(RequestHandler): + ''' + Endpoint for simulation-run-unique Results objects. + ''' + def __init__(self, application, request, **kwargs): + self.cache_dir = None + super().__init__(application, request, **kwargs) + + def data_received(self, chunk: bytes): + raise NotImplementedError() + + def initialize(self, cache_dir): + ''' + Set the cache directory. + + :param cache_dir: Path to the cache. + :type cache_dir: str + ''' + self.cache_dir = cache_dir + + async def get(self, results_id = None): + ''' + Process GET request. + + :param results_id: Unique id + :type results_id: str + + ''' + if '' == results_id or '/' in results_id: + self.set_status(404, reason=f'Malformed request: {self.request.uri}') + self.finish() + raise RemoteSimulationError(f'Malformed request | <{self.request.remote_ip}>') + # pylint:disable=possibly-unused-variable + remote_ip = str(self.request.remote_ip) + # pylint:enable=possibly-unused-variable + log.info('<%(remote_ip)s> | Results Request | <%(results_id)s>', locals()) + cache = Cache(self.cache_dir, results_id, unique=True) + if cache.is_ready(): + results = cache.read() + results_response = ResultsResponse(results) + self.write(results_response.encode()) + else: + # This should not happen! + self.set_status(404, f'Results "{results_id}" not found.') + self.finish() diff --git a/stochss_compute/server/run.py b/stochss_compute/server/run.py index d6afb6be..f8389e60 100644 --- a/stochss_compute/server/run.py +++ b/stochss_compute/server/run.py @@ -25,7 +25,8 @@ from tornado.ioloop import IOLoop from distributed import Client, Future from gillespy2.core import Results -from stochss_compute.core.messages import SimStatus, SimulationRunRequest, SimulationRunResponse +from stochss_compute.core.messages.status import SimStatus +from stochss_compute.core.messages.simulation_run import SimulationRunRequest, SimulationRunResponse from stochss_compute.server.cache import Cache @@ -34,16 +35,19 @@ class RunHandler(RequestHandler): Endpoint for running Gillespy2 simulations. ''' + scheduler_address = None + cache_dir = None def initialize(self, scheduler_address, cache_dir): ''' Sets the address to the Dask scheduler and the cache directory. - + :param scheduler_address: Scheduler address. :type scheduler_address: str :param cache_dir: Path to the cache. :type cache_dir: str ''' + self.scheduler_address = scheduler_address self.cache_dir = cache_dir diff --git a/stochss_compute/server/run_unique.py b/stochss_compute/server/run_unique.py new file mode 100644 index 00000000..d57fd713 --- /dev/null +++ b/stochss_compute/server/run_unique.py @@ -0,0 +1,127 @@ +''' +stochss_compute.server.run_unique +''' +# StochSS-Compute is a tool for running and caching GillesPy2 simulations remotely. +# Copyright (C) 2019-2023 GillesPy2 and StochSS developers. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from datetime import datetime + +from tornado.web import RequestHandler +from tornado.ioloop import IOLoop +from distributed import Client, Future +from stochss_compute.core.messages.status import SimStatus +from stochss_compute.core.exceptions import PRNGCollision +from stochss_compute.core.messages.simulation_run_unique import SimulationRunUniqueRequest, SimulationRunUniqueResponse +from stochss_compute.server.cache import Cache + + +class SimulationRunUniqueHandler(RequestHandler): + ''' + Endpoint for running Gillespy2 simulations. + ''' + + def __init__(self, application, request, **kwargs): + self.scheduler_address = None + self.cache_dir = None + self.unique_key = None + super().__init__(application, request, **kwargs) + + def data_received(self, chunk: bytes): + raise NotImplementedError() + + def initialize(self, scheduler_address, cache_dir): + ''' + Sets the address to the Dask scheduler and the cache directory. + Creates a new directory for one-off results files identifiable by token. + + :param scheduler_address: Scheduler address. + :type scheduler_address: str + + :param cache_dir: Path to the cache. + :type cache_dir: str + ''' + self.scheduler_address = scheduler_address + while cache_dir.endswith('/'): + cache_dir = cache_dir[:-1] + self.cache_dir = cache_dir + '/unique/' + + async def post(self): + ''' + Process simulation run unique POST request. + ''' + sim_request = SimulationRunUniqueRequest.parse(self.request.body) + self.unique_key = sim_request.unique_key + cache = Cache(self.cache_dir, self.unique_key) + if cache.exists(): + self.set_status(404, reason='Try again with a different key, because that one is taken.') + self.finish() + raise PRNGCollision('Try again with a different key, because that one is taken.') + cache.create() + client = Client(self.scheduler_address) + future = self._submit(sim_request, client) + log_string = f'{datetime.now()} | <{self.request.remote_ip}> | Simulation Run Unique Request | <{self.unique_key}> | ' + print(log_string + 'Running simulation.') + self._return_running() + IOLoop.current().run_in_executor(None, self._cache, future, client) + + def _cache(self, future, client): + ''' + Await results, close client, save to disk. + + :param future: Handle to the running simulation, to be awaited upon. + :type future: distributed.Future + + :param client: Client to the Dask scheduler. Closing here for good measure, not sure if strictly necessary. + :type client: distributed.Client + ''' + results = future.result() + client.close() + cache = Cache(self.cache_dir, self.unique_key) + cache.save(results) + + def _submit(self, sim_request, client): + ''' + Submit request to dask scheduler. + Uses pydoc.locate to convert str to solver class name object. + + :param sim_request: The user's request for a unique simulation. + :type sim_request: SimulationRunUniqueRequest + + :param client: Client to the Dask scheduler. + :type client: distributed.Client + + :returns: Handle to the running simulation and the results on the worker. + :rtype: distributed.Future + ''' + model = sim_request.model + kwargs = sim_request.kwargs + unique_key = sim_request.unique_key + if "solver" in kwargs: + # pylint:disable=import-outside-toplevel + from pydoc import locate + # pylint:enable=import-outside-toplevel + kwargs["solver"] = locate(kwargs["solver"]) + + future = client.submit(model.run, **kwargs, key=unique_key) + return future + + def _return_running(self): + ''' + Let the user know we submitted the simulation to the scheduler. + ''' + sim_response = SimulationRunUniqueResponse(SimStatus.RUNNING) + self.write(sim_response.encode()) + self.finish() diff --git a/stochss_compute/server/sourceip.py b/stochss_compute/server/sourceip.py index 1fce8e8e..a0068631 100644 --- a/stochss_compute/server/sourceip.py +++ b/stochss_compute/server/sourceip.py @@ -19,7 +19,7 @@ import os from tornado.web import RequestHandler -from stochss_compute.core.messages import SourceIpRequest, SourceIpResponse +from stochss_compute.core.messages.source_ip import SourceIpRequest, SourceIpResponse class SourceIpHandler(RequestHandler): ''' diff --git a/stochss_compute/server/status.py b/stochss_compute/server/status.py index 3ad4ec2e..87c303df 100644 --- a/stochss_compute/server/status.py +++ b/stochss_compute/server/status.py @@ -17,13 +17,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from datetime import datetime from distributed import Client from tornado.web import RequestHandler from stochss_compute.core.errors import RemoteSimulationError -from stochss_compute.core.messages import SimStatus, StatusResponse +from stochss_compute.core.messages.status import SimStatus, StatusResponse from stochss_compute.server.cache import Cache +from stochss_compute.core.log_config import init_logging +log = init_logging(__name__) + class StatusHandler(RequestHandler): ''' Endpoint for requesting the status of a simulation. @@ -37,7 +39,7 @@ def __init__(self, application, request, **kwargs): def data_received(self, chunk: bytes): raise NotImplementedError() - + def initialize(self, scheduler_address, cache_dir): ''' Sets the address to the Dask scheduler and the cache directory. @@ -68,51 +70,50 @@ async def get(self, results_id, n_traj, task_id): self.set_status(404, reason=f'Malformed request: {self.request.uri}') self.finish() raise RemoteSimulationError(f'Malformed request: {self.request.uri}') - self.results_id = results_id self.task_id = task_id n_traj = int(n_traj) + unique = results_id == task_id + log.debug('unique: %(unique)s', locals()) + cache = Cache(self.cache_dir, results_id, unique=unique) + log_string = f'<{self.request.remote_ip}> | Results ID: <{results_id}> | Trajectories: {n_traj} | Task ID: {task_id}' + log.info(log_string) - cache = Cache(self.cache_dir, results_id) - - print(f'{datetime.now()} | <{self.request.remote_ip}> | \ - Status Request | <{results_id}> | Trajectories: {n_traj} | \ - Task ID: {task_id}' ) - - msg = f'{datetime.now()} | <{results_id}> | <{task_id}> | Status: ' + msg = f'<{results_id}> | <{task_id}> | Status: ' exists = cache.exists() + log.debug('exists: %(exists)s', locals()) if exists: empty = cache.is_empty() if empty: if self.task_id not in ('', None): state, err = await self._check_with_scheduler() - print(msg + SimStatus.RUNNING.name + f' | Task: {state} | Error: {err}') + log.info(msg + SimStatus.RUNNING.name + f' | Task: {state} | Error: {err}') if state == 'erred': self._respond_error(err) else: self._respond_running(f'Scheduler task state: {state}') else: - print(msg+SimStatus.DOES_NOT_EXIST.name) + log.info(msg+SimStatus.DOES_NOT_EXIST.name) self._respond_dne() else: ready = cache.is_ready(n_traj) if ready: - print(msg+SimStatus.READY.name) + log.info(msg+SimStatus.READY.name) self._respond_ready() else: if self.task_id not in ('', None): state, err = await self._check_with_scheduler() - print(msg+SimStatus.RUNNING.name+f' | Task: {state} | error: {err}') + log.info(msg+SimStatus.RUNNING.name+f' | Task: {state} | error: {err}') if state == 'erred': self._respond_error(err) else: self._respond_running(f'Scheduler task state: {state}') else: - print(msg+SimStatus.DOES_NOT_EXIST.name) + log.info(msg+SimStatus.DOES_NOT_EXIST.name) self._respond_dne() else: - print(msg+SimStatus.DOES_NOT_EXIST.name) + log.info(msg+SimStatus.DOES_NOT_EXIST.name) self._respond_dne() def _respond_ready(self): diff --git a/test/integration_tests/test_cache.py b/test/integration_tests/test_cache.py index 470d989d..7ecf2f31 100644 --- a/test/integration_tests/test_cache.py +++ b/test/integration_tests/test_cache.py @@ -5,7 +5,7 @@ import subprocess import unittest from gillespy2 import Model -from stochss_compute.core.messages import SimulationRunRequest +from stochss_compute.core.messages.simulation_run import SimulationRunRequest from stochss_compute.server.cache import Cache from . import gillespy2_models diff --git a/test/unit_tests/test_compute_server.py b/test/unit_tests/test_compute_server.py index f6e9f1e2..42df8c88 100644 --- a/test/unit_tests/test_compute_server.py +++ b/test/unit_tests/test_compute_server.py @@ -5,7 +5,7 @@ from stochss_compute import ComputeServer from stochss_compute.client.server import Server from stochss_compute.client.endpoint import Endpoint -from stochss_compute.core.messages import SimulationRunRequest +from stochss_compute.core.messages.simulation_run import SimulationRunRequest from .gillespy2_models import create_decay class ComputeServerTest(unittest.TestCase): @@ -13,14 +13,14 @@ class ComputeServerTest(unittest.TestCase): Test ComputeServer class. ''' def setUp(self) -> None: - self.server = ComputeServer('cRaZyHoSt.lol', 60095) + self.server = ComputeServer('cRaZyHoSt.lol', 7) self.sim_endpoint = Endpoint.SIMULATION_GILLESPY2 def test_init_and_properties(self): ''' Calls init and tests address. ''' - assert self.server.address == 'http://cRaZyHoSt.lol:60095' + assert self.server.address == 'http://cRaZyHoSt.lol:7' def test_get(self): ''' @@ -32,7 +32,7 @@ def test_post(self): ''' calls post to timeout ''' - server = ComputeServer('cRaZyHoSt.lol', 60095) + server = ComputeServer('cRaZyHoSt.lol', 7) sim_endpoint = Endpoint.SIMULATION_GILLESPY2 server.post(sim_endpoint,'', SimulationRunRequest(create_decay())) diff --git a/test/unit_tests/test_ec2.py b/test/unit_tests/test_ec2.py index c21c7088..d7f81e45 100644 --- a/test/unit_tests/test_ec2.py +++ b/test/unit_tests/test_ec2.py @@ -14,8 +14,8 @@ def _get_source_ip(self, cloud_key): def _restrict_ingress(self, ip_address: str = ''): try: super()._restrict_ingress(ip_address) - except NotImplementedError as oh_well: - print(oh_well) + except NotImplementedError as _: + pass @mock_ec2 class EC2ClusterTest(unittest.TestCase): diff --git a/test/unit_tests/test_hash.py b/test/unit_tests/test_hash.py index 6f6c533a..ab9f8ac0 100644 --- a/test/unit_tests/test_hash.py +++ b/test/unit_tests/test_hash.py @@ -2,7 +2,7 @@ HashTest(unittest.TestCase) ''' import unittest -from stochss_compute.core.messages import SimulationRunRequest +from stochss_compute.core.messages.simulation_run import SimulationRunRequest from . import gillespy2_models class HashTest(unittest.TestCase): diff --git a/test/unit_tests/test_is_cached_handler.py b/test/unit_tests/test_is_cached_handler.py index 12581c90..6adde4a5 100644 --- a/test/unit_tests/test_is_cached_handler.py +++ b/test/unit_tests/test_is_cached_handler.py @@ -4,10 +4,9 @@ import os import subprocess from tornado.testing import AsyncHTTPTestCase -from stochss_compute.client.compute_server import ComputeServer -from stochss_compute.core.errors import RemoteSimulationError -from stochss_compute.core.messages import ResultsResponse, SimStatus, SimulationRunRequest, SimulationRunResponse, StatusResponse -from stochss_compute.core.remote_results import RemoteResults +from stochss_compute.core.messages.status import SimStatus +from stochss_compute.core.messages.status import StatusResponse +from stochss_compute.core.messages.simulation_run import SimulationRunRequest from stochss_compute.server.api import _make_app from stochss_compute.server.cache import Cache from .gillespy2_models import create_michaelis_menten diff --git a/test/unit_tests/test_results_handler.py b/test/unit_tests/test_results_handler.py index e8ce5b07..87258a21 100644 --- a/test/unit_tests/test_results_handler.py +++ b/test/unit_tests/test_results_handler.py @@ -1,10 +1,10 @@ ''' -test.unit_tests.test_results +test.unit_tests.test_results_handler ''' import os import subprocess from tornado.testing import AsyncHTTPTestCase -from stochss_compute.core.messages import SimulationRunRequest +from stochss_compute.core.messages.simulation_run import SimulationRunRequest from stochss_compute.server.api import _make_app from stochss_compute.server.cache import Cache from .gillespy2_models import create_michaelis_menten diff --git a/test/unit_tests/test_run_handler.py b/test/unit_tests/test_run_handler.py index c484a990..1bac117d 100644 --- a/test/unit_tests/test_run_handler.py +++ b/test/unit_tests/test_run_handler.py @@ -1,6 +1,6 @@ import os import subprocess -from stochss_compute.core.messages import SimulationRunRequest +from stochss_compute.core.messages.simulation_run import SimulationRunRequest from stochss_compute.server.api import _make_app from stochss_compute.server.cache import Cache from stochss_compute.server.run import RunHandler diff --git a/test/unit_tests/test_sourceip_handler.py b/test/unit_tests/test_sourceip_handler.py index cc6ee2df..09db5fdd 100644 --- a/test/unit_tests/test_sourceip_handler.py +++ b/test/unit_tests/test_sourceip_handler.py @@ -6,7 +6,7 @@ import os import subprocess from tornado.testing import AsyncHTTPTestCase -from stochss_compute.core.messages import SourceIpRequest, SourceIpResponse +from stochss_compute.core.messages.source_ip import SourceIpRequest, SourceIpResponse from stochss_compute.server.api import _make_app class SourceIpHandlerTest(AsyncHTTPTestCase): diff --git a/test/unit_tests/test_status_handler.py b/test/unit_tests/test_status_handler.py index dbb157d7..b99b892f 100644 --- a/test/unit_tests/test_status_handler.py +++ b/test/unit_tests/test_status_handler.py @@ -1,16 +1,18 @@ ''' -test.unit_tests.test_launch +test.unit_tests.test_status_handler ''' import os import subprocess from test.unit_tests.gillespy2_models import create_michaelis_menten from tornado.testing import AsyncHTTPTestCase -from stochss_compute.core.messages import SimStatus, SimulationRunRequest, StatusResponse +from stochss_compute.core.messages.status import SimStatus +from stochss_compute.core.messages.simulation_run import SimulationRunRequest +from stochss_compute.core.messages.status import StatusResponse from stochss_compute.server.api import _make_app from stochss_compute.server.cache import Cache -class StatusTest(AsyncHTTPTestCase): +class StatusHandlerTest(AsyncHTTPTestCase): ''' Test StatusHandler class. ''' @@ -100,3 +102,21 @@ def test_status_ready(self): status_response = StatusResponse.parse(response_raw.body) assert status_response.status == SimStatus.READY + def test_status_unique(self): + ''' + This uri should return a copy of these results + ''' + model = create_michaelis_menten() + results = model.run() + sim = SimulationRunRequest(model=model) + sim_hash = sim.hash() + cache = Cache(self.cache_dir, sim_hash) + cache.create() + cache.save(results) + uri = f'/api/v2/simulation/gillespy2/{sim_hash}/1//status' + response_raw = self.fetch(uri) + assert response_raw.code == 200 + status_response = StatusResponse.parse(response_raw.body) + assert status_response.status == SimStatus.READY + +