Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8660113
Add client response type hints
dmuhs Mar 18, 2019
eff1e81
Remove stale timeout config setting
dmuhs Mar 18, 2019
c11175e
Add repr to analysis model
dmuhs Mar 18, 2019
0f6dfea
Add handler middleware logging
dmuhs Mar 18, 2019
afe1b52
Add analysis list magic methods and tests
dmuhs Mar 18, 2019
615de49
Add client context handler
dmuhs Mar 18, 2019
de47683
Add client context optional var
dmuhs Mar 18, 2019
e1d40ea
Delegate property access to analysis in submission response
dmuhs Mar 19, 2019
20f10ab
Add analysis property delegation test
dmuhs Mar 19, 2019
67a9c40
Add issue report contains and test
dmuhs Mar 19, 2019
b7f5c3a
Add issue report length method
dmuhs Mar 19, 2019
df5d4ac
Add issue report iterator and tests
dmuhs Mar 19, 2019
c132ce9
Add list accessor methods to issue report and tests
dmuhs Mar 19, 2019
af9f535
Add jsonschema requirement
dmuhs Mar 19, 2019
30b6b53
Add validation for version response
dmuhs Mar 19, 2019
88b3676
Add response schema validation, prepare request models
dmuhs Mar 19, 2019
8a618fd
Add base request validation method
dmuhs Mar 20, 2019
c53600b
Add request schema validation and tests
dmuhs Mar 20, 2019
a5f92be
Remove obsolete decode error exceptions
dmuhs Mar 20, 2019
5bb813f
Apply isort and black
dmuhs Mar 20, 2019
4f353fe
Merge branch 'master' into feature/client-improvements
dmuhs Mar 21, 2019
87bad07
Add None schema catch to abstract base classes
dmuhs Mar 21, 2019
5d82ccc
Add model validator error tests
dmuhs Mar 21, 2019
88e05cb
Skip validation if no schema provided and log warning
dmuhs Mar 22, 2019
dd987b7
Fix validator tests
dmuhs Mar 22, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions pythx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
from pythx.models.exceptions import (
PythXBaseException,
PythXAPIError,
RequestDecodeError,
RequestValidationError,
ResponseDecodeError,
ResponseValidationError,
)
31 changes: 19 additions & 12 deletions pythx/api/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from datetime import datetime, timedelta
from datetime import datetime
from typing import Dict, List

import jwt
Expand Down Expand Up @@ -80,7 +80,7 @@ def assert_authentication(self):
LOGGER.debug("Access and refresh token have expired - logging in again")
self.login()

def login(self):
def login(self) -> respmodels.AuthLoginResponse:
req = reqmodels.AuthLoginRequest(
eth_address=self.eth_address, password=self.password
)
Expand All @@ -94,14 +94,14 @@ def login(self):
self.refresh_token = resp_model.refresh_token
return resp_model

def logout(self):
def logout(self) -> respmodels.AuthLogoutResponse:
req = reqmodels.AuthLogoutRequest()
resp_model = self._assemble_send_parse(req, respmodels.AuthLogoutResponse)
self.access_token = None
self.refresh_token = None
return resp_model

def refresh(self, assert_authentication=True):
def refresh(self, assert_authentication=True) -> respmodels.AuthRefreshResponse:
req = reqmodels.AuthRefreshRequest(
access_token=self.access_token, refresh_token=self.refresh_token
)
Expand All @@ -117,7 +117,7 @@ def refresh(self, assert_authentication=True):

def analysis_list(
self, date_from: datetime = None, date_to: datetime = None, offset: int = None
):
) -> respmodels.AnalysisListResponse:
req = reqmodels.AnalysisListRequest(
offset=offset, date_from=date_from, date_to=date_to
)
Expand All @@ -134,7 +134,7 @@ def analyze(
source_list: List[str] = None,
solc_version: str = None,
analysis_mode: str = "quick",
):
) -> respmodels.AnalysisSubmissionResponse:
req = reqmodels.AnalysisSubmissionRequest(
contract_name=contract_name,
bytecode=bytecode,
Expand All @@ -146,25 +146,25 @@ def analyze(
solc_version=solc_version,
analysis_mode=analysis_mode,
)
req.validate()
# req.validate()
return self._assemble_send_parse(req, respmodels.AnalysisSubmissionResponse)

def status(self, uuid: str):
def status(self, uuid: str) -> respmodels.AnalysisStatusResponse:
req = reqmodels.AnalysisStatusRequest(uuid)
return self._assemble_send_parse(req, respmodels.AnalysisStatusResponse)

def analysis_ready(self, uuid: str):
def analysis_ready(self, uuid: str) -> bool:
resp = self.status(uuid)
return (
resp.analysis.status == respmodels.AnalysisStatus.FINISHED
or resp.analysis.status == respmodels.AnalysisStatus.ERROR
)

def report(self, uuid: str):
def report(self, uuid: str) -> respmodels.DetectedIssuesResponse:
req = reqmodels.DetectedIssuesRequest(uuid)
return self._assemble_send_parse(req, respmodels.DetectedIssuesResponse)

def openapi(self, mode="yaml"):
def openapi(self, mode="yaml") -> respmodels.OASResponse:
req = reqmodels.OASRequest(mode=mode)
return self._assemble_send_parse(
req,
Expand All @@ -173,11 +173,18 @@ def openapi(self, mode="yaml"):
include_auth_header=False,
)

def version(self):
def version(self) -> respmodels.VersionResponse:
req = reqmodels.VersionRequest()
return self._assemble_send_parse(
req,
respmodels.VersionResponse,
assert_authentication=False,
include_auth_header=False,
)

def __enter__(self):
self.assert_authentication()
return self

def __exit__(self, exc_type, exc_value, traceback):
self.logout()
2 changes: 2 additions & 0 deletions pythx/api/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ def send_request(request_data: Dict, auth_header: Dict[str, str] = None):

def execute_request_middlewares(self, req):
for mw in self.middlewares:
LOGGER.debug("Executing request middleware: %s", mw)
req = mw.process_request(req)
return req

def execute_response_middlewares(self, resp):
for mw in self.middlewares:
LOGGER.debug("Executing response middleware: %s", mw)
resp = mw.process_response(resp)
return resp

Expand Down
11 changes: 3 additions & 8 deletions pythx/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"--solc-path",
type=click.Path(exists=True),
default=None,
help="Path to the solc compiler"
help="Path to the solc compiler",
)
uuid_arg = click.argument("uuid", type=click.UUID)

Expand Down Expand Up @@ -179,9 +179,7 @@ def compile_from_source(source_path: str, solc_path: str = None):
solc_path = spawn.find_executable("solc") if solc_path is None else solc_path
if solc_path is None:
# user solc path invalid or no default "solc" command found
click.echo(
"Invalid solc path. Please make sure solc is on your PATH."
)
click.echo("Invalid solc path. Please make sure solc is on your PATH.")
sys.exit(1)
solc_command = [
solc_path,
Expand Down Expand Up @@ -311,10 +309,7 @@ def check(config, staging, bytecode_file, source_file, solc_path):
elif source_file:
with open(source_file, "r") as source_f:
source_content = source_f.read().strip()
compiled = compile_from_source(
source_file,
solc_path=solc_path
)
compiled = compile_from_source(source_file, solc_path=solc_path)
if len(compiled["contracts"]) > 1:
click.echo(
(
Expand Down
1 change: 0 additions & 1 deletion pythx/conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,3 @@ def __init__(self):
"production": "https://api.mythx.io/",
"staging": "https://staging.api.mythx.io/",
}
self["timeouts"] = {"access": 600, "refresh": 3600} # 10m # 1h
8 changes: 0 additions & 8 deletions pythx/models/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,10 @@ class PythXBaseException(Exception):
pass


class ResponseDecodeError(PythXBaseException):
pass


class ResponseValidationError(PythXBaseException):
pass


class RequestDecodeError(PythXBaseException):
pass


class RequestValidationError(PythXBaseException):
pass

Expand Down
7 changes: 2 additions & 5 deletions pythx/models/request/analysis_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import dateutil.parser

from pythx.models.exceptions import RequestDecodeError
from pythx.models.exceptions import RequestValidationError
from pythx.models.request.base import BaseRequest

ANALYSIS_LIST_KEYS = ("offset", "dateFrom", "dateTo")
Expand Down Expand Up @@ -35,13 +35,10 @@ def parameters(self):
def payload(self):
return {}

def validate(self):
return (self.date_from <= self.date_to) and self.offset >= 0

@classmethod
def from_dict(cls, d: Dict[str, Any]):
if not all(k in d for k in ANALYSIS_LIST_KEYS):
raise RequestDecodeError(
raise RequestValidationError(
"Not all required keys {} found in data {}".format(
ANALYSIS_LIST_KEYS, d
)
Expand Down
7 changes: 2 additions & 5 deletions pythx/models/request/analysis_status.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from pythx.models.exceptions import RequestDecodeError
from pythx.models.exceptions import RequestValidationError
from pythx.models.request.base import BaseRequest


Expand Down Expand Up @@ -26,14 +26,11 @@ def parameters(self):
def payload(self):
return {}

def validate(self):
pass

@classmethod
def from_dict(cls, d):
uuid = d.get("uuid")
if uuid is None:
raise RequestDecodeError("Missing uuid field in data {}".format(d))
raise RequestValidationError("Missing uuid field in data {}".format(d))
return cls(uuid=uuid)

def to_dict(self):
Expand Down
60 changes: 21 additions & 39 deletions pythx/models/request/analysis_submission.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import json
import logging
from typing import Dict, List

from pythx.models.exceptions import RequestDecodeError, RequestValidationError
from pythx.models.request.base import BaseRequest
from pythx.models.util import dict_delete_none_fields
from pythx.models.util import dict_delete_none_fields, resolve_schema

LOGGER = logging.getLogger(__name__)
ANALYSIS_SUBMISSION_KEYS = ("bytecode", "sources")


class AnalysisSubmissionRequest(BaseRequest):
with open(resolve_schema(__file__, "analysis-submission.json")) as sf:
schema = json.load(sf)

def __init__(
self,
contract_name: str = None,
Expand Down Expand Up @@ -52,31 +54,9 @@ def headers(self):
def payload(self):
return {"data": self.to_dict()}

def validate(self):
LOGGER.debug("Validating %s", self.to_dict())
valid = True
msg = "Error validating analysis submission request: {}"
if self.analysis_mode not in ("full", "quick"):
valid = False
msg = msg.format("Analysis mode must be one of {full,quick}")
elif not (self.bytecode or self.sources):
valid = False
msg = msg.format("Must pass at least bytecode or source field")
# TODO: MOAR

if not valid:
raise RequestValidationError(msg)

@classmethod
def from_dict(cls, d: Dict):
if type(d) is not dict or not any(k in d for k in ANALYSIS_SUBMISSION_KEYS):
raise RequestDecodeError(
"Not all required keys {} found in data {}".format(
ANALYSIS_SUBMISSION_KEYS, d
)
)

# TODO: Should we validate here?
cls.validate(d)
return cls(
contract_name=d.get("contractName"),
bytecode=d.get("bytecode"),
Expand All @@ -90,16 +70,18 @@ def from_dict(cls, d: Dict):
)

def to_dict(self):
base_dict = {
"contractName": self.contract_name,
"bytecode": self.bytecode,
"sourceMap": self.source_map,
"deployedBytecode": self.deployed_bytecode,
"deployedSourceMap": self.deployed_source_map,
"sources": self.sources if self.sources else None,
"sourceList": self.source_list if self.source_list else None,
"version": self.solc_version,
"analysisMode": self.analysis_mode,
}

return dict_delete_none_fields(base_dict)
base_dict = dict_delete_none_fields(
{
"contractName": self.contract_name,
"bytecode": self.bytecode,
"sourceMap": self.source_map,
"deployedBytecode": self.deployed_bytecode,
"deployedSourceMap": self.deployed_source_map,
"sources": self.sources if self.sources else None,
"sourceList": self.source_list if self.source_list else None,
"version": self.solc_version,
"analysisMode": self.analysis_mode,
}
)
self.validate(base_dict)
return base_dict
18 changes: 9 additions & 9 deletions pythx/models/request/auth_login.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import json
from typing import Dict

from pythx.models.exceptions import RequestDecodeError
from pythx.models.request.base import BaseRequest
from pythx.models.util import resolve_schema

AUTH_LOGIN_KEYS = ("ethAddress", "password")


class AuthLoginRequest(BaseRequest):
with open(resolve_schema(__file__, "auth-login.json")) as sf:
schema = json.load(sf)

def __init__(self, eth_address: str, password: str):
self.eth_address = eth_address
self.password = password
Expand All @@ -31,16 +35,12 @@ def headers(self):
def payload(self):
return self.to_dict()

def validate(self):
pass

@classmethod
def from_dict(cls, d: Dict[str, str]):
if not all(k in d for k in AUTH_LOGIN_KEYS):
raise RequestDecodeError(
"Not all required keys {} found in data {}".format(AUTH_LOGIN_KEYS, d)
)
cls.validate(d)
return cls(eth_address=d["ethAddress"], password=d["password"])

def to_dict(self):
return {"ethAddress": self.eth_address, "password": self.password}
d = {"ethAddress": self.eth_address, "password": self.password}
self.validate(d)
return d
16 changes: 9 additions & 7 deletions pythx/models/request/auth_logout.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import json
from typing import Dict

from pythx.models.exceptions import RequestDecodeError
from pythx.models.request.base import BaseRequest
from pythx.models.util import resolve_schema


class AuthLogoutRequest(BaseRequest):
with open(resolve_schema(__file__, "auth-logout.json")) as sf:
schema = json.load(sf)

def __init__(self, global_: bool = False):
self.global_ = global_

Expand All @@ -28,14 +32,12 @@ def headers(self):
def payload(self):
return {}

def validate(self):
pass

@classmethod
def from_dict(cls, d: Dict):
if "global" not in d:
raise RequestDecodeError("Required key 'global' not in data {}".format(d))
cls.validate(d)
return cls(global_=d["global"])

def to_dict(self):
return {"global": self.global_}
d = {"global": self.global_}
self.validate(d)
return d
Loading