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
+
+