diff --git a/.coveragerc b/.coveragerc index 54856c6..e054045 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,9 +1,8 @@ [run] -omit = - *tests* source = openprocurement_client [report] +omit = *tests* exclude_lines = pragma: no cover def __repr__ diff --git a/.gitignore b/.gitignore index c5f8917..447ab9e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python +.env/ env/ build/ develop-eggs/ diff --git a/openprocurement_client/api_base_client.py b/openprocurement_client/api_base_client.py deleted file mode 100644 index c06123f..0000000 --- a/openprocurement_client/api_base_client.py +++ /dev/null @@ -1,297 +0,0 @@ -import logging -import uuid - -from .exceptions import http_exceptions_dict, InvalidResponse, RequestFailed - -from functools import wraps -from io import FileIO -from munch import munchify -from os import path -from requests import Session -from requests.auth import HTTPBasicAuth as BasicAuth -from simplejson import loads -from retrying import retry - -logger = logging.getLogger(__name__) -IGNORE_PARAMS = ('uri', 'path') - - -def verify_file(fn): - @wraps(fn) - def wrapper(self, file_, *args, **kwargs): - if isinstance(file_, basestring): - # Using FileIO here instead of open() - # to be able to override the filename - # which is later used when uploading the file. - # - # Explanation: - # - # 1) requests reads the filename - # from "name" attribute of a file-like object, - # there is no other way to specify a filename; - # - # 2) The attribute may contain the full path to file, - # which does not work well as a filename; - # - # 3) The attribute is readonly when using open(), - # unlike FileIO object. - file_ = FileIO(file_, 'rb') - file_.name = path.basename(file_.name) - if hasattr(file_, 'read'): - # A file-like object must have 'read' method - output = fn(self, file_, *args, **kwargs) - file_.close() - return output - else: - try: - file_.close() - except AttributeError: - pass - raise TypeError('Expected either a string ' - 'containing a path to file or a ' - 'file-like object, got {}'.format(type(file_))) - return wrapper - - -class APITemplateClient(object): - """base class for API""" - - def __init__(self, login_pass=None, headers=None, user_agent=None): - self.headers = headers or {} - self.session = Session() - if login_pass is not None: - self.session.auth = BasicAuth(*login_pass) - - if user_agent is None: - self.session.headers['User-Agent'] \ - = 'op.client/{}'.format(uuid.uuid4().hex) - else: - self.session.headers['User-Agent'] = user_agent - - def request(self, method, path=None, payload=None, json=None, - headers=None, params_dict=None, file_=None): - _headers = self.headers.copy() - _headers.update(headers or {}) - if file_: - _headers.pop('Content-Type', None) - - response = self.session.request( - method, path, data=payload, json=json, headers=_headers, - params=params_dict, files=file_ - ) - - if response.status_code >= 400: - raise http_exceptions_dict\ - .get(response.status_code, RequestFailed)(response) - - return response - - -class APIBaseClient(APITemplateClient): - """base class for API""" - - host_url = 'https://api-sandbox.openprocurement.org' - api_version = '2.0' - headers = {'Content-Type': 'application/json'} - - def __init__(self, - key, - resource, - host_url=None, - api_version=None, - params=None, - ds_client=None, - user_agent=None): - - super(APIBaseClient, self)\ - .__init__(login_pass=(key, ''), headers=self.headers, - user_agent=user_agent) - - self.ds_client = ds_client - self.host_url = host_url or self.host_url - self.api_version = api_version or self.api_version - - if not isinstance(params, dict): - params = {'mode': '_all_'} - self.params = params or {} - # To perform some operations (e.g. create a tender) - # we first need to obtain a cookie. For that reason, - # here we send a HEAD request to a neutral URL. - response = self.session.request( - 'HEAD', '{}/api/{}/spore'.format(self.host_url, self.api_version) - ) - response.raise_for_status() - - self.prefix_path = '{}/api/{}/{}'\ - .format(self.host_url, self.api_version, resource) - - @staticmethod - def _get_access_token(obj): - return getattr(getattr(obj, 'access', ''), 'token', '') - - def _update_params(self, params): - for key in params: - if key not in IGNORE_PARAMS: - self.params[key] = params[key] - - def _create_resource_item(self, url, payload, headers=None, method='POST'): - _headers = self.headers.copy() - _headers.update(headers or {}) - - response_item = self.request( - method, url, headers=_headers, json=payload - ) - if (response_item.status_code == 201 and method == 'POST') \ - or (response_item.status_code in (200, 204) - and method in ('PUT', 'DELETE')): - return munchify(loads(response_item.text)) - raise InvalidResponse(response_item) - - def _get_resource_item(self, url, headers=None): - _headers = self.headers.copy() - _headers.update(headers or {}) - response_item = self.request('GET', url, headers=_headers) - if response_item.status_code == 200: - return munchify(loads(response_item.text)) - raise InvalidResponse(response_item) - - @retry(stop_max_attempt_number=5) - def _get_resource_items(self, params=None, feed='changes'): - _params = (params or {}).copy() - _params['feed'] = feed - self._update_params(_params) - response = self.request('GET', - self.prefix_path, - params_dict=self.params) - if response.status_code == 200: - resource_items_list = munchify(loads(response.text)) - self._update_params(resource_items_list.next_page) - return resource_items_list.data - elif response.status_code == 404: - del self.params['offset'] - - raise InvalidResponse(response) - - def _patch_resource_item(self, url, payload, headers=None): - _headers = self.headers.copy() - _headers.update(headers or {}) - response_item = self.request( - 'PATCH', url, headers=_headers, json=payload - ) - if response_item.status_code == 200: - return munchify(loads(response_item.text)) - raise InvalidResponse(response_item) - - def _upload_resource_file( - self, url, file_=None, headers=None, method='POST', - use_ds_client=True, doc_registration=True - ): - if use_ds_client and self.ds_client: - if doc_registration: - response = self.ds_client.document_upload_registered( - file_=file_, headers=headers - ) - else: - response = self.ds_client.document_upload_not_registered( - file_=file_, headers=headers - ) - payload = {'data': response['data']} - response = self._create_resource_item( - url, - headers=headers, - payload=payload, - method=method - ) - else: - if use_ds_client: - logger.warning('use_ds_client parameter is True while ' - 'DS-client is not passed to the client ' - 'constructor.') - logger.warning( - 'File upload/download/delete outside of the Document Service ' - 'is deprecated' - ) - response = self.request( - method, url, headers=headers, file_={'file': file_} - ) - if response.status_code in (201, 200): - response = munchify(loads(response.text)) - else: - raise InvalidResponse(response) - - return response - - def _delete_resource_item(self, url, headers=None): - _headers = self.headers.copy() - _headers.update(headers or {}) - response_item = self.request('DELETE', url, headers=_headers) - if response_item.status_code == 200: - return munchify(loads(response_item.text)) - raise InvalidResponse(response_item) - - def _patch_obj_resource_item(self, patched_obj, item_obj, items_name): - return self._patch_resource_item( - '{}/{}/{}/{}'.format( - self.prefix_path, patched_obj.data.id, - items_name, item_obj['data']['id'] - ), - payload=item_obj, - headers={'X-Access-Token': self._get_access_token(patched_obj)} - ) - - def patch_document(self, obj, document): - return self._patch_obj_resource_item(obj, document, 'documents') - - @verify_file - def upload_document(self, file_, obj, use_ds_client=True, - doc_registration=True): - return self._upload_resource_file( - '{}/{}/documents'.format( - self.prefix_path, - obj.data.id - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(obj)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - def get_resource_item(self, id, headers=None): - return self._get_resource_item('{}/{}'.format(self.prefix_path, id), - headers=headers) - - def patch_credentials(self, id, access_token): - return self._patch_resource_item( - '{}/{}/credentials'.format(self.prefix_path, id), - payload=None, - headers={'X-Access-Token': access_token} - ) - - - def renew_cookies(self): - old_cookies = 'Old cookies:\n' - for k in self.session.cookies.keys(): - old_cookies += '{}={}\n'.format(k, self.session.cookies[k]) - logger.debug(old_cookies.strip()) - - self.session.cookies.clear() - - response = self.session.request( - 'HEAD', '{}/api/{}/spore'.format(self.host_url, self.api_version) - ) - response.raise_for_status() - - new_cookies = 'New cookies:\n' - for k in self.session.cookies.keys(): - new_cookies += '{}={}\n'.format(k, self.session.cookies[k]) - logger.debug(new_cookies) - - def create_resource_item(self, resource_item): - return self._create_resource_item(self.prefix_path, resource_item) - - def patch_resource_item(self, resource_item): - return self._patch_resource_item( - '{}/{}'.format(self.prefix_path, resource_item['data']['id']), - payload=resource_item, - headers={'X-Access-Token': self._get_access_token(resource_item)} - ) diff --git a/openprocurement_client/client.py b/openprocurement_client/client.py deleted file mode 100755 index 9fef895..0000000 --- a/openprocurement_client/client.py +++ /dev/null @@ -1,541 +0,0 @@ -import logging - -from .api_base_client import APIBaseClient, APITemplateClient, verify_file -from .exceptions import InvalidResponse - -from iso8601 import parse_date -from munch import munchify -from retrying import retry -from simplejson import loads - - -logger = logging.getLogger(__name__) - -IGNORE_PARAMS = ('uri', 'path') - - -class TendersClient(APIBaseClient): - """client for tenders""" - - def __init__(self, - key, - resource='tenders', # another possible value is 'auctions' - host_url=None, - api_version=None, - params=None, - ds_client=None, - user_agent=None): - super(TendersClient, self).__init__( - key, resource, host_url, api_version, params, ds_client, - user_agent - ) - - ########################################################################### - # GET ITEMS LIST API METHODS - ########################################################################### - - @retry(stop_max_attempt_number=5) - def get_tenders(self, params=None, feed='changes'): - _params = (params or {}).copy() - _params['feed'] = feed - self._update_params(_params) - response = self.request('GET', - self.prefix_path, - params_dict=self.params) - if response.status_code == 200: - tender_list = munchify(loads(response.text)) - self._update_params(tender_list.next_page) - return tender_list.data - elif response.status_code == 404: - del self.params['offset'] - - raise InvalidResponse(response) - - def get_latest_tenders(self, date, tender_id): - iso_dt = parse_date(date) - dt = iso_dt.strftime('%Y-%m-%d') - tm = iso_dt.strftime('%H:%M:%S') - data = self._get_resource_item( - '{}?offset={}T{}&opt_fields=tender_id&mode=test'.format( - self.prefix_path, - dt, - tm - ) - ) - return data - - def _get_tender_resource_list(self, tender, items_name): - return self._get_resource_item( - '{}/{}/{}'.format(self.prefix_path, tender.data.id, items_name), - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def get_questions(self, tender): - return self._get_tender_resource_list(tender, 'questions') - - def get_documents(self, tender): - return self._get_tender_resource_list(tender, 'documents') - - def get_awards_documents(self, tender, award_id): - return self._get_resource_item( - '{}/{}/awards/{}/documents'.format(self.prefix_path, tender.data.id, award_id), - headers={'X-Access-Token': - getattr(getattr(tender, 'access', ''), 'token', '')} - ) - - def get_qualification_documents(self, tender, qualification_id): - return self._get_resource_item( - '{}/{}/qualifications/{}/documents'.format(self.prefix_path, tender.data.id, qualification_id), - headers={'X-Access-Token': - getattr(getattr(tender, 'access', ''), 'token', '')} - ) - - def get_awards(self, tender): - return self._get_tender_resource_list(tender, 'awards') - - def get_lots(self, tender): - return self._get_tender_resource_list(tender, 'lots') - - ########################################################################### - # CREATE ITEM API METHODS - ########################################################################### - - def _create_tender_resource_item(self, tender, item_obj, items_name): - return self._create_resource_item( - '{}/{}/{}'.format(self.prefix_path, tender.data.id, items_name), - item_obj, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def create_tender(self, tender): - return self._create_resource_item(self.prefix_path, tender) - - def create_question(self, tender, question): - return self._create_tender_resource_item(tender, question, 'questions') - - def create_bid(self, tender, bid): - return self._create_tender_resource_item(tender, bid, 'bids') - - def create_lot(self, tender, lot): - return self._create_tender_resource_item(tender, lot, 'lots') - - def create_award(self, tender, award): - return self._create_tender_resource_item(tender, award, 'awards') - - def create_cancellation(self, tender, cancellation): - return self._create_tender_resource_item( - tender, cancellation, 'cancellations' - ) - - def create_complaint(self, tender, complaint): - return self\ - ._create_tender_resource_item(tender, complaint, 'complaints') - - def create_award_complaint(self, tender, complaint, award_id): - return self._create_resource_item( - '{}/{}/{}'.format(self.prefix_path, tender.data.id, - 'awards/{0}/complaints'.format(award_id)), - complaint, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def create_thin_document(self, tender, document_data): - return self._create_resource_item( - '{}/{}/documents'.format( - self.prefix_path, - tender.data.id - ), - document_data, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - ########################################################################### - # GET ITEM API METHODS - ########################################################################### - - def get_tender(self, id): - return self._get_resource_item('{}/{}'.format(self.prefix_path, id)) - - def _get_tender_resource_item(self, tender, item_id, items_name, - access_token=None): - access_token = access_token or self._get_access_token(tender) - headers = {'X-Access-Token': access_token} - return self._get_resource_item( - '{}/{}/{}/{}'.format(self.prefix_path, - tender.data.id, - items_name, - item_id), - headers=headers - ) - - def get_question(self, tender, question_id): - return self._get_tender_resource_item(tender, question_id, 'questions') - - def get_bid(self, tender, bid_id, access_token): - return self._get_tender_resource_item(tender, bid_id, 'bids', - access_token) - - def get_lot(self, tender, lot_id): - return self._get_tender_resource_item(tender, lot_id, 'lots') - - def get_file(self, url, access_token=None): - headers = {'X-Access-Token': access_token} if access_token else {} - - headers.update(self.headers) - response_item = self.request('GET', url, headers=headers) - - # if response_item.status_code == 302: - # response_obj = request(response_item.headers['location']) - if response_item.status_code == 200: - return response_item.text, \ - response_item.headers['Content-Disposition'] \ - .split("; filename=")[1].strip('"') - raise InvalidResponse(response_item) - - def get_file_properties(self, url, file_hash, access_token=None): - headers = {'X-Access-Token': access_token} if access_token else {} - headers.update(self.headers) - response_item = self.request('GET', url, headers=headers) - if response_item.status_code == 200: - file_properties={ - 'Content_Disposition': response_item.headers['Content-Disposition'], - 'Content_Type': response_item.headers['Content-Type'], - 'url': url, - 'hash': file_hash - } - return file_properties - raise InvalidResponse(response_item) - - def extract_credentials(self, id): - return self._get_resource_item( - '{}/{}/extract_credentials'.format(self.prefix_path, id) - ) - - ########################################################################### - # PATCH ITEM API METHODS - ########################################################################### - - def patch_tender(self, tender): - return self._patch_resource_item( - '{}/{}'.format(self.prefix_path, tender['data']['id']), - payload=tender, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def patch_question(self, tender, question): - return self._patch_obj_resource_item(tender, question, 'questions') - - def patch_bid(self, tender, bid): - return self._patch_obj_resource_item(tender, bid, 'bids') - - def patch_bid_document(self, tender, document_data, bid_id, document_id): - return self._patch_resource_item( - '{}/{}/{}/{}/documents/{}'.format( - self.prefix_path, tender.data.id, 'bids', bid_id, document_id - ), - payload=document_data, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def patch_award(self, tender, award): - return self._patch_obj_resource_item(tender, award, 'awards') - - def patch_award_document(self, tender, document_data, award_id, document_id): - return self._patch_resource_item( - '{}/{}/awards/{}/documents/{}'.format( - self.prefix_path, tender.data.id, award_id, document_id - ), - payload=document_data, - headers={'X-Access-Token': - getattr(getattr(tender, 'access', ''), 'token', '')} - ) - - def patch_cancellation(self, tender, cancellation): - return self._patch_obj_resource_item( - tender, cancellation, 'cancellations' - ) - - def patch_cancellation_document( - self, tender, cancellation, cancellation_id, cancellation_doc_id - ): - return self._patch_resource_item( - '{}/{}/{}/{}/documents/{}'.format( - self.prefix_path, tender.data.id, 'cancellations', - cancellation_id, cancellation_doc_id - ), - payload=cancellation, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def patch_complaint(self, tender, complaint): - return self._patch_obj_resource_item( - tender, complaint, 'complaints' - ) - - def patch_award_complaint(self, tender, complaint, award_id): - return self._patch_resource_item( - '{}/{}/awards/{}/complaints/{}'.format( - self.prefix_path, tender.data.id, award_id, complaint.data.id - ), - payload=complaint, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - def patch_lot(self, tender, lot): - return self._patch_obj_resource_item(tender, lot, 'lots') - - def patch_qualification(self, tender, qualification): - return self._patch_obj_resource_item( - tender, qualification, 'qualifications' - ) - - def patch_contract(self, tender, contract): - return self._patch_obj_resource_item(tender, contract, 'contracts') - - def patch_contract_document(self, tender, document_data, - contract_id, document_id): - return self._patch_resource_item( - '{}/{}/{}/{}/documents/{}'.format( - self.prefix_path, tender.data.id, 'contracts', - contract_id, document_id - ), - payload=document_data, - headers={'X-Access-Token': self._get_access_token(tender)} - ) - - ########################################################################### - # UPLOAD FILE API METHODS - ########################################################################### - - @verify_file - def upload_bid_document(self, file_, tender, bid_id, doc_type='documents', - use_ds_client=True, doc_registration=True): - return self._upload_resource_file( - '{}/{}/bids/{}/{}'.format( - self.prefix_path, - tender.data.id, - bid_id, - doc_type - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def update_bid_document(self, file_, tender, bid_id, document_id, - doc_type='documents', use_ds_client=True, - doc_registration=True): - return self._upload_resource_file( - '{}/{}/bids/{}/{}/{}'.format( - self.prefix_path, - tender.data.id, - bid_id, - doc_type, - document_id - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - method='PUT', - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def upload_cancellation_document(self, file_, tender, cancellation_id, - use_ds_client=True, - doc_registration=True): - return self._upload_resource_file( - '{}/{}/cancellations/{}/documents'.format( - self.prefix_path, - tender.data.id, - cancellation_id, - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def update_cancellation_document(self, file_, tender, cancellation_id, - document_id, use_ds_client=True, - doc_registration=True): - return self._upload_resource_file( - '{}/{}/cancellations/{}/documents/{}'.format( - self.prefix_path, - tender.data.id, - cancellation_id, - document_id - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - method='PUT', - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def upload_complaint_document(self, file_, tender, complaint_id, - use_ds_client=True, doc_registration=True): - return self._upload_resource_file( - '{}/{}/complaints/{}/documents'.format( - self.prefix_path, - tender.data.id, - complaint_id), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def upload_award_complaint_document(self, file_, tender, award_id, - complaint_id, use_ds_client=True, - doc_registration=True): - return self._upload_resource_file( - '{}/{}/awards/{}/complaints/{}/documents'.format( - self.prefix_path, - tender.data.id, - award_id, - complaint_id), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def upload_qualification_document(self, file_, tender, qualification_id, - use_ds_client=True, - doc_registration=True): - return self._upload_resource_file( - '{}/{}/qualifications/{}/documents'.format( - self.prefix_path, - tender.data.id, - qualification_id - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def upload_award_document(self, file_, tender, award_id, - use_ds_client=True, doc_registration=True, - doc_type='documents'): - return self._upload_resource_file( - '{}/{}/awards/{}/{}'.format( - self.prefix_path, - tender.data.id, - award_id, - doc_type - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - @verify_file - def upload_contract_document(self, file_, tender, contract_id, - use_ds_client=True, doc_registration=True): - return self._upload_resource_file( - '{}/{}/contracts/{}/documents'.format( - self.prefix_path, - tender.data.id, - contract_id - ), - file_=file_, - headers={'X-Access-Token': self._get_access_token(tender)}, - use_ds_client=use_ds_client, - doc_registration=doc_registration - ) - - ########################################################################### - # DELETE ITEMS LIST API METHODS - ########################################################################### - - def delete_bid(self, tender, bid, access_token=None): - logger.info('delete_lot is deprecated. In next update this function ' - 'will takes bid_id and access_token instead bid.') - if isinstance(bid, basestring): - bid_id = bid - access_token = access_token - else: - bid_id = bid.data.id - access_token = getattr(getattr(bid, 'access', ''), 'token', '') - return self._delete_resource_item( - '{}/{}/bids/{}'.format( - self.prefix_path, - tender.data.id, - bid_id - ), - headers={'X-Access-Token': access_token} - ) - - def delete_lot(self, tender, lot): - logger.info('delete_lot is deprecated. In next update this function ' - 'will takes lot_id instead lot.') - if isinstance(lot, basestring): - lot_id = lot - else: - lot_id = lot.data.id - return self._delete_resource_item( - '{}/{}/lots/{}'.format( - self.prefix_path, - tender.data.id, - lot_id - ), - headers={'X-Access-Token': self._get_access_token(tender)} - ) - ########################################################################### - - -class Client(TendersClient): - """client for tenders for backward compatibility""" - - -class TendersClientSync(TendersClient): - - def sync_tenders(self, params=None, extra_headers=None): - _params = (params or {}).copy() - _params['feed'] = 'changes' - self.headers.update(extra_headers or {}) - - response = self.request('GET', self.prefix_path, - params_dict=_params) - if response.status_code == 200: - tender_list = munchify(loads(response.text)) - return tender_list - - @retry(stop_max_attempt_number=5) - def get_tender(self, id, extra_headers=None): - self.headers.update(extra_headers or {}) - return super(TendersClientSync, self).get_tender(id) - - -class EDRClient(APITemplateClient): - """ Client for validate members by EDR """ - - host_url = 'https://api-sandbox.openprocurement.org' - api_version = '2.0' - - def __init__(self, host_url=None, api_version=None, username=None, - password=None): - super(EDRClient, self).__init__(login_pass=(username, password)) - self.host_url = host_url or self.host_url - self.api_version = api_version or self.api_version - - def verify_member(self, edrpou, extra_headers=None): - self.headers.update(extra_headers or {}) - response = self.request( - 'GET', - '{}/api/{}/verify'.format(self.host_url, self.api_version), - params_dict={'id': edrpou} - ) - if response.status_code == 200: - return munchify(loads(response.text)) - raise InvalidResponse(response) diff --git a/openprocurement_client/clients.py b/openprocurement_client/clients.py new file mode 100755 index 0000000..a84788b --- /dev/null +++ b/openprocurement_client/clients.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +import logging +from openprocurement_client.constants import DOCUMENTS +from openprocurement_client.exceptions import InvalidResponse +from openprocurement_client.templates import APITemplateClient +from openprocurement_client.utils import verify_file +from iso8601 import parse_date +from simplejson import loads +from retrying import retry +from munch import munchify + +from openprocurement_client.resources.document_service import \ + DocumentServiceClient + + +LOGGER = logging.getLogger(__name__) +IGNORE_PARAMS = ('uri', 'path') + + +class APIBaseClient(APITemplateClient): + """Base class for API""" + + host_url = 'https://api-sandbox.openprocurement.org' + api_version = '0' + headers = {'Content-Type': 'application/json'} + + def __init__(self, + key='', + resource='tenders', + host_url=None, + api_version=None, + params=None, + ds_config=None, + user_agent=None): + + super(APIBaseClient, self).__init__(login_pass=(key, ''), + headers=self.headers, + user_agent=user_agent) + if ds_config: + self.ds_client = DocumentServiceClient(ds_config['host_url'], + ds_config['auth_ds']) + self.host_url = host_url or self.host_url + self.api_version = api_version or self.api_version + + if not isinstance(params, dict): + params = {'mode': '_all_'} + self.params = params or {} + # To perform some operations (e.g. create a tender) + # we first need to obtain a cookie. For that reason, + # here we send a HEAD request to a neutral URL. + response = self.session.request( + 'HEAD', '{}/api/{}/spore'.format(self.host_url, self.api_version) + ) + response.raise_for_status() + self.resource = resource + self.prefix_path = '{}/api/{}/{}'.format(self.host_url, + self.api_version, resource) + + def _create_resource_item(self, url, payload, headers=None, method='POST'): + _headers = self.headers.copy() + _headers.update(headers or {}) + + response_item = self.request( + method, url, headers=_headers, json=payload + ) + if ((response_item.status_code == 201 and method == 'POST') or + (response_item.status_code in (200, 204) and + method in ('PUT', 'DELETE'))): + return munchify(loads(response_item.text)) + raise InvalidResponse(response_item) + + def _delete_resource_item(self, url, headers=None): + _headers = self.headers.copy() + _headers.update(headers or {}) + response_item = self.request('DELETE', url, headers=_headers) + if response_item.status_code == 200: + return munchify(loads(response_item.text)) + raise InvalidResponse(response_item) + + def _get_resource_item(self, url, headers=None): + _headers = self.headers.copy() + _headers.update(headers or {}) + response_item = self.request('GET', url, headers=_headers) + if response_item.status_code == 200: + return munchify(loads(response_item.text)) + raise InvalidResponse(response_item) + + @retry(stop_max_attempt_number=5) + def _get_resource_items(self, params=None, feed='changes'): + _params = (params or {}).copy() + _params['feed'] = feed + self._update_params(_params) + response = self.request('GET', + self.prefix_path, + params_dict=self.params) + if response.status_code == 200: + resource_items_list = munchify(loads(response.text)) + self._update_params(resource_items_list.next_page) + return resource_items_list.data + elif response.status_code == 404: + del self.params['offset'] + + raise InvalidResponse(response) + + def _patch_resource_item(self, url, payload, headers=None): + _headers = self.headers.copy() + _headers.update(headers or {}) + response_item = self.request( + 'PATCH', url, headers=_headers, json=payload + ) + if response_item.status_code == 200: + return munchify(loads(response_item.text)) + raise InvalidResponse(response_item) + + def _patch_obj_resource_item(self, patched_obj, item_obj, items_name): + return self._patch_resource_item( + '{}/{}/{}/{}'.format( + self.prefix_path, patched_obj.data.id, + items_name, item_obj['data']['id'] + ), + payload=item_obj, + headers={'X-Access-Token': self._get_access_token(patched_obj)} + ) + + def _update_params(self, params): + for key in params: + if key not in IGNORE_PARAMS: + self.params[key] = params[key] + + def _upload_resource_file(self, url, file_=None, headers=None, + method='POST', doc_registration=True): + if hasattr(self, 'ds_client'): + if doc_registration: + response = self.ds_client.document_upload_registered( + file_=file_, headers=headers + ) + else: + response = self.ds_client.document_upload_not_registered( + file_=file_, headers=headers + ) + payload = {'data': response['data']} + response = self._create_resource_item( + url, + headers=headers, + payload=payload, + method=method + ) + else: + LOGGER.warning( + 'File upload/download/delete outside of the Document Service ' + 'is deprecated' + ) + response = self.request( + method, url, headers=headers, file_={'file': file_} + ) + if response.status_code in (201, 200): + response = munchify(loads(response.text)) + else: + raise InvalidResponse(response) + + return response + + def renew_cookies(self): + old_cookies = 'Old cookies:\n' + for k in self.session.cookies.keys(): + old_cookies += '{}={}\n'.format(k, self.session.cookies[k]) + LOGGER.debug(old_cookies.strip()) + + self.session.cookies.clear() + + response = self.session.request( + 'HEAD', '{}/api/{}/spore'.format(self.host_url, self.api_version) + ) + response.raise_for_status() + + new_cookies = 'New cookies:\n' + for k in self.session.cookies.keys(): + new_cookies += '{}={}\n'.format(k, self.session.cookies[k]) + LOGGER.debug(new_cookies) + + +class APIResourceClient(APIBaseClient): + """ API Resource Client """ + + def __init__(self, *args, **kwargs): + super(APIResourceClient, self).__init__(*args, **kwargs) + + ########################################################################### + # CREATE CLIENT METHODS + ########################################################################### + + def create_resource_item(self, resource_item): + return self._create_resource_item(self.prefix_path, resource_item) + + def create_resource_item_subitem(self, resource_item_id, subitem_obj, + subitem_name, access_token=None, + depth_path=None): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + if depth_path: + url = '{}/{}/{}/{}'.format(self.prefix_path, resource_item_id, + depth_path, subitem_name) + else: + url = '{}/{}/{}'.format(self.prefix_path, resource_item_id, + subitem_name) + return self._create_resource_item(url, subitem_obj, headers=headers) + + ########################################################################### + # DELETE CLIENT METHODS + ########################################################################### + + def delete_resource_item_subitem(self, resource_item_id, subitem_name, + subitem_id, access_token=None, + depth_path=None): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + if depth_path: + url = '{}/{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, depth_path, subitem_name, + subitem_id + ) + else: + url = '{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, subitem_name, subitem_id + ) + return self._delete_resource_item(url, headers=headers) + + ########################################################################### + # GET CLIENT METHODS + ########################################################################### + + def get_resource_item(self, resource_item_id): + return self._get_resource_item( + '{}/{}'.format(self.prefix_path, resource_item_id) + ) + + def get_resource_item_subitem(self, resource_item_id, subitem_id_or_name, + access_token=None, depth_path=None): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + if depth_path: + url = '{}/{}/{}/{}'.format(self.prefix_path, resource_item_id, + depth_path, subitem_id_or_name) + else: + url = '{}/{}/{}'.format(self.prefix_path, resource_item_id, + subitem_id_or_name) + return self._get_resource_item(url, headers=headers) + + def get_resource_items(self, params=None, feed='changes'): + return self._get_resource_items(params=params, feed=feed) + + def get_latest_resource_items(self, date): + iso_dt = parse_date(date) + dt = iso_dt.strftime('%Y-%m-%d') + tm = iso_dt.strftime('%H:%M:%S') + data = self._get_resource_item( + '{}?offset={}T{}&opt_fields={}_id&mode=test'.format( + self.prefix_path, + dt, + tm, + self.resource[:-1] + ) + ) + return data + + def get_file(self, url, access_token=None): + headers = {'X-Access-Token': access_token} if access_token else {} + + headers.update(self.headers) + response_item = self.request('GET', url, headers=headers) + + # if response_item.status_code == 302: + # response_obj = request(response_item.headers['location']) + if response_item.status_code == 200: + return response_item.text, \ + response_item.headers['Content-Disposition'] \ + .split("; filename=")[1].strip('"') + raise InvalidResponse(response_item) + + def get_file_properties(self, url, file_hash, access_token=None): + headers = {'X-Access-Token': access_token} if access_token else {} + headers.update(self.headers) + response_item = self.request('GET', url, headers=headers) + if response_item.status_code == 200: + file_properties = { + 'Content_Disposition': + response_item.headers['Content-Disposition'], + 'Content_Type': response_item.headers['Content-Type'], + 'url': url, + 'hash': file_hash + } + return file_properties + raise InvalidResponse(response_item) + + ########################################################################### + # PATCH CLIENT METHODS + ########################################################################### + + def patch_credentials(self, resource_item_id, access_token): + return self._patch_resource_item( + '{}/{}/credentials'.format(self.prefix_path, resource_item_id), + payload=None, + headers={'X-Access-Token': access_token} + ) + + def patch_resource_item(self, resource_item_id, patch_data, + access_token=None): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + return self._patch_resource_item( + '{}/{}'.format(self.prefix_path, resource_item_id), + payload=patch_data, headers=headers + ) + + def patch_resource_item_subitem(self, resource_item_id, patch_data, + subitem_name, subitem_id=None, + access_token=None, depth_path=None): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + if depth_path: + url = '{}/{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, depth_path, subitem_name, + subitem_id + ) + else: + url = '{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, subitem_name, subitem_id + ) + return self._patch_resource_item(url, patch_data, headers=headers) + + def patch_document(self, resource_item_id, document_data, document_id, + access_token=None, depth_path=None): + return self.patch_resource_item_subitem( + resource_item_id, document_data, DOCUMENTS, document_id, + access_token, depth_path + ) + + ########################################################################### + # UPLOAD CLIENT METHODS + ########################################################################### + + @verify_file + def update_document(self, file_, resource_item_id, document_id, + doc_registration=True, access_token=None, + depth_path=None, doc_type=DOCUMENTS): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + if depth_path: + url = '{}/{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, depth_path, doc_type, + document_id + ) + else: + url = '{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, doc_type, document_id + ) + return self._upload_resource_file( + url, file_=file_, headers=headers, method='PUT', + doc_registration=doc_registration + ) + + ########################################################################### + # UPLOAD CLIENT METHODS + ########################################################################### + + @verify_file + def upload_document(self, file_, resource_item_id, doc_registration=True, + access_token=None, depth_path=None, + doc_type=DOCUMENTS): + headers = None + if access_token: + headers = {'X-Access-Token': access_token} + if depth_path: + url = '{}/{}/{}/{}'.format( + self.prefix_path, resource_item_id, depth_path, doc_type + ) + else: + url = '{}/{}/{}'.format( + self.prefix_path, resource_item_id, doc_type + ) + return self._upload_resource_file( + url, file_=file_, headers=headers, + doc_registration=doc_registration + ) + + def extract_credentials(self, resource_item_id): + return self._get_resource_item( + '{}/{}/extract_credentials'.format(self.prefix_path, + resource_item_id) + ) + + +class APIResourceClientSync(APIResourceClient): + + def sync_resource_items(self, params=None, extra_headers=None): + _params = (params or {}).copy() + _params['feed'] = 'changes' + self.headers.update(extra_headers or {}) + + response = self.request('GET', self.prefix_path, + params_dict=_params) + if response.status_code == 200: + tender_list = munchify(loads(response.text)) + return tender_list + + @retry(stop_max_attempt_number=5) + def get_resource_item(self, resource_item_id, extra_headers=None): + self.headers.update(extra_headers or {}) + return super(APIResourceClientSync, self).get_resource_item( + resource_item_id) diff --git a/openprocurement_client/constants.py b/openprocurement_client/constants.py new file mode 100644 index 0000000..4ead9f4 --- /dev/null +++ b/openprocurement_client/constants.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +ASSETS = 'assets' +AUCTIONS = 'auctions' +AWARDS = 'awards' +BIDS = 'bids' +CANCELLATIONS = 'cancellations' +CHANGES = 'changes' +COMPLAINTS = 'complaints' +CONTRACTS = 'contracts' +DOCUMENTS = 'documents' +ELIGIBILITY_DOCUMENTS = 'eligibility_documents' +FINANCIAL_DOCUMENTS = 'financial_documens' +LOTS = 'lots' +PLANS = 'plans' +QUALIFICATION_DOCUMENTS = 'qualificationDocuments' +QUALIFICATIONS = 'qualifications' +QUESTIONS = 'questions' +TENDERS = 'tenders' diff --git a/openprocurement_client/contract.py b/openprocurement_client/contract.py deleted file mode 100644 index 231e553..0000000 --- a/openprocurement_client/contract.py +++ /dev/null @@ -1,75 +0,0 @@ -from simplejson import loads -from munch import munchify -from client import APIBaseClient -from openprocurement_client.exceptions import ResourceNotFound, InvalidResponse - - -class ContractingClient(APIBaseClient): - """ contracting client """ - - def __init__(self, - key, - host_url=None, - api_version=None, - params=None, - ds_client=None): - super(ContractingClient, self).__init__(key, 'contracts', host_url, - api_version, params, ds_client) - - def create_contract(self, contract): - return self._create_resource_item(self.prefix_path, contract) - - def get_contract(self, id): - return self._get_resource_item('{}/{}'.format(self.prefix_path, id)) - - def get_contracts(self, params=None, feed='changes'): - _params = (params or {}).copy() - _params['feed'] = feed - self._update_params(_params) - try: - response = self.request('GET', self.prefix_path, - params_dict=self.params) - except ResourceNotFound as e: - self.params.pop('offset', None) - raise e - if response.status_code == 200: - contracts = munchify(loads(response.text)) - self._update_params(contracts.next_page) - return contracts.data - - raise InvalidResponse(response) - - def _create_contract_resource_item(self, contract_id, access_token, item_obj, items_name): - return self._create_resource_item( - '{}/{}/{}'.format(self.prefix_path, contract_id, items_name), - item_obj, - headers={'X-Access-Token': access_token} - ) - - def create_change(self, contract_id, access_token, change_data): - return self._create_contract_resource_item(contract_id, access_token, change_data, - "changes") - - def retrieve_contract_credentials(self, contract_id, access_token): - # In order to get rights for future contract editing, tender token is passed as access token here. - # Response will contain the new access token for further contract modification. - return self._patch_resource_item( - '{}/{}/credentials'.format(self.prefix_path, contract_id), - payload=None, - headers={'X-Access-Token': access_token} - ) - - def patch_contract(self, contract_id, access_token, data): - return self._patch_resource_item( - '{}/{}'.format(self.prefix_path, contract_id), - payload=data, - headers={'X-Access-Token': access_token} - ) - - def patch_change(self, contract_id, change_id, access_token, data): - return self._patch_resource_item( - '{}/{}/{}/{}'.format(self.prefix_path, contract_id, 'changes', - change_id), - payload=data, - headers={'X-Access-Token': access_token} - ) diff --git a/openprocurement_client/plan.py b/openprocurement_client/plan.py deleted file mode 100644 index a35fbb5..0000000 --- a/openprocurement_client/plan.py +++ /dev/null @@ -1,121 +0,0 @@ -from client import APIBaseClient, InvalidResponse -from iso8601 import parse_date -from munch import munchify -from retrying import retry -from simplejson import loads -import logging - -logger = logging.getLogger(__name__) - - -class PlansClient(APIBaseClient): - """client for plans""" - - api_version = '0.8' - - def __init__(self, - key, - host_url=None, - api_version=None, - params=None, - ds_client=None): - - super(PlansClient, self)\ - .__init__(key, 'plans', host_url, api_version, params, ds_client) - - ########################################################################### - # GET ITEMS LIST API METHODS - ########################################################################### - - @retry(stop_max_attempt_number=5) - def get_plans(self, params=None, feed='changes'): - _params = (params or {}).copy() - _params['feed'] = feed - self._update_params(_params) - response = self.request('GET', - self.prefix_path, - params_dict=self.params) - if response.status_code == 200: - plan_list = munchify(loads(response.text)) - self._update_params(plan_list.next_page) - return plan_list.data - elif response.status_code == 412: - del self.params['offset'] - - raise InvalidResponse - - def get_latest_plans(self, date): - iso_dt = parse_date(date) - dt = iso_dt.strftime('%Y-%m-%d') - tm = iso_dt.strftime('%H:%M:%S') - data = self._get_resource_item( - '{}?offset={}T{}&opt_fields=plan_id&mode=test'.format( - self.prefix_path, - dt, - tm - ) - ) - return data - - def _get_plan_resource_list(self, plan, items_name): - return self._get_resource_item( - '{}/{}/{}'.format(self.prefix_path, plan.data.id, items_name), - headers={'X-Access-Token': self._get_access_token(plan)} - ) - - ########################################################################### - # CREATE ITEM API METHODS - ########################################################################### - - def _create_plan_resource_item(self, plan, item_obj, items_name): - return self._create_resource_item( - '{}/{}/{}'.format(self.prefix_path, plan.data.id, items_name), - item_obj, - headers={'X-Access-Token': self._get_access_token(plan)} - ) - - def create_plan(self, plan): - return self._create_resource_item(self.prefix_path, plan) - - ########################################################################### - # GET ITEM API METHODS - ########################################################################### - - def get_plan(self, plan_id): - return self._get_resource_item('{}/{}' - .format(self.prefix_path, plan_id)) - - def _get_plan_resource_item(self, plan, item_id, items_name, - access_token=''): - if access_token: - headers = {'X-Access-Token': access_token} - else: - headers = {'X-Access-Token': self._get_access_token(plan)} - return self._get_resource_item( - '{}/{}/{}/{}'.format(self.prefix_path, - plan.data.id, - items_name, - item_id), - headers=headers - ) - - ########################################################################### - # PATCH ITEM API METHODS - ########################################################################### - - def _patch_plan_resource_item(self, plan, item_obj, items_name): - return self._patch_resource_item( - '{}/{}/{}/{}'.format( - self.prefix_path, plan.data.id, - items_name, item_obj['data']['id'] - ), - payload=item_obj, - headers={'X-Access-Token': self._get_access_token(plan)} - ) - - def patch_plan(self, plan): - return self._patch_resource_item( - '{}/{}'.format(self.prefix_path, plan['data']['id']), - payload=plan, - headers={'X-Access-Token': self._get_access_token(plan)} - ) diff --git a/openprocurement_client/registry_client.py b/openprocurement_client/registry_client.py deleted file mode 100644 index 1431e86..0000000 --- a/openprocurement_client/registry_client.py +++ /dev/null @@ -1,59 +0,0 @@ -import logging - -from .api_base_client import APIBaseClient -from .exceptions import InvalidResponse - -from iso8601 import parse_date -from munch import munchify -from retrying import retry -from simplejson import loads - - -class LotsClient(APIBaseClient): - """ Client for Openregistry Lots """ - - api_version = '0.1' - - def __init__(self, - key='', - resource='lots', - host_url=None, - api_version=None, - params=None, - ds_client=None, - user_agent=None): - super(LotsClient, self).__init__( - key=key, resource=resource, host_url=host_url, params=params, - api_version=api_version, ds_client=ds_client, - user_agent=user_agent) - - def get_lot(self, lot_id, headers=None): - return self.get_resource_item(lot_id, headers=headers) - - def get_lots(self, params=None, feed='changes'): - return self._get_resource_items(params=params, feed=feed) - - -class AssetsClient(APIBaseClient): - """ Client for Openregistry Assets """ - - api_version = '0.1' - - def __init__(self, - key='', - resource='assets', - host_url=None, - api_version=None, - params=None, - ds_client=None, - user_agent=None): - super(AssetsClient, self).__init__( - key=key, resource=resource, host_url=host_url, params=params, - api_version=api_version, ds_client=ds_client, - user_agent=user_agent) - - def get_asset(self, asset_id, headers=None): - return self.get_resource_item(asset_id, headers=headers) - - def get_assets(self, params=None, feed='changes'): - return self._get_resource_items(params=params, feed=feed) diff --git a/openprocurement_client/resources/__init__.py b/openprocurement_client/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/openprocurement_client/resources/assets.py b/openprocurement_client/resources/assets.py new file mode 100644 index 0000000..fce60b0 --- /dev/null +++ b/openprocurement_client/resources/assets.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import ASSETS + + +class AssetsClient(APIResourceClient): + """ Client for Openregistry Assets """ + + resource = ASSETS + + def __init__(self, *args, **kwargs): + super(AssetsClient, self).__init__(resource=self.resource, *args, + **kwargs) + + get_asset = APIResourceClient.get_resource_item + + get_assets = APIResourceClient.get_resource_items diff --git a/openprocurement_client/resources/contracts.py b/openprocurement_client/resources/contracts.py new file mode 100644 index 0000000..8452a0e --- /dev/null +++ b/openprocurement_client/resources/contracts.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import CHANGES, CONTRACTS + + +class ContractingClient(APIResourceClient): + """ Contracting client """ + resource = CONTRACTS + + def __init__(self, *args, **kwargs): + super(ContractingClient, self).__init__(resource=self.resource, *args, + **kwargs) + + def create_contract(self, contract): + return self.create_resource_item(contract) + + def get_contract(self, contract_id): + return self.get_resource_item(contract_id) + + def get_contracts(self, params=None, feed=CHANGES): + return self.get_resource_items(params=params, feed=feed) + + def create_change(self, contract_id, access_token, change_data): + return self.create_resource_item_subitem( + contract_id, change_data, CHANGES, access_token=access_token + ) + + def retrieve_contract_credentials(self, contract_id, access_token): + # In order to get rights for future contract editing, tender token + # is passed as access token here. + # Response will contain the new access token for further + # contract modification. + return self.patch_credentials(contract_id, access_token) + + def patch_contract(self, contract_id, access_token, data): + return self.patch_resource_item( + contract_id, data, access_token + ) + + def patch_change(self, contract_id, change_id, access_token, data): + return self.patch_resource_item_subitem( + contract_id, data, CHANGES, change_id, access_token=access_token + ) diff --git a/openprocurement_client/document_service_client.py b/openprocurement_client/resources/document_service.py similarity index 95% rename from openprocurement_client/document_service_client.py rename to openprocurement_client/resources/document_service.py index fc55cc2..b968132 100644 --- a/openprocurement_client/document_service_client.py +++ b/openprocurement_client/resources/document_service.py @@ -1,7 +1,6 @@ import hashlib - -from .api_base_client import APITemplateClient -from .exceptions import InvalidResponse +from openprocurement_client.templates import APITemplateClient +from openprocurement_client.exceptions import InvalidResponse from munch import munchify from simplejson import loads diff --git a/openprocurement_client/resources/edr.py b/openprocurement_client/resources/edr.py new file mode 100644 index 0000000..ee90135 --- /dev/null +++ b/openprocurement_client/resources/edr.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.exceptions import InvalidResponse +from openprocurement_client.templates import APITemplateClient +from munch import munchify +from simplejson import loads + + +class EDRClient(APITemplateClient): + """ Client for validate members by EDR """ + + host_url = 'https://api-sandbox.openprocurement.org' + api_version = '2.0' + + def __init__(self, host_url=None, api_version=None, username=None, + password=None): + super(EDRClient, self).__init__(login_pass=(username, password)) + self.host_url = host_url or self.host_url + self.api_version = api_version or self.api_version + + def verify_member(self, edrpou, extra_headers=None): + self.headers.update(extra_headers or {}) + response = self.request( + 'GET', + '{}/api/{}/verify'.format(self.host_url, self.api_version), + params_dict={'id': edrpou} + ) + if response.status_code == 200: + return munchify(loads(response.text)) + raise InvalidResponse(response) diff --git a/openprocurement_client/resources/lots.py b/openprocurement_client/resources/lots.py new file mode 100644 index 0000000..62b57b4 --- /dev/null +++ b/openprocurement_client/resources/lots.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import LOTS + + +class LotsClient(APIResourceClient): + """ Client for Openregistry Lots """ + + resource = LOTS + + def __init__(self, *args, **kwargs): + super(LotsClient, self).__init__(resource=self.resource, *args, + **kwargs) + + get_lot = APIResourceClient.get_resource_item + + get_lots = APIResourceClient.get_resource_items diff --git a/openprocurement_client/resources/plans.py b/openprocurement_client/resources/plans.py new file mode 100644 index 0000000..5d76eb1 --- /dev/null +++ b/openprocurement_client/resources/plans.py @@ -0,0 +1,57 @@ +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import PLANS +import logging + + +LOGGER = logging.getLogger(__name__) + + +class PlansClient(APIResourceClient): + """Client for plans""" + + api_version = '0' + resource = PLANS + + def __init__(self, *args, **kwargs): + + super(PlansClient, self).__init__(resource=self.resource, *args, + **kwargs) + + ########################################################################### + # GET ITEMS LIST API METHODS + ########################################################################### + + def get_plans(self, params=None, feed='changes'): + return self.get_resource_items(params, feed) + + def get_latest_plans(self, date): + return self.get_latest_resource_items(date) + + def _get_plan_resource_list(self, plan, items_name): + return self._get_resource_item( + '{}/{}/{}'.format(self.prefix_path, plan.data.id, items_name), + headers={'X-Access-Token': self._get_access_token(plan)} + ) + + ########################################################################### + # CREATE ITEM API METHODS + ########################################################################### + + def create_plan(self, plan): + return self.create_resource_item(plan) + + ########################################################################### + # GET ITEM API METHODS + ########################################################################### + + def get_plan(self, plan_id): + return self.get_resource_item(plan_id) + + ########################################################################### + # PATCH ITEM API METHODS + ########################################################################### + + def patch_plan(self, plan_id, patch_data, access_token=None): + return self.patch_resource_item( + plan_id, patch_data, access_token=access_token + ) diff --git a/openprocurement_client/sync.py b/openprocurement_client/resources/sync.py similarity index 57% rename from openprocurement_client/sync.py rename to openprocurement_client/resources/sync.py index 15b3905..3f4997d 100644 --- a/openprocurement_client/sync.py +++ b/openprocurement_client/resources/sync.py @@ -2,16 +2,12 @@ monkey.patch_all() import logging -from .client import TendersClientSync -from time import time +from openprocurement_client.clients import APIResourceClientSync +from openprocurement_client.utils import get_response + from gevent import spawn, sleep, idle from gevent.queue import Queue, Empty -from requests.exceptions import ConnectionError -from openprocurement_client.exceptions import ( - RequestFailed, - PreconditionFailed, - ResourceNotFound -) + DEFAULT_RETRIEVERS_PARAMS = { 'down_requests_sleep': 5, @@ -27,67 +23,7 @@ DEFAULT_API_EXTRA_PARAMS = { 'opt_fields': 'status', 'mode': '_all_'} -logger = logging.getLogger(__name__) - - -def get_response(client, params): - response_fail = True - sleep_interval = 0.2 - while response_fail: - try: - start = time() - response = client.sync_tenders(params) - end = time() - start - logger.debug( - 'Request duration {} sec'.format(end), - extra={'FEEDER_REQUEST_DURATION': end * 1000}) - response_fail = False - except PreconditionFailed as e: - logger.error('PreconditionFailed: {}'.format(e.message), - extra={'MESSAGE_ID': 'precondition_failed'}) - continue - except ConnectionError as e: - logger.error('ConnectionError: {}'.format(e.message), - extra={'MESSAGE_ID': 'connection_error'}) - if sleep_interval > 300: - raise e - sleep_interval = sleep_interval * 2 - logger.debug( - 'Client sleeping after ConnectionError {} sec.'.format( - sleep_interval)) - sleep(sleep_interval) - continue - except RequestFailed as e: - logger.error('Request failed. Status code: {}'.format( - e.status_code), extra={'MESSAGE_ID': 'request_failed'}) - if e.status_code == 429: - if sleep_interval > 120: - raise e - logger.debug( - 'Client sleeping after RequestFailed {} sec.'.format( - sleep_interval)) - sleep_interval = sleep_interval * 2 - sleep(sleep_interval) - continue - except ResourceNotFound as e: - logger.error('Resource not found: {}'.format(e.message), - extra={'MESSAGE_ID': 'resource_not_found'}) - logger.debug('Clear offset and client cookies.') - client.session.cookies.clear() - del params['offset'] - continue - except Exception as e: - logger.error('Exception: {}'.format(e.message), - extra={'MESSAGE_ID': 'exceptions'}) - if sleep_interval > 300: - raise e - sleep_interval = sleep_interval * 2 - logger.debug( - 'Client sleeping after Exception: {}, {} sec.'.format( - e.message, sleep_interval)) - sleep(sleep_interval) - continue - return response +LOGGER = logging.getLogger(__name__) class ResourceFeeder(object): @@ -113,24 +49,25 @@ def init_api_clients(self): self.backward_params.update(self.extra_params) self.forward_params = {'feed': 'changes'} self.forward_params.update(self.extra_params) - self.forward_client = TendersClientSync( + self.forward_client = APIResourceClientSync( self.key, resource=self.resource, host_url=self.host, api_version=self.version) - self.backward_client = TendersClientSync( + self.backward_client = APIResourceClientSync( self.key, resource=self.resource, host_url=self.host, api_version=self.version) self.cookies = self.forward_client.session.cookies =\ self.backward_client.session.cookies def handle_response_data(self, data): - for tender in data: + for resource_item in data: # self.idle() - self.queue.put(tender) + self.queue.put(resource_item) def start_sync(self): # self.init_api_clients() - response = self.backward_client.sync_tenders(self.backward_params) + response = self.backward_client.sync_resource_items( + self.backward_params) self.handle_response_data(response.data) @@ -145,7 +82,7 @@ def restart_sync(self): Restart retrieving from Openprocurement API. """ - logger.info('Restart workers') + LOGGER.info('Restart workers') self.forward_worker.kill() self.backward_worker.kill() self.init_api_clients() @@ -156,14 +93,18 @@ def get_resource_items(self): Prepare iterator for retrieving from Openprocurement API. :param: - host (str): Url of Openprocurement API. Defaults is DEFAULT_API_HOST - version (str): Verion of Openprocurement API. Defaults is DEFAULT_API_VERSION - key(str): Access key of broker in Openprocurement API. Defaults is DEFAULT_API_KEY + host (str): Url of Openprocurement API. + Defaults is DEFAULT_API_HOST + version (str): Verion of Openprocurement API. + Defaults is DEFAULT_API_VERSION + key(str): Access key of broker in Openprocurement API. + Defaults is DEFAULT_API_KEY (Empty string) extra_params(dict): Extra params of query :returns: - iterator of tender_object (Munch): object derived from the list of tenders + iterator of tender_object (Munch): object derived from the + list of tenders """ self.init_api_clients() @@ -172,7 +113,7 @@ def get_resource_items(self): while True: if check_down_worker and self.backward_worker.ready(): if self.backward_worker.value == 0: - logger.info('Stop check backward worker') + LOGGER.info('Stop check backward worker') check_down_worker = False else: self.restart_sync() @@ -192,14 +133,18 @@ def feeder(self): Prepare iterator for retrieving from Openprocurement API. :param: - host (str): Url of Openprocurement API. Defaults is DEFAULT_API_HOST - version (str): Verion of Openprocurement API. Defaults is DEFAULT_API_VERSION - key(str): Access key of broker in Openprocurement API. Defaults is DEFAULT_API_KEY + host (str): Url of Openprocurement API. + Defaults is DEFAULT_API_HOST + version (str): Verion of Openprocurement API. + Defaults is DEFAULT_API_VERSION + key(str): Access key of broker in Openprocurement API. + Defaults is DEFAULT_API_KEY (Empty string) extra_params(dict): Extra params of query :returns: - iterator of tender_object (Munch): object derived from the list of tenders + iterator of tender_object (Munch): object derived from the + list of tenders """ self.init_api_clients() @@ -208,7 +153,7 @@ def feeder(self): while 1: if check_down_worker and self.backward_worker.ready(): if self.backward_worker.value == 0: - logger.info('Stop check backward worker') + LOGGER.info('Stop check backward worker') check_down_worker = False else: self.restart_sync() @@ -216,7 +161,7 @@ def feeder(self): if self.forward_worker.ready(): self.restart_sync() check_down_worker = True - logger.debug('Feeder queue size {} items'.format( + LOGGER.debug('Feeder queue size {} items'.format( self.queue.qsize()), extra={'FEEDER_QUEUE_SIZE': self.queue.qsize()}) sleep(2) @@ -226,36 +171,36 @@ def run_feeder(self): return self.queue def retriever_backward(self): - logger.info('Backward: Start worker') + LOGGER.info('Backward: Start worker') response = get_response(self.backward_client, self.backward_params) - logger.debug('Backward response length {} items'.format( + LOGGER.debug('Backward response length {} items'.format( len(response.data)), extra={'BACKWARD_RESPONSE_LENGTH': len(response.data)}) if self.cookies != self.backward_client.session.cookies: raise Exception('LB Server mismatch') while response.data: - logger.debug('Backward: Start process data.') + LOGGER.debug('Backward: Start process data.') self.handle_response_data(response.data) self.backward_params['offset'] = response.next_page.offset self.log_retriever_state( 'Backward', self.backward_client, self.backward_params) - logger.debug('Backward: Start process request.') + LOGGER.debug('Backward: Start process request.') response = get_response(self.backward_client, self.backward_params) - logger.debug('Backward response length {} items'.format( + LOGGER.debug('Backward response length {} items'.format( len(response.data)), extra={'BACKWARD_RESPONSE_LENGTH': len(response.data)}) if self.cookies != self.backward_client.session.cookies: raise Exception('LB Server mismatch') - logger.info('Backward: pause between requests {} sec.'.format( + LOGGER.info('Backward: pause between requests {} sec.'.format( self.retrievers_params.get('down_requests_sleep', 5))) sleep(self.retrievers_params.get('down_requests_sleep', 5)) - logger.info('Backward: finished') + LOGGER.info('Backward: finished') return 0 def retriever_forward(self): - logger.info('Forward: Start worker') + LOGGER.info('Forward: Start worker') response = get_response(self.forward_client, self.forward_params) - logger.debug('Forward response length {} items'.format( + LOGGER.debug('Forward response length {} items'.format( len(response.data)), extra={'FORWARD_RESPONSE_LENGTH': len(response.data)}) if self.cookies != self.forward_client.session.cookies: @@ -268,18 +213,18 @@ def retriever_forward(self): 'Forward', self.forward_client, self.forward_params) response = get_response(self.forward_client, self.forward_params) - logger.debug('Forward response length {} items'.format( + LOGGER.debug('Forward response length {} items'.format( len(response.data)), extra={'FORWARD_RESPONSE_LENGTH': len(response.data)}) if self.cookies != self.forward_client.session.cookies: raise Exception('LB Server mismatch') if len(response.data) != 0: - logger.info( + LOGGER.info( 'Forward: pause between requests {} sec.'.format( self.retrievers_params.get('up_requests_sleep', 5.0))) sleep(self.retrievers_params.get('up_requests_sleep', 5.0)) - logger.info('Forward: pause after empty response {} sec.'.format( + LOGGER.info('Forward: pause after empty response {} sec.'.format( self.retrievers_params.get('up_wait_sleep', 30.0)), extra={'FORWARD_WAIT_SLEEP': self.retrievers_params.get('up_wait_sleep', 30.0)}) @@ -288,7 +233,7 @@ def retriever_forward(self): self.log_retriever_state( 'Forward', self.forward_client, self.forward_params) response = get_response(self.forward_client, self.forward_params) - logger.debug('Forward response length {} items'.format( + LOGGER.debug('Forward response length {} items'.format( len(response.data)), extra={'FORWARD_RESPONSE_LENGTH': len(response.data)}) if self.adaptive: @@ -305,52 +250,12 @@ def retriever_forward(self): return 1 def log_retriever_state(self, name, client, params): - logger.debug('{}: offset {}'.format(name, params.get('offset', ''))) - logger.debug('{}: AWSELB {}'.format( + LOGGER.debug('{}: offset {}'.format(name, params.get('offset', ''))) + LOGGER.debug('{}: AWSELB {}'.format( name, client.session.cookies.get('AWSELB', ' ') )) - logger.debug('{}: SERVER_ID {}'.format( + LOGGER.debug('{}: SERVER_ID {}'.format( name, client.session.cookies.get('SERVER_ID', '') )) - logger.debug('{}: limit {}'.format(name, params.get('limit', ''))) - - -def get_resource_items(host=DEFAULT_API_HOST, version=DEFAULT_API_VERSION, - key=DEFAULT_API_KEY, - extra_params=DEFAULT_API_EXTRA_PARAMS, - retrievers_params=DEFAULT_RETRIEVERS_PARAMS, - resource='tenders'): - """ - Prepare iterator for retrieving from Openprocurement API. - - :param: - host (str): Url of Openprocurement API. Defaults is DEFAULT_API_HOST - version (str): Verion of Openprocurement API. Defaults is DEFAULT_API_VERSION - key(str): Access key of broker in Openprocurement API. Defaults is DEFAULT_API_KEY - (Empty string) - extra_params(dict): Extra params of query - - :returns: - iterator of tender_object (Munch): object derived from the list of tenders - - """ - feeder = ResourceFeeder( - host=host, version=version, - key=key, extra_params=extra_params, - retrievers_params=retrievers_params, resource=resource - ) - return feeder.get_resource_items() - - -def get_tenders(host=DEFAULT_API_HOST, version=DEFAULT_API_VERSION, - key=DEFAULT_API_KEY, extra_params=DEFAULT_API_EXTRA_PARAMS, - retrievers_params=DEFAULT_RETRIEVERS_PARAMS): - return get_resource_items(host=host, version=version, key=key, - resource='tenders', extra_params=extra_params, - retrievers_params=retrievers_params) - - -if __name__ == '__main__': - for tender_item in get_tenders(): - print('Tender {0[id]}'.format(tender_item)) + LOGGER.debug('{}: limit {}'.format(name, params.get('limit', ''))) diff --git a/openprocurement_client/resources/tenders.py b/openprocurement_client/resources/tenders.py new file mode 100644 index 0000000..a2f6cf6 --- /dev/null +++ b/openprocurement_client/resources/tenders.py @@ -0,0 +1,312 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import ( + APIResourceClient, APIResourceClientSync +) +from openprocurement_client.constants import ( + AWARDS, BIDS, CANCELLATIONS, COMPLAINTS, CONTRACTS, DOCUMENTS, + LOTS, QUALIFICATIONS, QUESTIONS +) +from retrying import retry + + +class TendersClient(APIResourceClient): + """client for tenders""" + + def __init__(self, *args, **kwargs): + super(TendersClient, self).__init__(*args, **kwargs) + + ########################################################################### + # CREATE ITEM API METHODS + ########################################################################### + + def create_tender(self, tender): + return self.create_resource_item(tender) + + def create_question(self, tender_id, question): + return self.create_resource_item_subitem( + tender_id, question, QUESTIONS + ) + + def create_bid(self, tender_id, bid): + return self.create_resource_item_subitem(tender_id, bid, BIDS) + + def create_lot(self, tender_id, lot): + return self.create_resource_item_subitem(tender_id, lot, LOTS) + + def create_award(self, tender_id, award): + return self.create_resource_item_subitem(tender_id, award, AWARDS) + + def create_cancellation(self, tender_id, cancellation): + return self.create_resource_item_subitem( + tender_id, cancellation, CANCELLATIONS + ) + + def create_complaint(self, tender_id, complaint): + return self.create_resource_item_subitem( + tender_id, complaint, COMPLAINTS + ) + + def create_award_complaint(self, tender_id, complaint, award_id): + depth_path = '{}/{}'.format(AWARDS, award_id) + return self.create_resource_item_subitem( + tender_id, complaint, COMPLAINTS, depth_path=depth_path + ) + + def create_thin_document(self, tender_id, document_data): + return self.create_resource_item_subitem( + tender_id, document_data, DOCUMENTS + ) + + ########################################################################### + # GET ITEMS LIST API METHODS + ########################################################################### + + @retry(stop_max_attempt_number=5) + def get_tenders(self, params=None, feed='changes'): + return self.get_resource_items(params=params, feed=feed) + + def get_latest_tenders(self, date): + return self.get_latest_resource_items(date) + + def get_questions(self, tender_id): + return self.get_resource_item_subitem(tender_id, QUESTIONS) + + def get_documents(self, tender_id): + return self.get_resource_item_subitem(tender_id, DOCUMENTS) + + def get_awards_documents(self, tender_id, award_id): + return self.get_resource_item_subitem( + tender_id, DOCUMENTS, depth_path='{}/{}'.format(AWARDS, award_id) + ) + + def get_qualification_documents(self, tender_id, qualification_id): + return self.get_resource_item_subitem( + tender_id, DOCUMENTS, + depth_path='{}/{}'.format(QUALIFICATIONS, qualification_id) + ) + + def get_awards(self, tender_id): + return self.get_resource_item_subitem(tender_id, AWARDS) + + def get_lots(self, tender_id): + return self.get_resource_item_subitem(tender_id, LOTS) + + ########################################################################### + # GET ITEM API METHODS + ########################################################################### + + def get_tender(self, tender_id): + return self.get_resource_item(tender_id) + + def get_question(self, tender_id, question_id): + return self.get_resource_item_subitem( + tender_id, question_id, depth_path=QUESTIONS + ) + + def get_bid(self, tender_id, bid_id, access_token=None): + return self.get_resource_item_subitem( + tender_id, bid_id, depth_path=BIDS, access_token=access_token + ) + + def get_lot(self, tender_id, lot_id): + return self.get_resource_item_subitem( + tender_id, lot_id, depth_path=LOTS + ) + + ########################################################################### + # PATCH ITEM API METHODS + ########################################################################### + + def patch_tender(self, tender_id, patch_data): + return self.patch_resource_item(tender_id, patch_data) + + def patch_question(self, tender_id, question, question_id): + return self.patch_resource_item_subitem( + tender_id, question, QUESTIONS, subitem_id=question_id + ) + + def patch_bid(self, tender_id, bid, bid_id): + return self.patch_resource_item_subitem( + tender_id, bid, BIDS, subitem_id=bid_id + ) + + def patch_bid_document(self, tender_id, document_data, bid_id, + document_id): + depth_path = '{}/{}'.format(BIDS, bid_id) + return self.patch_resource_item_subitem( + tender_id, document_data, DOCUMENTS, subitem_id=document_id, + depth_path=depth_path + ) + + def patch_award(self, tender_id, award, award_id): + return self.patch_resource_item_subitem( + tender_id, award, AWARDS, subitem_id=award_id + ) + + def patch_award_document(self, tender_id, document_data, award_id, + document_id): + depth_path = '{}/{}'.format(AWARDS, award_id) + return self.patch_resource_item_subitem( + tender_id, document_data, DOCUMENTS, subitem_id=document_id, + depth_path=depth_path + ) + + def patch_cancellation(self, tender_id, cancellation, cancellation_id): + return self.patch_resource_item_subitem( + tender_id, cancellation, CANCELLATIONS, + subitem_id=cancellation_id + ) + + def patch_cancellation_document(self, tender_id, cancellation, + cancellation_id, cancellation_doc_id): + depth_path = '{}/{}'.format(CANCELLATIONS, cancellation_id) + return self.patch_resource_item_subitem( + tender_id, cancellation, DOCUMENTS, + subitem_id=cancellation_doc_id, depth_path=depth_path + ) + + def patch_complaint(self, tender_id, complaint, complaint_id): + return self.patch_resource_item_subitem( + tender_id, complaint, COMPLAINTS, subitem_id=complaint_id + ) + + def patch_award_complaint(self, tender_id, complaint, award_id, + complaint_id): + return self.patch_resource_item_subitem( + tender_id, complaint, COMPLAINTS, subitem_id=complaint_id, + depth_path='{}/{}'.format(AWARDS, award_id) + ) + + def patch_lot(self, tender_id, lot, lot_id): + return self.patch_resource_item_subitem( + tender_id, lot, LOTS, subitem_id=lot_id + ) + + def patch_qualification(self, tender_id, qualification, qualification_id): + return self.patch_resource_item_subitem( + tender_id, qualification, QUALIFICATIONS, + subitem_id=qualification_id + ) + + def patch_contract(self, tender_id, contract, contract_id): + return self.patch_resource_item_subitem( + tender_id, contract, CONTRACTS, subitem_id=contract_id + ) + + def patch_contract_document(self, tender_id, document_data, contract_id, + document_id): + return self.patch_resource_item_subitem( + tender_id, document_data, DOCUMENTS, subitem_id=document_id, + depth_path='{}/{}'.format(CONTRACTS, contract_id) + ) + + ########################################################################### + # UPLOAD FILE API METHODS + ########################################################################### + + def upload_bid_document(self, file_, tender_id, bid_id, + doc_registration=True, access_token=None, + doc_type=DOCUMENTS): + depth_path = '{}/{}'.format(BIDS, bid_id) + return self.upload_document( + file_, tender_id, doc_registration, + access_token, depth_path, doc_type + ) + + def upload_cancellation_document(self, file_, tender_id, cancellation_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(CANCELLATIONS, cancellation_id) + return self.upload_document(file_, tender_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_complaint_document(self, file_, tender_id, complaint_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(COMPLAINTS, complaint_id) + return self.upload_document(file_, tender_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_award_complaint_document(self, file_, tender_id, award_id, + complaint_id, + doc_registration=True, + access_token=None): + depth_path = '{}/{}/{}/{}'.format(AWARDS, award_id, + COMPLAINTS, complaint_id) + return self.upload_document(file_, tender_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_qualification_document(self, file_, tender_id, qualification_id, + doc_registration=True, + access_token=None): + depth_path = '{}/{}'.format(QUALIFICATIONS, qualification_id) + return self.upload_document(file_, tender_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_award_document(self, file_, tender_id, award_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(AWARDS, award_id) + return self.upload_document(file_, tender_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_contract_document(self, file_, tender_id, contract_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(CONTRACTS, contract_id) + return self.upload_document(file_, tender_id, doc_registration, + access_token, depth_path) + + ########################################################################### + # UPDATE FILE API METHODS + ########################################################################### + + def update_bid_document(self, file_, tender_id, bid_id, document_id, + doc_registration=True, access_token=None, + doc_type=DOCUMENTS): + depth_path = '{}/{}'.format(BIDS, bid_id) + return self.update_document( + file_, tender_id, document_id, doc_registration, + access_token, depth_path, doc_type + ) + + def update_cancellation_document(self, file_, tender_id, cancellation_id, + document_id, doc_registration=True, + access_token=None): + depth_path = '{}/{}'.format(CANCELLATIONS, cancellation_id) + return self.update_document( + file_, tender_id, document_id, doc_registration, + access_token, depth_path + ) + + ########################################################################### + # DELETE ITEMS LIST API METHODS + ########################################################################### + + def delete_bid(self, tender_id, bid_id, access_token=None): + return self.delete_resource_item_subitem( + tender_id, BIDS, bid_id, access_token=access_token + ) + + def delete_lot(self, tender_id, lot_id, access_token=None): + return self.delete_resource_item_subitem( + tender_id, LOTS, lot_id, access_token=access_token + ) + ########################################################################### + + +class Client(TendersClient): + """client for tenders for backward compatibility""" + + +class TendersClientSync(APIResourceClientSync): + + sync_tenders = APIResourceClientSync.sync_resource_items + + get_tender = APIResourceClientSync.get_resource_item diff --git a/openprocurement_client/templates.py b/openprocurement_client/templates.py new file mode 100644 index 0000000..758d45b --- /dev/null +++ b/openprocurement_client/templates.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +import uuid +from .exceptions import RequestFailed, http_exceptions_dict +from requests import Session +from requests.auth import HTTPBasicAuth as BasicAuth + + +class APITemplateClient(object): + """Base class template for API""" + + def __init__(self, login_pass=None, headers=None, user_agent=None): + self.headers = headers or {} + self.session = Session() + if login_pass is not None: + self.session.auth = BasicAuth(*login_pass) + + if user_agent is None: + self.session.headers['User-Agent'] = 'op.client/{}'.format( + uuid.uuid4().hex) + else: + self.session.headers['User-Agent'] = user_agent + + def request(self, method, path=None, payload=None, json=None, + headers=None, params_dict=None, file_=None): + _headers = self.headers.copy() + _headers.update(headers or {}) + if file_: + _headers.pop('Content-Type', None) + + response = self.session.request( + method, path, data=payload, json=json, headers=_headers, + params=params_dict, files=file_ + ) + + if response.status_code >= 400: + raise http_exceptions_dict\ + .get(response.status_code, RequestFailed)(response) + + return response diff --git a/openprocurement_client/tests/__init__.py b/openprocurement_client/tests/__init__.py index 813cf0d..e69de29 100644 --- a/openprocurement_client/tests/__init__.py +++ b/openprocurement_client/tests/__init__.py @@ -1 +0,0 @@ -# coding: utf8 \ No newline at end of file diff --git a/openprocurement_client/tests/_server.py b/openprocurement_client/tests/_server.py old mode 100755 new mode 100644 index 9ba746b..0c202e4 --- a/openprocurement_client/tests/_server.py +++ b/openprocurement_client/tests/_server.py @@ -1,7 +1,7 @@ from bottle import request, response, redirect, static_file from munch import munchify from simplejson import dumps, load -from openprocurement_client.document_service_client \ +from openprocurement_client.resources.document_service \ import DocumentServiceClient from openprocurement_client.tests.data_dict import ( TEST_TENDER_KEYS, @@ -64,13 +64,14 @@ def get_doc_title_from_request(req): return doc_title -### Base routes -# +# Base routes + def spore(): - response.set_cookie("SERVER_ID", ("a7afc9b1fc79e640f2487ba48243ca071c07a823d27" - "8cf9b7adf0fae467a524747e3c6c6973262130fac2b" - "96a11693fa8bd38623e4daee121f60b4301aef012c")) + response.set_cookie( + "SERVER_ID", + ("a7afc9b1fc79e640f2487ba48243ca071c07a823d278cf9b7adf0fae467a524747" + "e3c6c6973262130fac2b96a11693fa8bd38623e4daee121f60b4301aef012c")) def offset_error(resource_name): @@ -86,9 +87,7 @@ def resource_page_get(resource_name): return dumps(resources) -### Tender operations -# - +# Tender operations def resource_create(): response.status = 201 @@ -106,12 +105,11 @@ def resource_patch(resource_name, resource_id): resource = resource_partition(resource_id, resource_name) if not resource: return location_error(resource_name) - resource.update(request.json['data']) - return dumps({'data': resource}) + resource['data'].update(request.json['data']) + return dumps({'data': resource['data']}) -### Subpage operations -# +# Subpage operations def tender_subpage(tender_id, subpage_name): subpage = resource_partition(tender_id, part=subpage_name) @@ -178,8 +176,7 @@ def patch_credentials(resource_name, resource_id): {'token': RESOURCE_DICT[resource_name]['data']['new_token']} return resource -### Document and file operations -# +# Document and file operations def tender_document_create(tender_id): @@ -190,21 +187,23 @@ def tender_document_create(tender_id): return dumps({"data": document}) -def tender_subpage_document_create(tender_id, subpage_name, subpage_id, document_type): +def tender_subpage_document_create(tender_id, subpage_name, subpage_id, + document_type): response.status = 201 subpage = resource_partition(tender_id, part=subpage_name) if not subpage: return location_error("tender") for unit in subpage: if unit['id'] == subpage_id: - document= unit["documents"][0] + document = unit["documents"][0] document.title = get_doc_title_from_request(request) document.id = TEST_TENDER_KEYS.new_document_id return dumps({"data": document}) return location_error(subpage_name) -def tender_subpage_document_update(tender_id, subpage_name, subpage_id, document_type, document_id): +def tender_subpage_document_update(tender_id, subpage_name, subpage_id, + document_type, document_id): response.status = 200 subpage = resource_partition(tender_id, part=subpage_name) if not subpage: @@ -218,7 +217,8 @@ def tender_subpage_document_update(tender_id, subpage_name, subpage_id, document return location_error(subpage_name) -def tender_subpage_document_patch(tender_id, subpage_name, subpage_id, document_type, document_id): +def tender_subpage_document_patch(tender_id, subpage_name, subpage_id, + document_type, document_id): response.status = 200 subpage = resource_partition(tender_id, part=subpage_name) if not subpage: @@ -235,11 +235,10 @@ def tender_subpage_document_patch(tender_id, subpage_name, subpage_id, document_ def get_file(filename): redirect("/download/" + filename, code=302) + def download_file(filename): return static_file(filename, root=ROOT, download=True) -#### - def resource_partition(resource_id, resource_name='tender', part='all'): try: @@ -255,10 +254,20 @@ def resource_partition(resource_id, resource_name='tender', part='all'): def location_error(name): - return dumps({"status": "error", "errors": [{"location": "url", "name": name + '_id', "description": "Not Found"}]}) + return dumps( + { + "status": "error", + "errors": [ + { + "location": "url", + "name": name + '_id', + "description": "Not Found" + } + ] + } + ) -### Plan operations -# +# Plan operations def plan_patch(plan_id): @@ -273,15 +282,14 @@ def plan_partition(plan_id, part="plan"): try: with open(ROOT + 'plan_' + plan_id + '.json') as json: plan = load(json) - if part=="plan": + if part == "plan": return plan else: return munchify(plan['data'][part]) except (KeyError, IOError): return [] -### Contract operations -# +# Contract operations def contract_document_create(contract_id): @@ -300,15 +308,19 @@ def contract_change_patch(contract_id, change_id): change.data.rationale = TEST_CONTRACT_KEYS.patch_change_rationale return dumps(change) -#### Routes +# Routes routes_dict = { "spore": (SPORE_PATH, 'HEAD', spore), - "offset_error": (API_PATH.format(''), 'GET', offset_error), - "tenders": (API_PATH.format(''), 'GET', resource_page_get), + "offset_error": (API_PATH.format(''), + 'GET', offset_error), + "tenders": (API_PATH.format(''), + 'GET', resource_page_get), "tender_create": (TENDERS_PATH, 'POST', resource_create), - "tender": (API_PATH.format('') + '/', 'GET', resource_page), + "tender": (API_PATH.format( + '') + '/', + 'GET', resource_page), "tender_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), "tender_document_create": (TENDERS_PATH + "//documents", 'POST', tender_document_create), "tender_subpage": (TENDERS_PATH + "//", 'GET', tender_subpage), diff --git a/openprocurement_client/tests/main.py b/openprocurement_client/tests/main.py index 9655dd0..a6e892f 100644 --- a/openprocurement_client/tests/main.py +++ b/openprocurement_client/tests/main.py @@ -1,7 +1,7 @@ import unittest from openprocurement_client.tests import ( - tests, + tests_resources, tests_sync, test_registry_client ) @@ -9,9 +9,9 @@ def suite(): suite = unittest.TestSuite() - suite.addTest(tests.suite()) suite.addTest(tests_sync.suite()) suite.addTest(test_registry_client.suite()) + suite.addTest(tests_resources.suite()) return suite diff --git a/openprocurement_client/tests/test_registry_client.py b/openprocurement_client/tests/test_registry_client.py index 27355e4..5579718 100644 --- a/openprocurement_client/tests/test_registry_client.py +++ b/openprocurement_client/tests/test_registry_client.py @@ -1,35 +1,30 @@ from __future__ import print_function from gevent import monkey - monkey.patch_all() + +import mock +import sys +import unittest from gevent.pywsgi import WSGIServer from bottle import Bottle -from StringIO import StringIO from collections import Iterable -from simplejson import loads, load +from simplejson import load from munch import munchify -import mock -import sys -import unittest -from openprocurement_client.registry_client import LotsClient, AssetsClient -from openprocurement_client.document_service_client \ - import DocumentServiceClient -from openprocurement_client.exceptions import InvalidResponse, ResourceNotFound +from openprocurement_client.resources.assets import AssetsClient +from openprocurement_client.resources.lots import LotsClient +from openprocurement_client.exceptions import InvalidResponse from openprocurement_client.tests.data_dict import ( TEST_ASSET_KEYS, TEST_LOT_KEYS, - TEST_TENDER_KEYS_LIMITED, - TEST_PLAN_KEYS, - TEST_CONTRACT_KEYS ) from openprocurement_client.tests._server import \ - API_KEY, API_VERSION, AUTH_DS_FAKE, DS_HOST_URL, DS_PORT, \ - HOST_URL, location_error, PORT, ROOT, setup_routing, setup_routing_ds, \ - resource_partition, resource_filter + API_VERSION, AUTH_DS_FAKE, DS_HOST_URL, DS_PORT, \ + HOST_URL, PORT, ROOT, setup_routing, setup_routing_ds, \ + resource_filter class BaseTestClass(unittest.TestCase): - def setting_up(self, client, resource=None): + def setting_up(self, client): self.app = Bottle() self.app.router.add_filter('resource_filter', resource_filter) setup_routing(self.app) @@ -40,13 +35,13 @@ def setting_up(self, client, resource=None): print(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], file=sys.stderr) raise error - ds_client = getattr(self, 'ds_client', None) + + ds_config = { + 'host_url': DS_HOST_URL, + 'auth_ds': AUTH_DS_FAKE + } self.client = client('', host_url=HOST_URL, api_version=API_VERSION, - ds_client=ds_client) - if resource: - self.client = client( - '', host_url=HOST_URL, api_version=API_VERSION, - ds_client=ds_client, resource=resource) + ds_config=ds_config) @classmethod def setting_up_ds(cls): @@ -60,14 +55,6 @@ def setting_up_ds(cls): file=sys.stderr) raise error - cls.ds_client = DocumentServiceClient(host_url=DS_HOST_URL, - auth_ds=AUTH_DS_FAKE) - # to test units performing file operations outside the DS uncomment - # following lines: - # import logging - # logging.basicConfig() - # cls.ds_client = None - setup_routing_ds(cls.app_ds) @classmethod @@ -81,7 +68,7 @@ def tearDownClass(cls): class AssetsRegistryTestCase(BaseTestClass): def setUp(self): - self.setting_up(client=AssetsClient, resource='assets') + self.setting_up(client=AssetsClient) with open(ROOT + 'assets.json') as assets: self.assets = munchify(load(assets)) @@ -98,10 +85,10 @@ def test_get_assets(self): self.assertIsInstance(assets, Iterable) self.assertEqual(assets, self.assets.data) - @mock.patch('openprocurement_client.registry_client.AssetsClient.request') + @mock.patch('openprocurement_client.resources.assets.AssetsClient.request') def test_get_assets_failed(self, mock_request): mock_request.return_value = munchify({'status_code': 404}) - with self.assertRaises(InvalidResponse) as e: + with self.assertRaises(InvalidResponse): self.client.get_assets(params={'offset': 'offset_value'}) def test_get_asset(self): @@ -111,17 +98,18 @@ def test_get_asset(self): def test_patch_asset(self): setup_routing(self.app, routes=["asset_patch"]) - self.asset.data.description = 'test_patch_asset' - - patched_asset = self.client.patch_resource_item(self.asset) + asset_id = self.asset.data.id + patch_data = {'data': {'description': 'test_patch_asset'}} + patched_asset = self.client.patch_resource_item(asset_id, + patch_data) self.assertEqual(patched_asset.data.id, self.asset.data.id) self.assertEqual(patched_asset.data.description, - self.asset.data.description) + patch_data['data']['description']) class LotsRegistryTestCase(BaseTestClass): def setUp(self): - self.setting_up(client=LotsClient, resource='lots') + self.setting_up(client=LotsClient) with open(ROOT + 'lots.json') as lots: self.lots = munchify(load(lots)) @@ -137,10 +125,10 @@ def test_get_lots(self): self.assertIsInstance(lots, Iterable) self.assertEqual(lots, self.lots.data) - @mock.patch('openprocurement_client.registry_client.LotsClient.request') + @mock.patch('openprocurement_client.resources.lots.LotsClient.request') def test_get_lots_failed(self, mock_request): mock_request.return_value = munchify({'status_code': 404}) - with self.assertRaises(InvalidResponse) as e: + with self.assertRaises(InvalidResponse): self.client.get_lots(params={'offset': 'offset_value'}) def test_get_lot(self): @@ -150,12 +138,12 @@ def test_get_lot(self): def test_patch_lot(self): setup_routing(self.app, routes=["lot_patch"]) - self.lot.data.description = 'test_patch_lot' - - patched_lot = self.client.patch_resource_item(self.lot) - self.assertEqual(patched_lot.data.id, self.lot.data.id) + lot_id = self.lot.data.id + patch_data = {'data': {'description': 'test_patch_lot'}} + patched_lot = self.client.patch_resource_item(lot_id, patch_data) + self.assertEqual(patched_lot.data.id, lot_id) self.assertEqual(patched_lot.data.description, - self.lot.data.description) + patch_data['data']['description']) def suite(): diff --git a/openprocurement_client/tests/tests.py b/openprocurement_client/tests/tests_resources.py old mode 100755 new mode 100644 similarity index 63% rename from openprocurement_client/tests/tests.py rename to openprocurement_client/tests/tests_resources.py index 65d96e1..f789f3b --- a/openprocurement_client/tests/tests.py +++ b/openprocurement_client/tests/tests_resources.py @@ -1,5 +1,7 @@ from __future__ import print_function -from gevent import monkey; monkey.patch_all() +from gevent import monkey +monkey.patch_all() + from gevent.pywsgi import WSGIServer from bottle import Bottle from StringIO import StringIO @@ -9,17 +11,20 @@ import mock import sys import unittest -from openprocurement_client.client import TendersClient, TendersClientSync -from openprocurement_client.contract import ContractingClient -from openprocurement_client.document_service_client \ - import DocumentServiceClient +from openprocurement_client.constants import ( + ELIGIBILITY_DOCUMENTS, FINANCIAL_DOCUMENTS, QUALIFICATION_DOCUMENTS +) from openprocurement_client.exceptions import InvalidResponse, ResourceNotFound -from openprocurement_client.plan import PlansClient +from openprocurement_client.resources.contracts import ContractingClient +from openprocurement_client.resources.plans import PlansClient +from openprocurement_client.resources.tenders import ( + TendersClient, TendersClientSync +) from openprocurement_client.tests.data_dict import TEST_TENDER_KEYS, \ TEST_TENDER_KEYS_LIMITED, TEST_PLAN_KEYS, TEST_CONTRACT_KEYS from openprocurement_client.tests._server import \ API_KEY, API_VERSION, AUTH_DS_FAKE, DS_HOST_URL, DS_PORT, \ - HOST_URL, location_error, PORT, ROOT, setup_routing, setup_routing_ds, \ + HOST_URL, location_error, PORT, ROOT, setup_routing, setup_routing_ds, \ resource_partition, resource_filter @@ -44,9 +49,12 @@ def setting_up(self, client): print(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], file=sys.stderr) raise error - ds_client = getattr(self, 'ds_client', None) + ds_config = { + 'host_url': DS_HOST_URL, + 'auth_ds': AUTH_DS_FAKE + } self.client = client('', host_url=HOST_URL, api_version=API_VERSION, - ds_client=ds_client) + ds_config=ds_config) @classmethod def setting_up_ds(cls): @@ -60,8 +68,6 @@ def setting_up_ds(cls): file=sys.stderr) raise error - cls.ds_client = DocumentServiceClient(host_url=DS_HOST_URL, - auth_ds=AUTH_DS_FAKE) # to test units performing file operations outside the DS uncomment # following lines: # import logging @@ -74,7 +80,6 @@ def setting_up_ds(cls): def setUpClass(cls): cls.setting_up_ds() - @classmethod def tearDownClass(cls): cls.server_ds.stop() @@ -87,7 +92,8 @@ def setUp(self): with open(ROOT + 'tenders.json') as tenders: self.tenders = munchify(load(tenders)) - with open(ROOT + 'tender_' + TEST_TENDER_KEYS.tender_id + '.json') as tender: + with open(ROOT + 'tender_' + TEST_TENDER_KEYS.tender_id + '.json') as\ + tender: self.tender = munchify(load(tender)) def tearDown(self): @@ -101,11 +107,13 @@ def test_get_tenders(self): def test_get_latest_tenders(self): setup_routing(self.app, routes=["tenders"]) - tenders = self.client.get_latest_tenders('2015-11-16T12:00:00.960077+02:00', '') + tenders = self.client.get_latest_tenders( + '2015-11-16T12:00:00.960077+02:00') self.assertIsInstance(tenders, Iterable) self.assertEqual(tenders.data, self.tenders.data) - @mock.patch('openprocurement_client.client.TendersClient.request') + @mock.patch('openprocurement_client.resources.tenders.TendersClient.' + 'request') def test_get_tenders_failed(self, mock_request): mock_request.return_value = munchify({'status_code': 404}) self.client.params['offset'] = 'offset_value' @@ -151,17 +159,18 @@ def test_get_plans(self): def test_get_latest_plans(self): setup_routing(self.app, routes=["plans"]) - plans = self.client.get_latest_plans('2015-11-16T12:00:00.960077+02:00') + plans = self.client.get_latest_plans( + '2015-11-16T12:00:00.960077+02:00') self.assertIsInstance(plans, Iterable) self.assertEqual(plans.data, self.plans.data) - @mock.patch('openprocurement_client.plan.PlansClient.request') + @mock.patch('openprocurement_client.resources.plans.PlansClient.request') def test_get_plans_failed(self, mock_request): mock_request.return_value = munchify({'status_code': 412}) self.client.params['offset'] = 'offset_value' - with self.assertRaises(KeyError) as e: + with self.assertRaises(InvalidResponse) as e: self.client.get_plans() - self.assertEqual(e.exception.message, 'offset') + self.assertEqual(e.exception.message, 'Not described error yet.') def test_get_plan(self): setup_routing(self.app, routes=["plan"]) @@ -181,10 +190,11 @@ def test_offset_error(self): def test_patch_plan(self): setup_routing(self.app, routes=['plan_patch']) - self.plan.data.description = 'test_patch_plan' - patched_tender = self.client.patch_plan(self.plan) + patch_data = {'data': {'description': 'test_patch_plan'}} + patched_tender = self.client.patch_plan(self.plan.data.id, patch_data) self.assertEqual(patched_tender.data.id, self.plan.data.id) - self.assertEqual(patched_tender.data.description, self.plan.data.description) + self.assertEqual(patched_tender.data.description, + patch_data['data']['description']) def test_create_plan(self): setup_routing(self.app, routes=["plan_create"]) @@ -199,7 +209,8 @@ def setUp(self): with open(ROOT + 'tenders.json') as tenders: self.tenders = munchify(load(tenders)) - with open(ROOT + 'tender_' + TEST_TENDER_KEYS.tender_id + '.json') as tender: + with open(ROOT + 'tender_' + TEST_TENDER_KEYS.tender_id + '.json') as \ + tender: self.tender = munchify(load(tender)) def tearDown(self): @@ -217,51 +228,78 @@ class UserTestCase(BaseTestClass): def setUp(self): self.setting_up(client=TendersClient) - with open(ROOT + 'tender_' + TEST_TENDER_KEYS.tender_id + '.json') as tender: + with open(ROOT + 'tender_' + TEST_TENDER_KEYS.tender_id + '.json') \ + as tender: self.tender = munchify(load(tender)) - self.tender.update({'access': {'token': TEST_TENDER_KEYS['token']}}) - with open(ROOT + 'tender_' + TEST_TENDER_KEYS.empty_tender + '.json') as tender: + self.tender.update( + {'access': {'token': TEST_TENDER_KEYS['token']}} + ) + with open(ROOT + 'tender_' + TEST_TENDER_KEYS.empty_tender + '.json') \ + as tender: self.empty_tender = munchify(load(tender)) - with open(ROOT + 'tender_' + TEST_TENDER_KEYS_LIMITED.tender_id + '.json') as tender: + with open( + ROOT + 'tender_' + TEST_TENDER_KEYS_LIMITED.tender_id + '.json') \ + as tender: self.limited_tender = munchify(load(tender)) def tearDown(self): self.server.stop() - ########################################################################### # GET ITEMS LIST TEST ########################################################################### def test_get_questions(self): setup_routing(self.app, routes=["tender_subpage"]) - questions = munchify({'data': self.tender['data'].get('questions', [])}) - self.assertEqual(self.client.get_questions(self.tender), questions) + questions = munchify( + {'data': self.tender['data'].get('questions', [])} + ) + self.assertEqual(self.client.get_questions(self.tender.data.id), + questions) def test_get_documents(self): setup_routing(self.app, routes=["tender_subpage"]) - documents = munchify({'data': self.tender['data'].get('documents', [])}) - self.assertEqual(self.client.get_documents(self.tender), documents) + documents = munchify( + {'data': self.tender['data'].get('documents', [])} + ) + self.assertEqual(self.client.get_documents(self.tender.data.id), + documents) def test_get_awards_documents(self): setup_routing(self.app, routes=["tender_award_documents"]) - documents = munchify({'data': self.tender['data']['awards'][0].get('documents', [])}) - self.assertEqual(self.client.get_awards_documents(self.tender, self.tender['data']['awards'][0]['id']), documents) + documents = munchify({ + 'data': self.tender['data']['awards'][0].get('documents', []) + }) + self.assertEqual( + self.client.get_awards_documents( + self.tender.data.id, self.tender['data']['awards'][0]['id'] + ), + documents + ) def test_get_qualification_documents(self): setup_routing(self.app, routes=["tender_qualification_documents"]) - documents = munchify({'data': self.tender['data']['qualifications'][0].get('documents', [])}) - self.assertEqual(self.client.get_qualification_documents(self.tender, self.tender['data']['qualifications'][0]['id']), documents) + documents = munchify({ + 'data': + self.tender['data']['qualifications'][0].get('documents', []) + }) + self.assertEqual( + self.client.get_qualification_documents( + self.tender.data.id, + self.tender['data']['qualifications'][0]['id'] + ), + documents + ) def test_get_awards(self): setup_routing(self.app, routes=["tender_subpage"]) awards = munchify({'data': self.tender['data'].get('awards', [])}) - self.assertEqual(self.client.get_awards(self.tender), awards) + self.assertEqual(self.client.get_awards(self.tender.data.id), awards) def test_get_lots(self): setup_routing(self.app, routes=["tender_subpage"]) lots = munchify({'data': self.tender['data'].get('lots', [])}) - self.assertEqual(self.client.get_lots(self.tender), lots) + self.assertEqual(self.client.get_lots(self.tender.data.id), lots) ########################################################################### # CREATE ITEM TEST @@ -275,32 +313,48 @@ def test_create_tender(self): def test_create_question(self): setup_routing(self.app, routes=["tender_subpage_item_create"]) question = munchify({'data': 'question'}) - self.assertEqual(self.client.create_question(self.tender, question), question) + self.assertEqual( + self.client.create_question(self.tender.data.id, question), + question + ) def test_create_bid(self): setup_routing(self.app, routes=["tender_subpage_item_create"]) bid = munchify({'data': 'bid'}) - self.assertEqual(self.client.create_bid(self.tender, bid), bid) + self.assertEqual(self.client.create_bid(self.tender.data.id, bid), + bid) def test_create_lot(self): setup_routing(self.app, routes=["tender_subpage_item_create"]) lot = munchify({'data': 'lot'}) - self.assertEqual(self.client.create_lot(self.tender, lot), lot) + self.assertEqual(self.client.create_lot(self.tender.data.id, lot), + lot) def test_create_award(self): setup_routing(self.app, routes=["tender_subpage_item_create"]) award = munchify({'data': 'award'}) - self.assertEqual(self.client.create_award(self.limited_tender, award), award) + self.assertEqual( + self.client.create_award(self.limited_tender.data.id, award), + award) def test_create_cancellation(self): setup_routing(self.app, routes=["tender_subpage_item_create"]) cancellation = munchify({'data': 'cancellation'}) - self.assertEqual(self.client.create_cancellation(self.limited_tender, cancellation), cancellation) + self.assertEqual( + self.client.create_cancellation( + self.limited_tender.data.id, cancellation + ), + cancellation) def test_create_complaint(self): setup_routing(self.app, routes=["tender_subpage_item_create"]) complaint = munchify({'data': 'complaint'}) - self.assertEqual(self.client.create_complaint(self.limited_tender, complaint), complaint) + self.assertEqual( + self.client.create_complaint( + self.limited_tender.data.id, complaint + ), + complaint + ) ########################################################################### # GET ITEM TEST @@ -315,7 +369,7 @@ def test_get_question(self): question_ = munchify({"data": question}) break question = self.client.get_question( - self.tender, question_id=TEST_TENDER_KEYS.question_id + self.tender.data.id, question_id=TEST_TENDER_KEYS.question_id ) self.assertEqual(question, question_) @@ -326,7 +380,8 @@ def test_get_lot(self): if lot['id'] == TEST_TENDER_KEYS.lot_id: lot_ = munchify({"data": lot}) break - lot = self.client.get_lot(self.tender, lot_id=TEST_TENDER_KEYS.lot_id) + lot = self.client.get_lot(self.tender.data.id, + lot_id=TEST_TENDER_KEYS.lot_id) self.assertEqual(lot, lot_) def test_get_bid(self): @@ -336,18 +391,31 @@ def test_get_bid(self): if bid['id'] == TEST_TENDER_KEYS.bid_id: bid_ = munchify({"data": bid}) break - bid = self.client.get_bid(self.tender, bid_id=TEST_TENDER_KEYS.bid_id, access_token=API_KEY) + bid = self.client.get_bid(self.tender.data.id, + bid_id=TEST_TENDER_KEYS.bid_id, + access_token=API_KEY) self.assertEqual(bid, bid_) def test_get_location_error(self): setup_routing(self.app, routes=["tender_subpage_item"]) - self.assertEqual(self.client.get_question(self.empty_tender, question_id=TEST_TENDER_KEYS.question_id), - munchify(loads(location_error('questions')))) - self.assertEqual(self.client.get_lot(self.empty_tender, lot_id=TEST_TENDER_KEYS.lot_id), - munchify(loads(location_error('lots')))) - self.assertEqual(self.client.get_bid(self.empty_tender, bid_id=TEST_TENDER_KEYS.bid_id, access_token=API_KEY), - munchify(loads(location_error('bids')))) - + self.assertEqual( + self.client.get_question( + self.empty_tender.data.id, TEST_TENDER_KEYS.question_id + ), + munchify(loads(location_error('questions'))) + ) + self.assertEqual( + self.client.get_lot( + self.empty_tender.data.id, lot_id=TEST_TENDER_KEYS.lot_id + ), + munchify(loads(location_error('lots'))) + ) + self.assertEqual( + self.client.get_bid( + self.empty_tender.data.id, TEST_TENDER_KEYS.bid_id, API_KEY + ), + munchify(loads(location_error('bids'))) + ) ########################################################################### # PATCH ITEM TEST @@ -355,110 +423,231 @@ def test_get_location_error(self): def test_patch_tender(self): setup_routing(self.app, routes=["tender_patch"]) - self.tender.data.description = 'test_patch_tender' - patched_tender = self.client.patch_tender(self.tender) + patch_data = {'data': {'description': 'test_patch_tender'}} + patched_tender = self.client.patch_tender( + self.tender.data.id, patch_data + ) self.assertEqual(patched_tender.data.id, self.tender.data.id) - self.assertEqual(patched_tender.data.description, self.tender.data.description) + self.assertEqual(patched_tender.data.description, + patch_data['data']['description']) def test_patch_question(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - question = munchify({"data": {"id": TEST_TENDER_KEYS.question_id, "description": "test_patch_question"}}) - patched_question = self.client.patch_question(self.tender, question) + question = munchify({ + "data": { + "id": TEST_TENDER_KEYS.question_id, + "description": "test_patch_question" + } + }) + patched_question = self.client.patch_question( + self.tender.data.id, question, question.data.id + ) self.assertEqual(patched_question.data.id, question.data.id) - self.assertEqual(patched_question.data.description, question.data.description) + self.assertEqual(patched_question.data.description, + question.data.description) def test_patch_bid(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - bid = munchify({"data": {"id": TEST_TENDER_KEYS.bid_id, "description": "test_patch_bid"}}) - patched_bid = self.client.patch_bid(self.tender, bid) + bid = munchify({ + "data": { + "id": TEST_TENDER_KEYS.bid_id, + "description": "test_patch_bid" + } + }) + patched_bid = self.client.patch_bid( + self.tender.data.id, bid, bid.data.id + ) self.assertEqual(patched_bid.data.id, bid.data.id) self.assertEqual(patched_bid.data.description, bid.data.description) def test_patch_award(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - award = munchify({"data": {"id": TEST_TENDER_KEYS.award_id, "description": "test_patch_award"}}) - patched_award =self.client.patch_award(self.tender, award) + award = munchify({ + "data": { + "id": TEST_TENDER_KEYS.award_id, + "description": "test_patch_award" + } + }) + patched_award = self.client.patch_award(self.tender.data.id, award, + award.data.id) self.assertEqual(patched_award.data.id, award.data.id) - self.assertEqual(patched_award.data.description, award.data.description) + self.assertEqual(patched_award.data.description, + award.data.description) def test_patch_cancellation(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - cancellation = munchify({"data": {"id": TEST_TENDER_KEYS_LIMITED.cancellation_id, "description": "test_patch_cancellation"}}) - patched_cancellation =self.client.patch_cancellation(self.limited_tender, cancellation) + cancellation = munchify({ + "data": { + "id": TEST_TENDER_KEYS_LIMITED.cancellation_id, + "description": "test_patch_cancellation" + } + }) + patched_cancellation = self.client.patch_cancellation( + self.limited_tender.data.id, cancellation, cancellation.data.id + ) self.assertEqual(patched_cancellation.data.id, cancellation.data.id) - self.assertEqual(patched_cancellation.data.description, cancellation.data.description) + self.assertEqual(patched_cancellation.data.description, + cancellation.data.description) def test_patch_cancellation_document(self): setup_routing(self.app, routes=["tender_subpage_document_patch"]) - cancellation_document = munchify({"data": {"id": TEST_TENDER_KEYS_LIMITED.cancellation_document_id, "description": "test_patch_cancellation_document"}}) - patched_cancellation_document =self.client.patch_cancellation_document(self.limited_tender, cancellation_document, TEST_TENDER_KEYS_LIMITED.cancellation_id, TEST_TENDER_KEYS_LIMITED.cancellation_document_id) - self.assertEqual(patched_cancellation_document.data.id, cancellation_document.data.id) - self.assertEqual(patched_cancellation_document.data.description, cancellation_document.data.description) + cancellation_document = munchify({ + "data": { + "id": TEST_TENDER_KEYS_LIMITED.cancellation_document_id, + "description": "test_patch_cancellation_document" + } + }) + patched_cancellation_document = \ + self.client.patch_cancellation_document( + self.limited_tender.data.id, cancellation_document, + TEST_TENDER_KEYS_LIMITED.cancellation_id, + TEST_TENDER_KEYS_LIMITED.cancellation_document_id + ) + self.assertEqual(patched_cancellation_document.data.id, + cancellation_document.data.id) + self.assertEqual(patched_cancellation_document.data.description, + cancellation_document.data.description) def test_patch_complaint(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - complaint = munchify({"data": {"id": TEST_TENDER_KEYS_LIMITED.complaint_id, "description": "test_patch_complaint"}}) - patched_complaint =self.client.patch_complaint(self.limited_tender, complaint) + complaint = munchify({ + "data": { + "id": TEST_TENDER_KEYS_LIMITED.complaint_id, + "description": "test_patch_complaint" + } + }) + patched_complaint = self.client.patch_complaint( + self.limited_tender.data.id, complaint, complaint.data.id + ) self.assertEqual(patched_complaint.data.id, complaint.data.id) - self.assertEqual(patched_complaint.data.description, complaint.data.description) + self.assertEqual(patched_complaint.data.description, + complaint.data.description) def test_patch_qualification(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - qualification = munchify({"data": {"id": TEST_TENDER_KEYS.qualification_id, "description": "test_patch_qualification"}}) - patched_qualification = self.client.patch_qualification(self.tender, qualification) + qualification = munchify({ + "data": { + "id": TEST_TENDER_KEYS.qualification_id, + "description": "test_patch_qualification" + } + }) + patched_qualification = self.client.patch_qualification( + self.tender.data.id, qualification, qualification.data.id + ) self.assertEqual(patched_qualification.data.id, qualification.data.id) - self.assertEqual(patched_qualification.data.description, qualification.data.description) + self.assertEqual(patched_qualification.data.description, + qualification.data.description) def test_patch_lot(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - lot = munchify({"data": {"id": TEST_TENDER_KEYS.lot_id, "description": "test_patch_lot"}}) - patched_lot = self.client.patch_lot(self.tender, lot) + lot = munchify({ + "data": { + "id": TEST_TENDER_KEYS.lot_id, + "description": "test_patch_lot" + } + }) + patched_lot = self.client.patch_lot( + self.tender.data.id, lot, lot.data.id + ) self.assertEqual(patched_lot.data.id, lot.data.id) self.assertEqual(patched_lot.data.description, lot.data.description) def test_patch_document(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - document = munchify({"data": {"id": TEST_TENDER_KEYS.document_id, "title": "test_patch_document.txt"}}) - patched_document = self.client.patch_document(self.tender, document) + document = munchify({ + "data": { + "id": TEST_TENDER_KEYS.document_id, + "title": "test_patch_document.txt" + } + }) + patched_document = self.client.patch_document( + self.tender.data.id, document, document.data.id + ) self.assertEqual(patched_document.data.id, document.data.id) self.assertEqual(patched_document.data.title, document.data.title) def test_patch_contract(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - contract = munchify({"data": {"id": TEST_TENDER_KEYS_LIMITED.contract_id, "title": "test_patch_contract.txt"}}) - patched_contract = self.client.patch_contract(self.limited_tender, contract) + contract = munchify({ + "data": { + "id": TEST_TENDER_KEYS_LIMITED.contract_id, + "title": "test_patch_contract.txt" + } + }) + patched_contract = self.client.patch_contract( + self.limited_tender.data.id, contract, contract.data.id + ) self.assertEqual(patched_contract.data.id, contract.data.id) self.assertEqual(patched_contract.data.title, contract.data.title) def test_patch_location_error(self): setup_routing(self.app, routes=["tender_subpage_item_patch"]) - error = munchify({"data": {"id": TEST_TENDER_KEYS.error_id}, "access": {"token": API_KEY}}) - self.assertEqual(self.client.patch_question(self.empty_tender, error), - munchify(loads(location_error('questions')))) - self.assertEqual(self.client.patch_bid(self.empty_tender, error), - munchify(loads(location_error('bids')))) - self.assertEqual(self.client.patch_award(self.empty_tender, error), - munchify(loads(location_error('awards')))) - self.assertEqual(self.client.patch_lot(self.empty_tender, error), - munchify(loads(location_error('lots')))) - self.assertEqual(self.client.patch_document(self.empty_tender, error), - munchify(loads(location_error('documents')))) - self.assertEqual(self.client.patch_qualification(self.empty_tender, error), - munchify(loads(location_error('qualifications')))) + error = munchify({ + "data": {"id": TEST_TENDER_KEYS.error_id}, + "access": {"token": API_KEY} + }) + self.assertEqual( + self.client.patch_question( + self.empty_tender.data.id, error, error.data.id + ), + munchify(loads(location_error('questions'))) + ) + self.assertEqual( + self.client.patch_bid( + self.empty_tender.data.id, error, error.data.id + ), + munchify(loads(location_error('bids'))) + ) + self.assertEqual( + self.client.patch_award( + self.empty_tender.data.id, error, error.data.id + ), + munchify(loads(location_error('awards'))) + ) + self.assertEqual( + self.client.patch_lot( + self.empty_tender.data.id, error, error.data.id + ), + munchify(loads(location_error('lots'))) + ) + self.assertEqual( + self.client.patch_document( + self.empty_tender.data.id, error, error.data.id + ), + munchify(loads(location_error('documents'))) + ) + self.assertEqual( + self.client.patch_qualification( + self.empty_tender.data.id, error, error.data.id + ), + munchify(loads(location_error('qualifications'))) + ) def test_patch_bid_document(self): setup_routing(self.app, routes=["tender_subpage_document_patch"]) - document = munchify({"data": {"id": TEST_TENDER_KEYS.document_id, "title": "test_patch_document.txt"}}) - patched_document = self.client.patch_bid_document(self.tender, document, TEST_TENDER_KEYS.bid_id, TEST_TENDER_KEYS.bid_document_id) + document = munchify({ + "data": { + "id": TEST_TENDER_KEYS.document_id, + "title": "test_patch_document.txt" + } + }) + patched_document = self.client.patch_bid_document( + self.tender.data.id, document, TEST_TENDER_KEYS.bid_id, + TEST_TENDER_KEYS.bid_document_id + ) self.assertEqual(patched_document.data.id, document.data.id) self.assertEqual(patched_document.data.title, document.data.title) def test_patch_credentials(self): setup_routing(self.app, routes=['tender_patch_credentials']) - patched_credentials = self.client.patch_credentials(self.tender.data.id, self.tender.access['token']) + patched_credentials = self.client.patch_credentials( + self.tender.data.id, self.tender.access['token'] + ) self.assertEqual(patched_credentials.data.id, self.tender.data.id) - self.assertNotEqual(patched_credentials.access.token, self.tender.access['token']) - self.assertEqual(patched_credentials.access.token, TEST_TENDER_KEYS['new_token']) + self.assertNotEqual(patched_credentials.access.token, + self.tender.access['token']) + self.assertEqual(patched_credentials.access.token, + TEST_TENDER_KEYS['new_token']) ########################################################################### # DOCUMENTS FILE TEST @@ -496,7 +685,10 @@ def test_upload_tender_document(self): file_.name = 'test_document.txt' file_.write("test upload tender document text data") file_.seek(0) - doc = self.client.upload_document(file_, self.tender) + doc = self.client.upload_document( + file_, self.tender.data.id, + access_token=self.tender.access['token'] + ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) @@ -504,18 +696,25 @@ def test_upload_tender_document_path(self): setup_routing(self.app, routes=["tender_document_create"]) file_name = "test_document.txt" file_path = ROOT + file_name - doc = self.client.upload_document(file_path, self.tender) + doc = self.client.upload_document( + file_path, self.tender.data.id, + access_token=self.tender.access['token'] + ) self.assertEqual(doc.data.title, file_name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) - @mock.patch('openprocurement_client.document_service_client.DocumentServiceClient.request') + @mock.patch('openprocurement_client.resources.document_service.' + 'DocumentServiceClient.request') def test_upload_tender_document_path_failed(self, mock_request): mock_request.return_value = munchify({'status_code': 204}) setup_routing(self.app, routes=["tender_document_create"]) file_name = "test_document.txt" file_path = ROOT + file_name with self.assertRaises(InvalidResponse): - self.client.upload_document(file_path, self.tender) + self.client.upload_document( + file_path, self.tender.data.id, + access_token=self.tender.access['token'] + ) def test_upload_qualification_document(self): setup_routing(self.app, routes=["tender_subpage_document_create"]) @@ -524,7 +723,7 @@ def test_upload_qualification_document(self): file_.write("test upload qualification document text data") file_.seek(0) doc = self.client.upload_qualification_document( - file_, self.tender, TEST_TENDER_KEYS.qualification_id + file_, self.tender.data.id, TEST_TENDER_KEYS.qualification_id ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) @@ -536,48 +735,49 @@ def test_upload_bid_document(self): file_.write("test upload tender document text data") file_.seek(0) doc = self.client.upload_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) def test_upload_bid_financial_document(self): setup_routing(self.app, routes=["tender_subpage_document_create"]) - document_type = "financial_documents" + document_type = FINANCIAL_DOCUMENTS file_ = StringIO() file_.name = 'test_document.txt' file_.write("test upload tender document text data") file_.seek(0) doc = self.client.upload_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, - document_type + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, + doc_type=document_type ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) def test_upload_bid_qualification_document(self): setup_routing(self.app, routes=["tender_subpage_document_create"]) - document_type = "qualificationDocuments" + document_type = QUALIFICATION_DOCUMENTS file_ = StringIO() file_.name = 'test_document.txt' file_.write("test upload tender document text data") file_.seek(0) doc = self.client.upload_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, - document_type + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, + doc_type=document_type ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) def test_upload_bid_eligibility_document(self): setup_routing(self.app, routes=["tender_subpage_document_create"]) - document_type = "eligibility_documents" + document_type = ELIGIBILITY_DOCUMENTS file_ = StringIO() file_.name = 'test_document.txt' file_.write("test upload tender document text data") file_.seek(0) doc = self.client.upload_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, document_type + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, + doc_type=document_type ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) @@ -589,7 +789,8 @@ def test_upload_cancellation_document(self): file_.write("test upload tender document text data") file_.seek(0) doc = self.client.upload_cancellation_document( - file_, self.limited_tender, TEST_TENDER_KEYS_LIMITED.cancellation_id + file_, self.limited_tender.data.id, + TEST_TENDER_KEYS_LIMITED.cancellation_id ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) @@ -601,7 +802,8 @@ def test_upload_complaint_document(self): file_.write("test upload tender document text data") file_.seek(0) doc = self.client.upload_complaint_document( - file_, self.limited_tender, TEST_TENDER_KEYS_LIMITED.complaint_id + file_, self.limited_tender.data.id, + TEST_TENDER_KEYS_LIMITED.complaint_id ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) @@ -613,14 +815,16 @@ def test_upload_award_document(self): file_.write("test upload award document text data") file_.seek(0) doc = self.client.upload_award_document( - file_, self.tender, TEST_TENDER_KEYS.award_id + file_, self.tender.data.id, TEST_TENDER_KEYS.award_id ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_TENDER_KEYS.new_document_id) def test_upload_document_type_error(self): setup_routing(self.app, routes=["tender_document_create"]) - self.assertRaises(TypeError, self.client.upload_document, (object, self.tender)) + self.assertRaises( + TypeError, self.client.upload_document, (object, self.tender) + ) def test_update_bid_document(self): setup_routing(self.app, routes=["tender_subpage_document_update"]) @@ -629,7 +833,7 @@ def test_update_bid_document(self): file_.write("test upload tender document text data") file_.seek(0) doc = self.client.update_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, TEST_TENDER_KEYS.bid_document_id ) self.assertEqual(doc.data.title, file_.name) @@ -641,13 +845,15 @@ def test_update_bid_qualification_document(self): file_.name = 'test_document.txt' file_.write("test upload tender qualification_document text data") file_.seek(0) - document_type = "qualificationDocuments" + document_type = QUALIFICATION_DOCUMENTS doc = self.client.update_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, - TEST_TENDER_KEYS.bid_qualification_document_id, document_type + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, + TEST_TENDER_KEYS.bid_qualification_document_id, + doc_type=document_type ) self.assertEqual(doc.data.title, file_.name) - self.assertEqual(doc.data.id, TEST_TENDER_KEYS.bid_qualification_document_id) + self.assertEqual(doc.data.id, + TEST_TENDER_KEYS.bid_qualification_document_id) def test_update_bid_financial_document(self): setup_routing(self.app, routes=["tender_subpage_document_update"]) @@ -655,13 +861,14 @@ def test_update_bid_financial_document(self): file_.name = 'test_document.txt' file_.write("test upload tender financial_document text data") file_.seek(0) - document_type = "financial_documens" + document_type = FINANCIAL_DOCUMENTS doc = self.client.update_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, - TEST_TENDER_KEYS.bid_financial_document_id, document_type + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, + TEST_TENDER_KEYS.bid_financial_document_id, doc_type=document_type ) self.assertEqual(doc.data.title, file_.name) - self.assertEqual(doc.data.id, TEST_TENDER_KEYS.bid_financial_document_id) + self.assertEqual(doc.data.id, + TEST_TENDER_KEYS.bid_financial_document_id) def test_update_bid_eligibility_document(self): setup_routing(self.app, routes=["tender_subpage_document_update"]) @@ -669,13 +876,15 @@ def test_update_bid_eligibility_document(self): file_.name = 'test_document.txt' file_.write("test upload tender eligibility_document text data") file_.seek(0) - document_type = "eligibility_documents" + document_type = ELIGIBILITY_DOCUMENTS doc = self.client.update_bid_document( - file_, self.tender, TEST_TENDER_KEYS.bid_id, - TEST_TENDER_KEYS.bid_eligibility_document_id, document_type + file_, self.tender.data.id, TEST_TENDER_KEYS.bid_id, + TEST_TENDER_KEYS.bid_eligibility_document_id, + doc_type=document_type ) self.assertEqual(doc.data.title, file_.name) - self.assertEqual(doc.data.id, TEST_TENDER_KEYS.bid_eligibility_document_id) + self.assertEqual(doc.data.id, + TEST_TENDER_KEYS.bid_eligibility_document_id) def test_update_cancellation_document(self): setup_routing(self.app, routes=["tender_subpage_document_update"]) @@ -684,11 +893,13 @@ def test_update_cancellation_document(self): file_.write("test upload tender document text data") file_.seek(0) doc = self.client.update_cancellation_document( - file_, self.limited_tender, TEST_TENDER_KEYS_LIMITED.cancellation_id, + file_, self.limited_tender.data.id, + TEST_TENDER_KEYS_LIMITED.cancellation_id, TEST_TENDER_KEYS_LIMITED.cancellation_document_id ) self.assertEqual(doc.data.title, file_.name) - self.assertEqual(doc.data.id, TEST_TENDER_KEYS_LIMITED.cancellation_document_id) + self.assertEqual(doc.data.id, + TEST_TENDER_KEYS_LIMITED.cancellation_document_id) ########################################################################### # DELETE ITEMS LIST TEST @@ -696,22 +907,33 @@ def test_update_cancellation_document(self): def test_delete_bid(self): setup_routing(self.app, routes=["tender_subpage_item_delete"]) - bid_id = resource_partition(TEST_TENDER_KEYS.tender_id, part="bids")[0]['id'] - deleted_bid = self.client.delete_bid(self.tender, bid_id, API_KEY) + bid_id = resource_partition( + TEST_TENDER_KEYS.tender_id, part="bids")[0]['id'] + deleted_bid = self.client.delete_bid(self.tender.data.id, bid_id, + API_KEY) self.assertFalse(deleted_bid) def test_delete_lot(self): setup_routing(self.app, routes=["tender_subpage_item_delete"]) - lot_id = resource_partition(TEST_TENDER_KEYS.tender_id, part="lots")[0]['id'] - deleted_lot = self.client.delete_lot(self.tender, lot_id) + lot_id = resource_partition( + TEST_TENDER_KEYS.tender_id, part="lots")[0]['id'] + deleted_lot = self.client.delete_lot(self.tender.data.id, lot_id) self.assertFalse(deleted_lot) def test_delete_location_error(self): setup_routing(self.app, routes=["tender_subpage_item_delete"]) - self.assertEqual(self.client.delete_bid(self.empty_tender, TEST_TENDER_KEYS.error_id, API_KEY), - munchify(loads(location_error('bids')))) - self.assertEqual(self.client.delete_lot(self.empty_tender, TEST_TENDER_KEYS.error_id), - munchify(loads(location_error('lots')))) + self.assertEqual( + self.client.delete_bid( + self.empty_tender.data.id, TEST_TENDER_KEYS.error_id, API_KEY + ), + munchify(loads(location_error('bids'))) + ) + self.assertEqual( + self.client.delete_lot( + self.empty_tender, TEST_TENDER_KEYS.error_id + ), + munchify(loads(location_error('lots'))) + ) class ContractingUserTestCase(BaseTestClass): @@ -770,7 +992,10 @@ def test_create_contract(self): def test_create_change(self): setup_routing(self.app, routes=['contract_subpage_item_create']) - change = self.client.create_change(self.contract, self.change) + contract_id = self.contract.data.id + access_token = self.contract.access['token'] + change = self.client.create_change(contract_id, access_token, + self.change) self.assertEqual(change, self.change) ########################################################################### @@ -781,7 +1006,10 @@ def test_upload_contract_document(self): setup_routing(self.app, routes=['contract_document_create']) file_ = generate_file_obj('test_document.txt', 'test upload contract document text data') - doc = self.client.upload_document(file_, self.contract) + doc = self.client.upload_document( + file_, self.contract.data.id, + access_token=self.contract.access['token'] + ) self.assertEqual(doc.data.title, file_.name) self.assertEqual(doc.data.id, TEST_CONTRACT_KEYS.new_document_id) @@ -793,22 +1021,27 @@ def test_patch_document(self): setup_routing(self.app, routes=['contract_subpage_item_patch']) document = munchify({'data': {'id': TEST_CONTRACT_KEYS.document_id, 'title': 'test_patch_document.txt'}}) - patched_document = self.client.patch_document(self.contract, document) + patched_document = self.client.patch_document( + self.contract.data.id, document, document.data.id, + self.contract.access['token']) self.assertEqual(patched_document.data.id, document.data.id) self.assertEqual(patched_document.data.title, document.data.title) def test_patch_change(self): setup_routing(self.app, routes=['contract_change_patch']) - patch_change_data = \ - {'data': {'rationale': - TEST_CONTRACT_KEYS['patch_change_rationale']}} + patch_change_data = { + 'data': { + 'rationale': TEST_CONTRACT_KEYS['patch_change_rationale'] + } + } patched_change = self.change.copy() patched_change['data'].update(patch_change_data['data']) patched_change = munchify(patched_change) - + contract_id = self.contract.data.id + access_token = self.contract.access['token'] + changes_id = self.change.data.id response_change = self.client.patch_change( - self.contract, self.change.data.id, - data=patch_change_data + contract_id, changes_id, access_token, patch_change_data ) self.assertEqual(response_change, patched_change) @@ -827,10 +1060,14 @@ def test_retrieve_contract_credentials(self): def test_patch_contract(self): setup_routing(self.app, routes=["contract_patch"]) - self.contract.data.description = 'test_patch_contract' - patched_contract = self.client.patch_contract(self.contract) + patch_data = {'data': {'description': 'test_patch_contract'}} + access_token = self.contract.access['token'] + patched_contract = self.client.patch_contract( + self.contract.data.id, access_token, patch_data + ) self.assertEqual(patched_contract.data.id, self.contract.data.id) - self.assertEqual(patched_contract.data.description, self.contract.data.description) + self.assertEqual(patched_contract.data.description, + patch_data['data']['description']) def suite(): diff --git a/openprocurement_client/tests/tests_sync.py b/openprocurement_client/tests/tests_sync.py index e8bf964..1aef39b 100644 --- a/openprocurement_client/tests/tests_sync.py +++ b/openprocurement_client/tests/tests_sync.py @@ -1,19 +1,16 @@ from __future__ import print_function -from gevent import monkey; monkey.patch_all() +from gevent import monkey +monkey.patch_all() -from openprocurement_client.client import TendersClientSync +from openprocurement_client.clients import APIResourceClientSync from openprocurement_client.exceptions import ( RequestFailed, PreconditionFailed, ResourceNotFound ) -from openprocurement_client.sync import ( - get_response, - get_resource_items, - get_tenders, - ResourceFeeder, -) -from openprocurement_client.sync import logger +from openprocurement_client.utils import get_response +from openprocurement_client.resources.sync import ResourceFeeder +from openprocurement_client.utils import LOGGER from gevent.queue import Queue from munch import munchify @@ -38,7 +35,7 @@ def __nonzero__(self): return bool(0) -class TestTendersClientSync(TendersClientSync): +class TestAPIResourceClientSync(APIResourceClientSync): def __init__(self): pass @@ -50,66 +47,80 @@ def setUp(self): self.log_capture_string = StringIO() self.ch = logging.StreamHandler(self.log_capture_string) self.ch.setLevel(logging.ERROR) - logger.addHandler(self.ch) - self.logger = logger - - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - def test_success_response(self, mock_sync_tenders): - mock_sync_tenders.return_value = 'success' - mock_client = TestTendersClientSync() + LOGGER.addHandler(self.ch) + self.logger = LOGGER + + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + def test_success_response(self, mock_sync_resource_items): + mock_sync_resource_items.return_value = 'success' + mock_client = TestAPIResourceClientSync() response = get_response(mock_client, {}) self.assertEqual(response, 'success') - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - def test_precondition_failed_error(self, mock_sync_tenders): - mock_sync_tenders.side_effect = [PreconditionFailed(), 'success'] - mock_client = TestTendersClientSync() + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + def test_precondition_failed_error(self, mock_sync_resource_items): + mock_sync_resource_items.side_effect = [PreconditionFailed(), + 'success'] + mock_client = TestAPIResourceClientSync() response = get_response(mock_client, {}) log_strings = self.log_capture_string.getvalue().split('\n') - self.assertEqual(log_strings[0], 'PreconditionFailed: Not described error yet.') + self.assertEqual(log_strings[0], + 'PreconditionFailed: Not described error yet.') self.assertEqual(response, 'success') - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - def test_connection_error(self, mock_sync_tenders): + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + def test_connection_error(self, mock_sync_resource_items): error = ConnectionError('connection error') - mock_sync_tenders.side_effect = [error, error, 'success'] - mock_client = TestTendersClientSync() + mock_sync_resource_items.side_effect = [error, error, 'success'] + mock_client = TestAPIResourceClientSync() response = get_response(mock_client, {}) log_strings = self.log_capture_string.getvalue().split('\n') self.assertEqual(log_strings[0], 'ConnectionError: connection error') self.assertEqual(log_strings[1], 'ConnectionError: connection error') self.assertEqual(response, 'success') - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - def test_request_failed_error(self, mock_sync_tenders): + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + def test_request_failed_error(self, mock_sync_resource_items): error1 = munchify({'status_code': 429, }) error2 = munchify({'status_code': 404, }) - mock_sync_tenders.side_effect = [RequestFailed(error1), RequestFailed(error2), 'success'] - mock_client = TestTendersClientSync() + mock_sync_resource_items.side_effect = [RequestFailed(error1), + RequestFailed(error2), + 'success'] + mock_client = TestAPIResourceClientSync() response = get_response(mock_client, {}) log_strings = self.log_capture_string.getvalue().split('\n') self.assertEqual(log_strings[0], 'Request failed. Status code: 429') self.assertEqual(log_strings[1], 'Request failed. Status code: 404') self.assertEqual(response, 'success') - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - def test_resource_not_found_error(self, mock_sync_tenders): - mock_sync_tenders.side_effect = [ResourceNotFound(), 'success'] + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + def test_resource_not_found_error(self, mock_sync_resource_items): + mock_sync_resource_items.side_effect = [ResourceNotFound(), 'success'] params = {'offset': 'offset', 'some_data': 'data'} - mock_client = TestTendersClientSync() + mock_client = TestAPIResourceClientSync() mock_client.session = mock.MagicMock() mock_client.session.cookies.clear = mock.Mock() response = get_response(mock_client, params) log_strings = self.log_capture_string.getvalue().split('\n') - self.assertEqual(log_strings[0], 'Resource not found: Not described error yet.') + self.assertEqual(log_strings[0], + 'Resource not found: Not described error yet.') self.assertEqual(mock_client.session.cookies.clear.call_count, 1) self.assertEqual(params, {'some_data': 'data'}) self.assertEqual(response, 'success') - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - def test_exception_error(self, mock_sync_tenders): - mock_sync_tenders.side_effect = [InvalidHeader('invalid header'), Exception('exception message'), 'success'] - mock_client = TestTendersClientSync() + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + def test_exception_error(self, mock_sync_resource_items): + mock_sync_resource_items.side_effect = [ + InvalidHeader('invalid header'), Exception('exception message'), + 'success' + ] + mock_client = TestAPIResourceClientSync() response = get_response(mock_client, {}) log_strings = self.log_capture_string.getvalue().split('\n') self.assertEqual(log_strings[0], 'Exception: invalid header') @@ -151,11 +162,13 @@ def setUp(self): def test_instance_initialization(self): self.resource_feeder = ResourceFeeder() self.assertEqual(self.resource_feeder.key, '') - self.assertEqual(self.resource_feeder.host, 'https://lb.api-sandbox.openprocurement.org/') + self.assertEqual(self.resource_feeder.host, + 'https://lb.api-sandbox.openprocurement.org/') self.assertEqual(self.resource_feeder.version, '2.3') self.assertEqual(self.resource_feeder.resource, 'tenders') self.assertEqual(self.resource_feeder.adaptive, False) - self.assertEqual(self.resource_feeder.extra_params, {'opt_fields': 'status', 'mode': '_all_'}) + self.assertEqual(self.resource_feeder.extra_params, + {'opt_fields': 'status', 'mode': '_all_'}) self.assertEqual(self.resource_feeder.retrievers_params, { 'down_requests_sleep': 5, 'up_requests_sleep': 1, @@ -189,39 +202,48 @@ def test_handle_response_data(self): self.assertIn('tender2', list(self.resource_feeder.queue.queue)) self.assertNotIn('tender3', list(self.resource_feeder.queue.queue)) - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - @mock.patch('openprocurement_client.sync.spawn') - def test_start_sync(self, mock_spawn, mock_sync_tenders): + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + @mock.patch('openprocurement_client.resources.sync.spawn') + def test_start_sync(self, mock_spawn, mock_sync_resource_items): mock_spawn.return_value = 'spawn result' - mock_sync_tenders.return_value = self.response + mock_sync_resource_items.return_value = self.response self.resource_feeder = ResourceFeeder() self.resource_feeder.init_api_clients() self.resource_feeder.start_sync() - self.assertEqual(self.resource_feeder.backward_params['offset'], self.response.next_page.offset) - self.assertEqual(self.resource_feeder.forward_params['offset'], self.response.prev_page.offset) - self.assertEqual(self.resource_feeder.forward_params['offset'], self.response.prev_page.offset) + self.assertEqual(self.resource_feeder.backward_params['offset'], + self.response.next_page.offset) + self.assertEqual(self.resource_feeder.forward_params['offset'], + self.response.prev_page.offset) + self.assertEqual(self.resource_feeder.forward_params['offset'], + self.response.prev_page.offset) self.assertEqual(mock_spawn.call_count, 2) mock_spawn.assert_called_with(self.resource_feeder.retriever_forward) self.assertEqual(self.resource_feeder.backward_worker, 'spawn result') self.assertEqual(self.resource_feeder.forward_worker, 'spawn result') - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - @mock.patch('openprocurement_client.sync.spawn') - def test_restart_sync(self, mock_spawn, mock_sync_tenders): + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + @mock.patch('openprocurement_client.resources.sync.spawn') + def test_restart_sync(self, mock_spawn, mock_sync_resource_items): mock_spawn.return_value = mock.MagicMock() mock_spawn.return_value.kill = mock.MagicMock('kill result') - mock_sync_tenders.return_value = self.response + mock_sync_resource_items.return_value = self.response self.resource_feeder = ResourceFeeder() self.resource_feeder.init_api_clients() self.resource_feeder.start_sync() self.resource_feeder.restart_sync() self.assertEqual(mock_spawn.return_value.kill.call_count, 2) - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - @mock.patch('openprocurement_client.sync.spawn') - def test_get_resource_items_zero_value(self, mock_spawn, mock_sync_tenders): - mock_sync_tenders.side_effect = [self.response, munchify( - {'data': {}, 'next_page': {'offset': 'next_page'}, 'prev_page': {'offset': 'next_page'}} + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + @mock.patch('openprocurement_client.resources.sync.spawn') + def test_get_resource_items_zero_value(self, mock_spawn, + mock_sync_resource_items): + mock_sync_resource_items.side_effect = [self.response, munchify( + {'data': {}, + 'next_page': {'offset': 'next_page'}, + 'prev_page': {'offset': 'next_page'}} )] mock_spawn.return_value = mock.MagicMock() mock_spawn.return_value.value = 0 @@ -231,11 +253,15 @@ def test_get_resource_items_zero_value(self, mock_spawn, mock_sync_tenders): result = self.resource_feeder.get_resource_items() self.assertEqual(tuple(result), tuple(self.response.data)) - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - @mock.patch('openprocurement_client.sync.spawn') - def test_get_resource_items_non_zero_value(self, mock_spawn, mock_sync_tenders): - mock_sync_tenders.side_effect = [self.response, munchify( - {'data': {}, 'next_page': {'offset': 'next_page'}, 'prev_page': {'offset': 'next_page'}} + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + @mock.patch('openprocurement_client.resources.sync.spawn') + def test_get_resource_items_non_zero_value(self, mock_spawn, + mock_sync_resource_items): + mock_sync_resource_items.side_effect = [self.response, munchify( + {'data': {}, + 'next_page': {'offset': 'next_page'}, + 'prev_page': {'offset': 'next_page'}} )] mock_spawn.return_value = mock.MagicMock() mock_spawn.return_value.value = 1 @@ -245,12 +271,15 @@ def test_get_resource_items_non_zero_value(self, mock_spawn, mock_sync_tenders): result = self.resource_feeder.get_resource_items() self.assertEqual(tuple(result), tuple(self.response.data)) - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - @mock.patch('openprocurement_client.sync.spawn') - @mock.patch('openprocurement_client.sync.sleep') - def test_feeder_zero_value(self, mock_sleep, mock_spawn, mock_sync_tenders): + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + @mock.patch('openprocurement_client.resources.sync.spawn') + @mock.patch('openprocurement_client.resources.sync.sleep') + def test_feeder_zero_value(self, mock_sleep, mock_spawn, + mock_sync_resource_items): mock_sleep.return_value = 'sleeping' - mock_sync_tenders.side_effect = [self.response, self.response, ConnectionError('conn error')] + mock_sync_resource_items.side_effect = [self.response, self.response, + ConnectionError('conn error')] self.resource_feeder = ResourceFeeder() mock_spawn.return_value = mock.MagicMock() mock_spawn.return_value.value = 0 @@ -260,12 +289,14 @@ def test_feeder_zero_value(self, mock_sleep, mock_spawn, mock_sync_tenders): self.assertEqual(e.exception.message, 'conn error') self.assertEqual(mock_sleep.call_count, 1) - @mock.patch('openprocurement_client.client.TendersClientSync.sync_tenders') - @mock.patch('openprocurement_client.sync.spawn') - @mock.patch('openprocurement_client.sync.sleep') - def test_feeder(self, mock_sleep, mock_spawn, mock_sync_tenders): + @mock.patch('openprocurement_client.clients.APIResourceClientSync.' + 'sync_resource_items') + @mock.patch('openprocurement_client.resources.sync.spawn') + @mock.patch('openprocurement_client.resources.sync.sleep') + def test_feeder(self, mock_sleep, mock_spawn, mock_sync_resource_items): mock_sleep.return_value = 'sleeping' - mock_sync_tenders.side_effect = [self.response, self.response, ConnectionError('conn error')] + mock_sync_resource_items.side_effect = [self.response, self.response, + ConnectionError('conn error')] self.resource_feeder = ResourceFeeder() mock_spawn.return_value = mock.MagicMock() mock_spawn.return_value.value = 1 @@ -275,7 +306,7 @@ def test_feeder(self, mock_sleep, mock_spawn, mock_sync_tenders): self.assertEqual(e.exception.message, 'conn error') self.assertEqual(mock_sleep.call_count, 1) - @mock.patch('openprocurement_client.sync.spawn') + @mock.patch('openprocurement_client.resources.sync.spawn') def test_run_feeder(self, mock_spawn): mock_spawn.return_value = mock.MagicMock() self.resource_feeder = ResourceFeeder() @@ -283,18 +314,20 @@ def test_run_feeder(self, mock_spawn): mock_spawn.assert_called_with(self.resource_feeder.feeder) self.assertEqual(result, self.resource_feeder.queue) - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_backward(self, mock_get_response): mock_get_response.side_effect = [self.response, munchify({'data': {}})] self.resource_feeder = ResourceFeeder() self.resource_feeder.init_api_clients() self.resource_feeder.backward_params = {"limit": 0} self.resource_feeder.backward_client = mock.MagicMock() - self.resource_feeder.cookies = self.resource_feeder.backward_client.session.cookies + self.resource_feeder.cookies =\ + self.resource_feeder.backward_client.session.cookies self.resource_feeder.retriever_backward() - self.assertEqual(self.resource_feeder.backward_params['offset'], self.response.next_page.offset) + self.assertEqual(self.resource_feeder.backward_params['offset'], + self.response.next_page.offset) - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_backward_wrong_cookies(self, mock_get_response): mock_get_response.return_value = self.response self.resource_feeder = ResourceFeeder() @@ -307,7 +340,7 @@ def test_retriever_backward_wrong_cookies(self, mock_get_response): self.resource_feeder.retriever_backward() self.assertEqual(e.exception.message, 'LB Server mismatch') - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_forward_wrong_cookies(self, mock_get_response): mock_get_response.return_value = self.response self.resource_feeder = ResourceFeeder() @@ -320,7 +353,7 @@ def test_retriever_forward_wrong_cookies(self, mock_get_response): self.resource_feeder.retriever_forward() self.assertEqual(e.exception.message, 'LB Server mismatch') - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_forward(self, mock_get_response): mock_get_response.side_effect = [ self.response, @@ -331,13 +364,15 @@ def test_retriever_forward(self, mock_get_response): self.resource_feeder.init_api_clients() self.resource_feeder.forward_params = {"limit": 0} self.resource_feeder.forward_client = mock.MagicMock() - self.resource_feeder.forward_client.session.cookies = self.resource_feeder.cookies + self.resource_feeder.forward_client.session.cookies = \ + self.resource_feeder.cookies with self.assertRaises(ConnectionError) as e: self.resource_feeder.retriever_forward() self.assertEqual(e.exception.message, 'connection error') - self.assertEqual(self.resource_feeder.forward_params['offset'], self.response.next_page.offset) + self.assertEqual(self.resource_feeder.forward_params['offset'], + self.response.next_page.offset) - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_forward_no_data(self, mock_get_response): mock_get_response.side_effect = [ munchify({'data': {}, 'next_page': {'offset': 'next_page'}}), @@ -347,13 +382,15 @@ def test_retriever_forward_no_data(self, mock_get_response): self.resource_feeder.init_api_clients() self.resource_feeder.forward_params = {"limit": 0} self.resource_feeder.forward_client = mock.MagicMock() - self.resource_feeder.forward_client.session.cookies = self.resource_feeder.cookies + self.resource_feeder.forward_client.session.cookies = \ + self.resource_feeder.cookies with self.assertRaises(ConnectionError) as e: self.resource_feeder.retriever_forward() self.assertEqual(e.exception.message, 'connection error') - self.assertEqual(self.resource_feeder.forward_params['offset'], 'next_page') + self.assertEqual(self.resource_feeder.forward_params['offset'], + 'next_page') - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_forward_adaptive(self, mock_get_response): mock_get_response.side_effect = [ self.response, @@ -365,13 +402,15 @@ def test_retriever_forward_adaptive(self, mock_get_response): self.resource_feeder.init_api_clients() self.resource_feeder.forward_params = {"limit": 0} self.resource_feeder.forward_client = mock.MagicMock() - self.resource_feeder.forward_client.session.cookies = self.resource_feeder.cookies + self.resource_feeder.forward_client.session.cookies = \ + self.resource_feeder.cookies with self.assertRaises(ConnectionError) as e: self.resource_feeder.retriever_forward() self.assertEqual(e.exception.message, 'connection error') - self.assertEqual(self.resource_feeder.forward_params['offset'], self.response.next_page.offset) + self.assertEqual(self.resource_feeder.forward_params['offset'], + self.response.next_page.offset) - @mock.patch('openprocurement_client.sync.get_response') + @mock.patch('openprocurement_client.resources.sync.get_response') def test_retriever_forward_no_data_adaptive(self, mock_get_response): mock_get_response.side_effect = [ munchify({'data': {}, 'next_page': {'offset': 'next_page'}}), @@ -391,25 +430,13 @@ def test_retriever_forward_no_data_adaptive(self, mock_get_response): self.resource_feeder.init_api_clients() self.resource_feeder.forward_params = {"limit": 0} self.resource_feeder.forward_client = mock.MagicMock() - self.resource_feeder.forward_client.session.cookies = self.resource_feeder.cookies + self.resource_feeder.forward_client.session.cookies = \ + self.resource_feeder.cookies with self.assertRaises(ConnectionError) as e: self.resource_feeder.retriever_forward() self.assertEqual(e.exception.message, 'connection error') - self.assertEqual(self.resource_feeder.forward_params['offset'], 'next_page') - - @mock.patch('openprocurement_client.sync.ResourceFeeder.get_resource_items') - def test_get_resource_items(self, mock_get_resource_items): - mock_get_resource_items.return_value = 'feeder_instance' - result = get_resource_items(resource='tenders') - self.assertEqual(result, 'feeder_instance') - self.assertEqual(mock_get_resource_items.call_count, 1) - - @mock.patch('openprocurement_client.sync.get_resource_items') - def test_get_tenders(self, mock_get_resource_items): - mock_get_resource_items.return_value = 'get_resource_items_call' - result = get_tenders() - self.assertEqual(result, 'get_resource_items_call') - self.assertEqual(mock_get_resource_items.call_count, 1) + self.assertEqual(self.resource_feeder.forward_params['offset'], + 'next_page') def suite(): diff --git a/openprocurement_client/tests/tests_utils.py b/openprocurement_client/tests/tests_utils.py index 61b19bb..4853d0b 100644 --- a/openprocurement_client/tests/tests_utils.py +++ b/openprocurement_client/tests/tests_utils.py @@ -1,9 +1,12 @@ from __future__ import print_function -from gevent import monkey; monkey.patch_all() +from gevent import monkey +monkey.patch_all() -from openprocurement_client.client import TendersClient +from openprocurement_client.resources.tenders import TendersClient from openprocurement_client.exceptions import IdNotFound -from openprocurement_client.utils import tenders_feed, get_tender_id_by_uaid, get_tender_by_uaid +from openprocurement_client.utils import ( + tenders_feed, get_tender_id_by_uaid, get_tender_by_uaid +) from munch import munchify @@ -49,7 +52,8 @@ def setUp(self): ] }""")) - @mock.patch('openprocurement_client.client.TendersClient.get_tenders') + @mock.patch('openprocurement_client.resources.tenders.TendersClient.' + 'get_tenders') def test_tenders_feed(self, mock_get_tenders): mock_get_tenders.side_effect = [self.response.data, []] client = TestTendersClient() @@ -61,8 +65,10 @@ def test_tenders_feed(self, mock_get_tenders): result.next() @mock.patch('openprocurement_client.utils.get_tender_id_by_uaid') - @mock.patch('openprocurement_client.client.TendersClient.get_tender') - def test_get_tender_by_uaid(self, mock_get_tender, mock_get_tender_id_by_uaid): + @mock.patch('openprocurement_client.resources.tenders.TendersClient.' + 'get_tender') + def test_get_tender_by_uaid(self, mock_get_tender, + mock_get_tender_id_by_uaid): mock_get_tender_id_by_uaid.return_value = 'tender_id' mock_get_tender.return_value = 'called get_tender' client = TestTendersClient() @@ -73,12 +79,14 @@ def test_get_tender_by_uaid(self, mock_get_tender, mock_get_tender_id_by_uaid): self.assertEqual(mock_get_tender.call_count, 1) self.assertEqual(result, 'called get_tender') - @mock.patch('openprocurement_client.client.TendersClient.get_tenders') + @mock.patch('openprocurement_client.resources.tenders.TendersClient.' + 'get_tenders') def test_get_tender_id_by_uaid(self, mock_get_tenders): mock_get_tenders.side_effect = [self.response.data, []] client = TestTendersClient() with self.assertRaises(IdNotFound): - result = get_tender_id_by_uaid('f3849ade33534174b8402579152a5f41', client, id_field='dateModified') + result = get_tender_id_by_uaid('f3849ade33534174b8402579152a5f41', + client, id_field='dateModified') self.assertEqual(result, self.response.data[0]['id']) diff --git a/openprocurement_client/utils.py b/openprocurement_client/utils.py index 64a5a6e..a16c8f5 100644 --- a/openprocurement_client/utils.py +++ b/openprocurement_client/utils.py @@ -1,20 +1,72 @@ # -*- coding: utf-8 -*- +from io import FileIO +from functools import wraps from openprocurement_client.exceptions import IdNotFound -from time import sleep +from requests.exceptions import ConnectionError +from openprocurement_client.exceptions import ( + RequestFailed, + PreconditionFailed, + ResourceNotFound +) +from os import path +from time import sleep, time import logging -logger = logging.getLogger() + + +LOGGER = logging.getLogger() + + +# Using FileIO here instead of open() +# to be able to override the filename +# which is later used when uploading the file. +# +# Explanation: +# +# 1) requests reads the filename +# from "name" attribute of a file-like object, +# there is no other way to specify a filename; +# +# 2) The attribute may contain the full path to file, +# which does not work well as a filename; +# +# 3) The attribute is readonly when using open(), +# unlike FileIO object. + + +def verify_file(fn): + """ Decorator for upload or update document methods""" + @wraps(fn) + def wrapper(self, file_, *args, **kwargs): + if isinstance(file_, basestring): + file_ = FileIO(file_, 'rb') + file_.name = path.basename(file_.name) + if hasattr(file_, 'read'): + # A file-like object must have 'read' method + output = fn(self, file_, *args, **kwargs) + file_.close() + return output + else: + try: + file_.close() + except AttributeError: + pass + raise TypeError( + 'Expected either a string containing a path to file or ' + 'a file-like object, got {}'.format(type(file_)) + ) + return wrapper def tenders_feed(client, sleep_time=10): while True: tender_list = True while tender_list: - logger.info("Get next batch") + LOGGER.info("Get next batch") tender_list = client.get_tenders() for tender in tender_list: - logger.debug("Return tender {}".format(str(tender))) + LOGGER.debug("Return tender {}".format(str(tender))) yield tender - logger.info("Wait to get next batch") + LOGGER.info("Wait to get next batch") sleep(sleep_time) @@ -58,3 +110,63 @@ def get_plan_id_by_uaid(ua_id, client, descending=True, id_field='planID'): if tender[id_field] == ua_id: return tender.id raise IdNotFound + + +def get_response(client, params): + response_fail = True + sleep_interval = 0.2 + while response_fail: + try: + start = time() + response = client.sync_resource_items(params) + end = time() - start + LOGGER.debug( + 'Request duration {} sec'.format(end), + extra={'FEEDER_REQUEST_DURATION': end * 1000}) + response_fail = False + except PreconditionFailed as e: + LOGGER.error('PreconditionFailed: {}'.format(e.message), + extra={'MESSAGE_ID': 'precondition_failed'}) + continue + except ConnectionError as e: + LOGGER.error('ConnectionError: {}'.format(e.message), + extra={'MESSAGE_ID': 'connection_error'}) + if sleep_interval > 300: + raise e + sleep_interval = sleep_interval * 2 + LOGGER.debug( + 'Client sleeping after ConnectionError {} sec.'.format( + sleep_interval)) + sleep(sleep_interval) + continue + except RequestFailed as e: + LOGGER.error('Request failed. Status code: {}'.format( + e.status_code), extra={'MESSAGE_ID': 'request_failed'}) + if e.status_code == 429: + if sleep_interval > 120: + raise e + LOGGER.debug( + 'Client sleeping after RequestFailed {} sec.'.format( + sleep_interval)) + sleep_interval = sleep_interval * 2 + sleep(sleep_interval) + continue + except ResourceNotFound as e: + LOGGER.error('Resource not found: {}'.format(e.message), + extra={'MESSAGE_ID': 'resource_not_found'}) + LOGGER.debug('Clear offset and client cookies.') + client.session.cookies.clear() + del params['offset'] + continue + except Exception as e: + LOGGER.error('Exception: {}'.format(e.message), + extra={'MESSAGE_ID': 'exceptions'}) + if sleep_interval > 300: + raise e + sleep_interval = sleep_interval * 2 + LOGGER.debug( + 'Client sleeping after Exception: {}, {} sec.'.format( + e.message, sleep_interval)) + sleep(sleep_interval) + continue + return response diff --git a/setup.cfg b/setup.cfg index 1003930..f2cee2d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,4 @@ [nosetests] +all-modules=1 cover-package=openprocurement_client with-coverage=1 diff --git a/setup.py b/setup.py index 1b64636..35c3ddd 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ from setuptools import find_packages, setup -import os version = '2.0b7' @@ -45,9 +44,9 @@ # -*- Extra requirements: -*- ], tests_require=tests_require, - extras_require = tests_require, + extras_require=tests_require, entry_points=""" # -*- Entry points: -*- """, - test_suite="openprocurement_client.tests" + test_suite="openprocurement_client.tests.main:suite" )