Skip to content

Commit c1bd783

Browse files
committed
Migrate endpoints and tests to new mythx models
1 parent 3a4e178 commit c1bd783

22 files changed

+298
-185
lines changed

pythx/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,5 @@
44
__email__ = "[email protected]"
55
__version__ = "1.6.2"
66

7-
from mythx_models.exceptions import MythXAPIError, MythXBaseException, ValidationError
8-
7+
from mythx_models.exceptions import MythXAPIError
98
from pythx.api.client import Client

pythx/api/client.py

Lines changed: 89 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@
77
import jwt
88
from mythx_models import request as reqmodels
99
from mythx_models import response as respmodels
10-
from mythx_models.request.base import BaseRequest
11-
from mythx_models.response.base import BaseResponse
12-
10+
from pythx.types import RESPONSE_MODELS, REQUEST_MODELS
1311
from pythx.api.handler import APIHandler
1412
from pythx.middleware import (
1513
AnalysisCacheMiddleware,
@@ -52,7 +50,7 @@ def __init__(
5250
refresh_token: str = None,
5351
handler: APIHandler = None,
5452
no_cache: bool = False,
55-
middlewares: List[Type[BaseMiddleware]] = None,
53+
middlewares: List[BaseMiddleware] = None,
5654
api_url: str = None,
5755
):
5856
"""Instantiate a new MythX API client.
@@ -101,11 +99,11 @@ class unless a custom instance has already been provided.
10199

102100
def _assemble_send_parse(
103101
self,
104-
req_obj: Type[BaseRequest],
105-
resp_model: Type[BaseResponse],
102+
req_obj: REQUEST_MODELS,
103+
resp_model: Type[RESPONSE_MODELS],
106104
assert_authentication: bool = True,
107105
include_auth_header: bool = True,
108-
) -> Type[BaseResponse]:
106+
) -> RESPONSE_MODELS:
109107
"""Assemble the request, send it, parse and return the response.
110108
111109
This method takes a request model instance and:
@@ -189,13 +187,13 @@ def login(self) -> respmodels.AuthLoginResponse:
189187
:return: :code:`AuthLoginResponse`
190188
"""
191189
req = reqmodels.AuthLoginRequest(username=self.username, password=self.password)
192-
resp_model = self._assemble_send_parse(
190+
resp_model: respmodels.AuthLoginResponse = self._assemble_send_parse(
193191
req,
194192
respmodels.AuthLoginResponse,
195193
assert_authentication=False,
196194
include_auth_header=False,
197195
)
198-
self.api_key = resp_model.api_key
196+
self.api_key = resp_model.access_token
199197
self.refresh_token = resp_model.refresh_token
200198
return resp_model
201199

@@ -204,7 +202,7 @@ def logout(self) -> respmodels.AuthLogoutResponse:
204202
205203
:return: :code:`AuthLogoutResponse`
206204
"""
207-
req = reqmodels.AuthLogoutRequest()
205+
req = reqmodels.AuthLogoutRequest(**{"global": True})
208206
resp_model = self._assemble_send_parse(req, respmodels.AuthLogoutResponse)
209207
self.api_key = None
210208
self.refresh_token = None
@@ -218,7 +216,7 @@ def refresh(self) -> respmodels.AuthRefreshResponse:
218216
req = reqmodels.AuthRefreshRequest(
219217
access_token=self.api_key, refresh_token=self.refresh_token
220218
)
221-
resp_model = self._assemble_send_parse(
219+
resp_model: respmodels.AuthRefreshResponse = self._assemble_send_parse(
222220
req,
223221
respmodels.AuthRefreshResponse,
224222
assert_authentication=False,
@@ -228,6 +226,76 @@ def refresh(self) -> respmodels.AuthRefreshResponse:
228226
self.refresh_token = resp_model.refresh_token
229227
return resp_model
230228

229+
def project_list(
230+
self, offset: int = None, limit: int = None, name: str = ""
231+
) -> respmodels.ProjectListResponse:
232+
"""List the existing projects on the platform
233+
234+
:param offset: The number of projects to skip (optional)
235+
:param limit: The number of projects to return (optional)
236+
:param name: The name to filter projects by (optional)
237+
:return:
238+
"""
239+
req = reqmodels.ProjectListRequest(
240+
offset=offset,
241+
limit=limit,
242+
name=name,
243+
)
244+
return self._assemble_send_parse(req, respmodels.ProjectListResponse)
245+
246+
def project_status(self, project_id: str = "") -> respmodels.ProjectStatusResponse:
247+
"""Get detailed information for a project.
248+
249+
:param project_id: The project's ID
250+
:return: :code:`ProjectStatusResponse`
251+
"""
252+
req = reqmodels.ProjectStatusRequest(project_id=project_id)
253+
return self._assemble_send_parse(req, respmodels.ProjectStatusResponse)
254+
255+
def delete_project(
256+
self, project_id: str = ""
257+
) -> respmodels.ProjectDeletionResponse:
258+
"""Delete an existing project.
259+
260+
:param project_id: The project's ID
261+
:return: :code:`ProjectDeletionResponse`
262+
"""
263+
req = reqmodels.ProjectDeleteRequest(project_id=project_id)
264+
return self._assemble_send_parse(req, respmodels.ProjectDeletionResponse)
265+
266+
def create_project(
267+
self, name: str = "", description: str = "", groups: List[str] = None
268+
) -> respmodels.ProjectCreationResponse:
269+
"""Create a new project.
270+
271+
:param name: The project name
272+
:param description: The project description
273+
:param groups: List of group IDs belonging to the project (optional)
274+
:return: :code:`ProjectCreationResponse`
275+
"""
276+
req = reqmodels.ProjectCreationRequest(
277+
name=name, description=description, groups=groups or []
278+
)
279+
return self._assemble_send_parse(req, respmodels.ProjectCreationResponse)
280+
281+
def update_project(
282+
self, project_id: str = "", name: str = "", description: str = ""
283+
) -> respmodels.ProjectUpdateResponse:
284+
"""Update an existing project.
285+
286+
A new name, a new description, or both should be given.
287+
288+
:param project_id: The ID of the project to update
289+
:param name: The new project name (optional)
290+
:param description: The new project description (optional)
291+
:return: :code:`ProjectUpdateResponse`
292+
"""
293+
294+
req = reqmodels.ProjectUpdateRequest(
295+
project_id=project_id, name=name, description=description
296+
)
297+
return self._assemble_send_parse(req, respmodels.ProjectUpdateResponse)
298+
231299
def group_list(
232300
self,
233301
offset: int = None,
@@ -288,13 +356,13 @@ def analysis_list(
288356

289357
def analyze(
290358
self,
291-
contract_name: str = None,
292359
bytecode: str = None,
360+
main_source: str = None,
361+
sources: Dict[str, Dict[str, str]] = None,
362+
contract_name: str = None,
293363
source_map: str = None,
294364
deployed_bytecode: str = None,
295365
deployed_source_map: str = None,
296-
main_source: str = None,
297-
sources: Dict[str, Dict[str, str]] = None,
298366
source_list: List[str] = None,
299367
solc_version: str = None,
300368
analysis_mode: str = "quick",
@@ -330,7 +398,6 @@ def analyze(
330398
solc_version=solc_version,
331399
analysis_mode=analysis_mode,
332400
)
333-
# req.validate()
334401
return self._assemble_send_parse(req, respmodels.AnalysisSubmissionResponse)
335402

336403
def group_status(self, group_id: str) -> respmodels.GroupStatusResponse:
@@ -342,14 +409,13 @@ def group_status(self, group_id: str) -> respmodels.GroupStatusResponse:
342409
req = reqmodels.GroupStatusRequest(group_id=group_id)
343410
return self._assemble_send_parse(req, respmodels.GroupStatusResponse)
344411

345-
def status(self, uuid: str) -> respmodels.AnalysisStatusResponse:
412+
def analysis_status(self, uuid: str) -> respmodels.AnalysisStatusResponse:
346413
"""Get the status of an analysis job based on its UUID.
347414
348415
:param uuid: The job's UUID
349416
:return: :code:`AnalysisStatusResponse`
350417
"""
351-
# TODO: rename to analysis_status
352-
req = reqmodels.AnalysisStatusRequest(uuid)
418+
req = reqmodels.AnalysisStatusRequest(uuid=uuid)
353419
return self._assemble_send_parse(req, respmodels.AnalysisStatusResponse)
354420

355421
def analysis_ready(self, uuid: str) -> bool:
@@ -359,10 +425,10 @@ def analysis_ready(self, uuid: str) -> bool:
359425
:param uuid: The analysis job UUID
360426
:return: bool indicating whether the analysis has finished
361427
"""
362-
resp = self.status(uuid)
428+
resp = self.analysis_status(uuid)
363429
return (
364-
resp.analysis.status == respmodels.AnalysisStatus.FINISHED
365-
or resp.analysis.status == respmodels.AnalysisStatus.ERROR
430+
resp.status == respmodels.AnalysisStatus.FINISHED
431+
or resp.status == respmodels.AnalysisStatus.ERROR
366432
)
367433

368434
def report(self, uuid: str) -> respmodels.DetectedIssuesResponse:
@@ -372,7 +438,7 @@ def report(self, uuid: str) -> respmodels.DetectedIssuesResponse:
372438
:param uuid: The analysis job UUID
373439
:return: :code:`DetectedIssuesResponse`
374440
"""
375-
req = reqmodels.DetectedIssuesRequest(uuid)
441+
req = reqmodels.DetectedIssuesRequest(uuid=uuid)
376442
return self._assemble_send_parse(req, respmodels.DetectedIssuesResponse)
377443

378444
def request_by_uuid(self, uuid: str) -> respmodels.AnalysisInputResponse:
@@ -381,7 +447,7 @@ def request_by_uuid(self, uuid: str) -> respmodels.AnalysisInputResponse:
381447
:param uuid: The analysis job UUID
382448
:return: :code:`AnalysisInputResponse`
383449
"""
384-
req = reqmodels.AnalysisInputRequest(uuid)
450+
req = reqmodels.AnalysisInputRequest(uuid=uuid)
385451
return self._assemble_send_parse(req, respmodels.AnalysisInputResponse)
386452

387453
def create_group(self, group_name: str = "") -> respmodels.GroupCreationResponse:
@@ -404,20 +470,6 @@ def seal_group(self, group_id: str) -> respmodels.GroupOperationResponse:
404470
req = reqmodels.GroupOperationRequest(group_id=group_id, type_="seal_group")
405471
return self._assemble_send_parse(req, respmodels.GroupOperationResponse)
406472

407-
def openapi(self, mode: str = "yaml") -> respmodels.OASResponse:
408-
"""Return the OpenAPI specification either in HTML or YAML.
409-
410-
:param mode: "yaml" or "html"
411-
:return: :code:`OASResponse`
412-
"""
413-
req = reqmodels.OASRequest(mode=mode)
414-
return self._assemble_send_parse(
415-
req,
416-
respmodels.OASResponse,
417-
assert_authentication=False,
418-
include_auth_header=False,
419-
)
420-
421473
def version(self) -> respmodels.VersionResponse:
422474
"""Call the APIs version endpoint to get its backend version numbers.
423475

pythx/api/handler.py

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
"""This module contains the API request handler implementation."""
2-
2+
from json import JSONDecodeError
33
import logging
44
import os
55
import re
66
import urllib.parse
77
from typing import Dict, List, Type
8-
98
import requests
109
from mythx_models.exceptions import MythXAPIError
11-
from mythx_models.request.base import BaseRequest
12-
from mythx_models.response.base import BaseResponse
13-
10+
from mythx_models.response import DetectedIssuesResponse, IssueReport
11+
from pythx.types import RESPONSE_MODELS, REQUEST_MODELS
1412
from pythx.middleware.base import BaseMiddleware
15-
13+
from pydantic import parse_obj_as
1614
DEFAULT_API_URL = "https://api.mythx.io/"
1715

1816

@@ -56,7 +54,7 @@ class APIHandler:
5654
"""
5755

5856
def __init__(
59-
self, middlewares: List[Type[BaseMiddleware]] = None, api_url: str = None
57+
self, middlewares: List[BaseMiddleware] = None, api_url: str = None
6058
):
6159
"""Instantiate a new API handler class.
6260
@@ -85,7 +83,7 @@ def _normalize_url(url: str) -> str:
8583
return url + "/" if not url.endswith("/") else url
8684

8785
@staticmethod
88-
def send_request(request_data: Dict, auth_header: Dict[str, str] = None) -> str:
86+
def send_request(request_data: Dict, auth_header: Dict[str, str] = None) -> Dict:
8987
"""Send a request to the API.
9088
9189
This method takes a data dictionary holding the request's method (HTTP verb),
@@ -100,7 +98,7 @@ def send_request(request_data: Dict, auth_header: Dict[str, str] = None) -> str:
10098
{
10199
"method": "GET",
102100
"headers": {},
103-
"url": "https://api.mythx.io/v1/analyses/6b9e4a52-f061-4960-8246-e1560627336a/issues",
101+
"url": "https://api.mythx.io/v1/analyses/<uuid>/issues",
104102
"payload": "",
105103
"params": {}
106104
}
@@ -133,7 +131,15 @@ def send_request(request_data: Dict, auth_header: Dict[str, str] = None) -> str:
133131
response.status_code, response.content.decode()
134132
)
135133
)
136-
return response.text
134+
try:
135+
content = response.json()
136+
except JSONDecodeError:
137+
raise MythXAPIError(
138+
"Got unexpected response data: Expected JSON but got {}".format(
139+
response.text
140+
)
141+
)
142+
return content
137143

138144
def execute_request_middlewares(self, req: Dict) -> Dict:
139145
"""Sequentially execute the registered request middlewares.
@@ -157,7 +163,7 @@ def execute_request_middlewares(self, req: Dict) -> Dict:
157163
req = mw.process_request(req)
158164
return req
159165

160-
def execute_response_middlewares(self, resp: Type[BaseResponse]) -> Dict:
166+
def execute_response_middlewares(self, resp: RESPONSE_MODELS) -> RESPONSE_MODELS:
161167
"""Sequentially execute the registered response middlewares.
162168
163169
Each middleware gets the serialized response domain model. On top of the request any
@@ -176,12 +182,12 @@ def execute_response_middlewares(self, resp: Type[BaseResponse]) -> Dict:
176182
"""
177183
for mw in self.middlewares:
178184
LOGGER.debug("Executing response middleware: %s", mw)
179-
resp = mw.process_response(resp)
185+
resp = mw.process_response(resp=resp)
180186
return resp
181187

182188
def parse_response(
183-
self, resp: str, model: Type[BaseResponse]
184-
) -> Type[BaseResponse]:
189+
self, resp: dict, model_cls: Type[RESPONSE_MODELS]
190+
) -> RESPONSE_MODELS:
185191
"""Parse the API response into its respective domain model variant.
186192
187193
This method takes the raw HTTP response and a class it should deserialize the responsse
@@ -192,13 +198,16 @@ def parse_response(
192198
on to the user.
193199
194200
:param resp: The raw HTTP response JSON payload
195-
:param model: The domain model class the data should be deserialized into
201+
:param model_cls: The domain model class the data should be deserialized into
196202
:return: The domain model holding the response data
197203
"""
198-
m = model.from_json(resp)
204+
if type(resp) is list and model_cls is DetectedIssuesResponse:
205+
m = DetectedIssuesResponse(issue_reports=parse_obj_as(List[IssueReport], resp))
206+
else:
207+
m = model_cls(**resp)
199208
return self.execute_response_middlewares(m)
200209

201-
def assemble_request(self, req: Type[BaseRequest]) -> Dict:
210+
def assemble_request(self, req: REQUEST_MODELS) -> Dict:
202211
"""Assemble a request that is later sent to the API.
203212
204213
This method generates an intermediate data dictionary format holding all the relevant

pythx/middleware/analysiscache.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
field."""
33

44
import logging
5-
from typing import Dict, Type
5+
from typing import Type
66

7-
from mythx_models.response.base import BaseResponse
7+
from pythx.types import RESPONSE_MODELS, REQUEST_MODELS
88

99
from pythx.middleware.base import BaseMiddleware
1010

@@ -23,7 +23,7 @@ def __init__(self, no_cache: bool = False):
2323
LOGGER.debug("Initializing with no_cache=%s", no_cache)
2424
self.no_cache = no_cache
2525

26-
def process_request(self, req: Dict) -> Dict:
26+
def process_request(self, req: REQUEST_MODELS) -> REQUEST_MODELS:
2727
"""Add the :code:`noCacheLookup` field if the request we are making is
2828
the submission of a new analysis job.
2929
@@ -40,7 +40,7 @@ def process_request(self, req: Dict) -> Dict:
4040
req["payload"]["noCacheLookup"] = self.no_cache
4141
return req
4242

43-
def process_response(self, resp: Type[BaseResponse]) -> Type[BaseResponse]:
43+
def process_response(self, resp: Type[RESPONSE_MODELS]) -> Type[RESPONSE_MODELS]:
4444
"""This method is irrelevant for adding our tool name data, so we don't
4545
do anything here.
4646

0 commit comments

Comments
 (0)