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/README.rst b/README.rst index f1a0078..cd9e30c 100644 --- a/README.rst +++ b/README.rst @@ -8,5 +8,5 @@ Reference implementation of a client for OpenProcurement API. .. |Build Status| image:: https://travis-ci.org/openprocurement/openprocurement.client.python.svg?branch=use_requests :target: https://travis-ci.org/openprocurement/openprocurement.client.python -.. |Coverage Status| image:: https://coveralls.io/repos/openprocurement/openprocurement.client.python/badge.svg?branch=master&service=github - :target: https://coveralls.io/github/openprocurement/openprocurement.client.python?branch=use_requests +.. |Coverage Status| image:: https://coveralls.io/repos/github/openprocurement/openprocurement.client.python/badge.svg?branch=registry + :target: https://coveralls.io/github/openprocurement/openprocurement.client.python?branch=registry diff --git a/openprocurement_client/api_base_client.py b/openprocurement_client/api_base_client.py deleted file mode 100644 index 3ccc095..0000000 --- a/openprocurement_client/api_base_client.py +++ /dev/null @@ -1,250 +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 - -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) - - 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} - ) 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..eab51bf --- /dev/null +++ b/openprocurement_client/clients.py @@ -0,0 +1,424 @@ +# -*- 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'} + resource = '' + + def __init__(self, + key='', + resource=None, + 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() + # print (self.resource, resource) + self.resource = self.resource or resource + self.prefix_path = '{}/api/{}/{}'.format(self.host_url, + self.api_version, self.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 """ + + ########################################################################### + # 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) + ) + + def change_ownership(self, obj_id, transfer, access_token=None): + try: + self.create_resource_item_subitem( + obj_id, transfer, 'ownership', access_token=access_token + ) + except InvalidResponse as e: + if e.status_code == 200: + return munchify(loads(e.response.text)) + raise e + + +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..ceb41ac --- /dev/null +++ b/openprocurement_client/constants.py @@ -0,0 +1,20 @@ +# -*- 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' +MILESTONES = 'milestones' +PLANS = 'plans' +QUALIFICATIONS = 'qualifications' +QUALIFICATION_DOCUMENTS = 'qualificationDocuments' +QUESTIONS = 'questions' +TENDERS = 'tenders' +TRANSFERS = 'transfers' 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/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..e0b5da8 --- /dev/null +++ b/openprocurement_client/resources/assets.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import ASSETS + + +class AssetsClient(APIResourceClient): + """ Client for Openregistry Assets """ + + resource = ASSETS + + get_asset = APIResourceClient.get_resource_item + + get_assets = APIResourceClient.get_resource_items + + patch_asset = APIResourceClient.patch_resource_item diff --git a/openprocurement_client/resources/auctions.py b/openprocurement_client/resources/auctions.py new file mode 100644 index 0000000..eb1242f --- /dev/null +++ b/openprocurement_client/resources/auctions.py @@ -0,0 +1,236 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import ( + APIResourceClient, APIResourceClientSync +) +from openprocurement_client.constants import ( + AUCTIONS, + AWARDS, + BIDS, + CANCELLATIONS, + CONTRACTS, + DOCUMENTS, + QUESTIONS +) +from retrying import retry + + +class AuctionsClient(APIResourceClient): + """client for auctions""" + + resource = AUCTIONS + + ########################################################################### + # CREATE ITEM API METHODS + ########################################################################### + + def create_auction(self, auction): + return self.create_resource_item(auction) + + def create_question(self, auction_id, question, access_token=None): + return self.create_resource_item_subitem( + auction_id, question, QUESTIONS, access_token=access_token + ) + + def create_bid(self, auction_id, bid, access_token=None): + return self.create_resource_item_subitem( + auction_id, bid, BIDS, access_token=access_token + ) + + def create_cancellation(self, auction_id, cancellation, + access_token=None): + return self.create_resource_item_subitem( + auction_id, cancellation, CANCELLATIONS, + access_token=access_token + ) + + def create_thin_document(self, auction_id, document_data, access_token=None): + return self.create_resource_item_subitem( + auction_id, document_data, DOCUMENTS, access_token=access_token + ) + + ########################################################################### + # GET ITEMS LIST API METHODS + ########################################################################### + + @retry(stop_max_attempt_number=5) + def get_auctions(self, params=None, feed='changes'): + return self.get_resource_items(params=params, feed=feed) + + def get_latest_auctions(self, date): + return self.get_latest_resource_items(date) + + def get_questions(self, auction_id, access_token=None): + return self.get_resource_item_subitem(auction_id, QUESTIONS, + access_token=access_token) + + def get_documents(self, auction_id, access_token=None): + return self.get_resource_item_subitem(auction_id, DOCUMENTS, + access_token=access_token) + + def get_awards_documents(self, auction_id, award_id, access_token=None): + return self.get_resource_item_subitem( + auction_id, DOCUMENTS, depth_path='{}/{}'.format(AWARDS, award_id), + access_token=access_token + ) + + def get_awards(self, auction_id, access_token=None): + return self.get_resource_item_subitem(auction_id, AWARDS, + access_token=access_token) + + ########################################################################### + # GET ITEM API METHODS + ########################################################################### + + def get_auction(self, auction_id): + return self.get_resource_item(auction_id) + + def get_question(self, auction_id, question_id, access_token=None): + return self.get_resource_item_subitem( + auction_id, question_id, depth_path=QUESTIONS, + access_token=access_token + ) + + def get_bid(self, auction_id, bid_id, access_token=None): + return self.get_resource_item_subitem( + auction_id, bid_id, depth_path=BIDS, access_token=access_token + ) + + ########################################################################### + # PATCH ITEM API METHODS + ########################################################################### + + def patch_auction(self, auction_id, patch_data, access_token=None): + return self.patch_resource_item(auction_id, patch_data, + access_token=access_token) + + def patch_question(self, auction_id, question, question_id, + access_token=None): + return self.patch_resource_item_subitem( + auction_id, question, QUESTIONS, + subitem_id=question_id, access_token=access_token + ) + + def patch_bid(self, auction_id, bid, bid_id, access_token=None): + return self.patch_resource_item_subitem( + auction_id, bid, BIDS, subitem_id=bid_id, access_token=access_token + ) + + def patch_bid_document(self, auction_id, document_data, bid_id, + document_id, access_token=None): + depth_path = '{}/{}'.format(BIDS, bid_id) + return self.patch_resource_item_subitem( + auction_id, document_data, DOCUMENTS, subitem_id=document_id, + depth_path=depth_path, access_token=access_token + ) + + def patch_award(self, auction_id, award, award_id, access_token=None): + return self.patch_resource_item_subitem( + auction_id, award, AWARDS, subitem_id=award_id, + access_token=access_token + ) + + def patch_award_document(self, auction_id, document_data, award_id, + document_id, access_token=None): + depth_path = '{}/{}'.format(AWARDS, award_id) + return self.patch_resource_item_subitem( + auction_id, document_data, DOCUMENTS, subitem_id=document_id, + depth_path=depth_path, access_token=access_token + ) + + def patch_cancellation(self, auction_id, cancellation, cancellation_id, + access_token=None): + return self.patch_resource_item_subitem( + auction_id, cancellation, CANCELLATIONS, + subitem_id=cancellation_id, access_token=access_token + ) + + def patch_cancellation_document(self, auction_id, cancellation, cancellation_id, + cancellation_doc_id, access_token=None): + depth_path = '{}/{}'.format(CANCELLATIONS, cancellation_id) + return self.patch_resource_item_subitem( + auction_id, cancellation, DOCUMENTS, subitem_id=cancellation_doc_id, + depth_path=depth_path, access_token=access_token + ) + + def patch_contract(self, auction_id, contract, contract_id, access_token=None): + return self.patch_resource_item_subitem( + auction_id, contract, CONTRACTS, subitem_id=contract_id, + access_token=access_token + ) + + def patch_contract_document(self, auction_id, document_data, contract_id, + document_id, access_token=None): + return self.patch_resource_item_subitem( + auction_id, document_data, DOCUMENTS, subitem_id=document_id, + depth_path='{}/{}'.format(CONTRACTS, contract_id), + access_token=access_token + ) + + ########################################################################### + # UPLOAD FILE API METHODS + ########################################################################### + + def upload_bid_document(self, file_, auction_id, bid_id, + doc_registration=True, access_token=None, + doc_type=DOCUMENTS): + depth_path = '{}/{}'.format(BIDS, bid_id) + return self.upload_document( + file_, auction_id, doc_registration, + access_token, depth_path, doc_type + ) + + def upload_cancellation_document(self, file_, auction_id, cancellation_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(CANCELLATIONS, cancellation_id) + return self.upload_document(file_, auction_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_award_document(self, file_, auction_id, award_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(AWARDS, award_id) + return self.upload_document(file_, auction_id, + doc_registration=doc_registration, + access_token=access_token, + depth_path=depth_path) + + def upload_contract_document(self, file_, auction_id, contract_id, + doc_registration=True, access_token=None): + depth_path = '{}/{}'.format(CONTRACTS, contract_id) + return self.upload_document(file_, auction_id, doc_registration, + access_token, depth_path) + + ########################################################################### + # UPDATE FILE API METHODS + ########################################################################### + + def update_bid_document(self, file_, auction_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_, auction_id, document_id, doc_registration, + access_token, depth_path, doc_type + ) + + def update_cancellation_document(self, file_, auction_id, cancellation_id, + document_id, doc_registration=True, + access_token=None): + depth_path = '{}/{}'.format(CANCELLATIONS, cancellation_id) + return self.update_document( + file_, auction_id, document_id, doc_registration, + access_token, depth_path + ) + + ########################################################################### + # DELETE ITEMS LIST API METHODS + ########################################################################### + + def delete_bid(self, auction_id, bid_id, access_token=None): + return self.delete_resource_item_subitem( + auction_id, BIDS, bid_id, access_token=access_token + ) + + ########################################################################### + diff --git a/openprocurement_client/resources/contracts.py b/openprocurement_client/resources/contracts.py new file mode 100644 index 0000000..a02e731 --- /dev/null +++ b/openprocurement_client/resources/contracts.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import CHANGES, CONTRACTS, MILESTONES + + +class ContractingClient(APIResourceClient): + """ Contracting client """ + resource = CONTRACTS + + 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 + ) + + def patch_milestone(self, contract_id, milestone_id, access_token, data): + return self.patch_resource_item_subitem( + contract_id, data, MILESTONES, milestone_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..383ad9a --- /dev/null +++ b/openprocurement_client/resources/lots.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import LOTS, CONTRACTS + + +class LotsClient(APIResourceClient): + """ Client for Openregistry Lots """ + + resource = LOTS + + get_lot = APIResourceClient.get_resource_item + + get_lots = APIResourceClient.get_resource_items + + patch_lot = APIResourceClient.patch_resource_item + + def patch_auction(self, lot_id, data, auction_id, access_token): + return self.patch_resource_item_subitem( + lot_id, {'data': data}, 'auctions', auction_id, access_token=access_token + ) + + def patch_contract(self, lot_id, contract_id, access_token, data): + if not access_token: + # used by caravan + return self.patch_resource_item_subitem(lot_id, data, CONTRACTS, contract_id) + return self.patch_resource_item_subitem( + lot_id, data, CONTRACTS, contract_id, access_token=access_token + ) + + def get_contract(self, lot_id, contract_id): + return self.get_resource_item_subitem(lot_id, contract_id, depth_path=CONTRACTS) diff --git a/openprocurement_client/resources/plans.py b/openprocurement_client/resources/plans.py new file mode 100644 index 0000000..1da6bcf --- /dev/null +++ b/openprocurement_client/resources/plans.py @@ -0,0 +1,52 @@ +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 + + ########################################################################### + # 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..a3e9b4c --- /dev/null +++ b/openprocurement_client/resources/tenders.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import ( + APIResourceClient, APIResourceClientSync +) +from openprocurement_client.constants import ( + AWARDS, BIDS, CANCELLATIONS, COMPLAINTS, CONTRACTS, DOCUMENTS, + LOTS, QUALIFICATIONS, QUESTIONS, TENDERS +) +from retrying import retry + + +class TendersClient(APIResourceClient): + """client for tenders""" + resource = TENDERS + + ########################################################################### + # CREATE ITEM API METHODS + ########################################################################### + + def create_tender(self, tender): + return self.create_resource_item(tender) + + def create_question(self, tender_id, question, access_token=None): + return self.create_resource_item_subitem( + tender_id, question, QUESTIONS, access_token=access_token + ) + + def create_bid(self, tender_id, bid, access_token=None): + return self.create_resource_item_subitem( + tender_id, bid, BIDS, access_token=access_token + ) + + def create_lot(self, tender_id, lot, access_token=None): + return self.create_resource_item_subitem( + tender_id, lot, LOTS, access_token=access_token + ) + + def create_award(self, tender_id, award, access_token=None): + return self.create_resource_item_subitem( + tender_id, award, AWARDS, access_token=access_token + ) + + def create_cancellation(self, tender_id, cancellation, access_token=None): + return self.create_resource_item_subitem( + tender_id, cancellation, CANCELLATIONS, access_token=access_token + ) + + def create_complaint(self, tender_id, complaint, access_token=None): + return self.create_resource_item_subitem( + tender_id, complaint, COMPLAINTS, access_token=access_token + ) + + def create_award_complaint(self, tender_id, complaint, award_id, + access_token=None): + depth_path = '{}/{}'.format(AWARDS, award_id) + return self.create_resource_item_subitem( + tender_id, complaint, COMPLAINTS, + depth_path=depth_path, access_token=access_token + ) + + def create_thin_document(self, tender_id, document_data, access_token=None): + return self.create_resource_item_subitem( + tender_id, document_data, DOCUMENTS, access_token=access_token + ) + + ########################################################################### + # 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, access_token=None): + return self.get_resource_item_subitem(tender_id, QUESTIONS, + access_token=access_token) + + def get_documents(self, tender_id, access_token=None): + return self.get_resource_item_subitem(tender_id, DOCUMENTS, + access_token=access_token) + + def get_awards_documents(self, tender_id, award_id, access_token=None): + return self.get_resource_item_subitem( + tender_id, DOCUMENTS, depth_path='{}/{}'.format(AWARDS, award_id), + access_token=access_token + ) + + def get_qualification_documents(self, tender_id, qualification_id, + access_token=None): + return self.get_resource_item_subitem( + tender_id, DOCUMENTS, + depth_path='{}/{}'.format(QUALIFICATIONS, qualification_id), + access_token=access_token + ) + + def get_awards(self, tender_id, access_token=None): + return self.get_resource_item_subitem(tender_id, AWARDS, + access_token=access_token) + + def get_lots(self, tender_id, access_token=None): + return self.get_resource_item_subitem(tender_id, LOTS, + access_token=access_token) + + ########################################################################### + # 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, access_token=None): + return self.get_resource_item_subitem( + tender_id, question_id, depth_path=QUESTIONS, + access_token=access_token + ) + + 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, access_token=None): + return self.get_resource_item_subitem( + tender_id, lot_id, depth_path=LOTS, access_token=access_token + ) + + ########################################################################### + # PATCH ITEM API METHODS + ########################################################################### + + def patch_tender(self, tender_id, patch_data, access_token=None): + return self.patch_resource_item(tender_id, patch_data, + access_token=access_token) + + def patch_question(self, tender_id, question, question_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, question, QUESTIONS, + subitem_id=question_id, access_token=access_token + ) + + def patch_bid(self, tender_id, bid, bid_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, bid, BIDS, subitem_id=bid_id, access_token=access_token + ) + + def patch_bid_document(self, tender_id, document_data, bid_id, + document_id, access_token=None): + 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, access_token=access_token + ) + + def patch_award(self, tender_id, award, award_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, award, AWARDS, subitem_id=award_id, + access_token=access_token + ) + + def patch_award_document(self, tender_id, document_data, award_id, + document_id, access_token=None): + 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, access_token=access_token + ) + + def patch_cancellation(self, tender_id, cancellation, cancellation_id, + access_token=None): + return self.patch_resource_item_subitem( + tender_id, cancellation, CANCELLATIONS, + subitem_id=cancellation_id, access_token=access_token + ) + + def patch_cancellation_document(self, tender_id, cancellation, + cancellation_id, cancellation_doc_id, + access_token=None): + 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, + access_token=access_token + ) + + def patch_complaint(self, tender_id, complaint, complaint_id, + access_token=None): + return self.patch_resource_item_subitem( + tender_id, complaint, COMPLAINTS, subitem_id=complaint_id, + access_token=access_token + ) + + def patch_award_complaint(self, tender_id, complaint, award_id, + complaint_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, complaint, COMPLAINTS, subitem_id=complaint_id, + depth_path='{}/{}'.format(AWARDS, award_id), + access_token=access_token + ) + + def patch_lot(self, tender_id, lot, lot_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, lot, LOTS, subitem_id=lot_id, access_token=access_token + ) + + def patch_qualification(self, tender_id, qualification, qualification_id, + access_token=None): + return self.patch_resource_item_subitem( + tender_id, qualification, QUALIFICATIONS, + subitem_id=qualification_id, access_token=access_token + ) + + def patch_contract(self, tender_id, contract, contract_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, contract, CONTRACTS, subitem_id=contract_id, + access_token=access_token + ) + + def patch_contract_document(self, tender_id, document_data, contract_id, + document_id, access_token=None): + return self.patch_resource_item_subitem( + tender_id, document_data, DOCUMENTS, + subitem_id=document_id, + depth_path='{}/{}'.format(CONTRACTS, contract_id), + access_token=access_token + ) + + ########################################################################### + # 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): + resource = TENDERS + + sync_tenders = APIResourceClientSync.sync_resource_items + + get_tender = APIResourceClientSync.get_resource_item diff --git a/openprocurement_client/resources/transfers.py b/openprocurement_client/resources/transfers.py new file mode 100644 index 0000000..88e3fe0 --- /dev/null +++ b/openprocurement_client/resources/transfers.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +from openprocurement_client.clients import APIResourceClient +from openprocurement_client.constants import TRANSFERS + + +class TransfersClient(APIResourceClient): + """ Client for OpenProcurement Transfers """ + + resource = TRANSFERS + + get_transfer = APIResourceClient.get_resource_item + + create_transfer = APIResourceClient.create_resource_item + + patch_transfer = APIResourceClient.patch_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 62e9dbf..0c202e4 --- a/openprocurement_client/tests/_server.py +++ b/openprocurement_client/tests/_server.py @@ -1,10 +1,15 @@ 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, \ - TEST_PLAN_KEYS, TEST_CONTRACT_KEYS +from openprocurement_client.tests.data_dict import ( + TEST_TENDER_KEYS, + TEST_PLAN_KEYS, + TEST_CONTRACT_KEYS, + TEST_ASSET_KEYS, + TEST_LOT_KEYS +) import magic import os @@ -23,10 +28,13 @@ CONTRACTS_PATH = API_PATH.format('contracts') SPORE_PATH = API_PATH.format('spore') DOWNLOAD_URL_EXTENSION = 'some_key_etc' -RESOURCE_DICT = \ - {'tender': {'sublink': 'tenders', 'data': TEST_TENDER_KEYS}, - 'contract': {'sublink': 'contracts', 'data': TEST_CONTRACT_KEYS}, - 'plan': {'sublink': 'plans', 'data': TEST_PLAN_KEYS}} +RESOURCE_DICT = { + 'tender': {'sublink': 'tenders', 'data': TEST_TENDER_KEYS}, + 'contract': {'sublink': 'contracts', 'data': TEST_CONTRACT_KEYS}, + 'plan': {'sublink': 'plans', 'data': TEST_PLAN_KEYS}, + 'asset': {'sublink': 'assets', 'data': TEST_ASSET_KEYS}, + 'lot': {'sublink': 'lots', 'data': TEST_LOT_KEYS} +} def resource_filter(resource_name): @@ -56,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): @@ -78,9 +87,7 @@ def resource_page_get(resource_name): return dumps(resources) -### Tender operations -# - +# Tender operations def resource_create(): response.status = 201 @@ -98,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) @@ -170,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): @@ -182,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: @@ -210,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: @@ -227,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: @@ -247,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): @@ -265,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): @@ -292,45 +308,55 @@ 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), - "tender_create": (TENDERS_PATH, 'POST', resource_create), - "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), - "tender_subpage_item_create": (TENDERS_PATH + "//", 'POST', resource_subpage_item_create), - "tender_award_documents": (TENDERS_PATH + "//awards//documents", 'GET', tender_award_documents), - "tender_qualification_documents": (TENDERS_PATH + "//qualifications//documents", 'GET', tender_qualification_documents), - "tender_subpage_document_create": (TENDERS_PATH + "////", 'POST', tender_subpage_document_create), - "tender_subpage_document_update": (TENDERS_PATH + "/////", 'PUT', tender_subpage_document_update), - "tender_subpage_document_patch": (TENDERS_PATH + "/////", 'PATCH', tender_subpage_document_patch), - "tender_subpage_item": (TENDERS_PATH + "///", 'GET', tender_subpage_item), - "tender_subpage_item_patch": (API_PATH.format('') + '///', 'PATCH', object_subpage_item_patch), - "tender_subpage_item_delete": (TENDERS_PATH + "///", 'DELETE', tender_subpage_item_delete), - "tender_patch_credentials": (API_PATH.format('') + '//credentials', 'PATCH', patch_credentials), - "redirect": ('/redirect/', 'GET', get_file), - "download": ('/download/', 'GET', download_file), - "plans": (API_PATH.format(''), 'GET', resource_page_get), - "plan_create": (PLANS_PATH, 'POST', resource_create), - "plan_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), - "plan": (API_PATH.format('') + '/', 'GET', resource_page), - "plan_offset_error": (API_PATH.format(''), 'GET', offset_error), - "contracts": (API_PATH.format(''), 'GET', resource_page_get), - "contract_create": (CONTRACTS_PATH, 'POST', resource_create), - "contract_document_create": (CONTRACTS_PATH + "//documents", 'POST', contract_document_create), - "contract": (API_PATH.format('') + '/', 'GET', resource_page), - "contract_subpage_item_create": (CONTRACTS_PATH + "//", 'POST', resource_subpage_item_create), - "contract_subpage_item_patch": (API_PATH.format('') + '///', 'PATCH', object_subpage_item_patch), - "contract_change_patch": (API_PATH.format('contracts') + '//changes/', 'PATCH', contract_change_patch), - "contract_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), - "contract_patch_credentials": (API_PATH.format('') + '//credentials', 'PATCH', patch_credentials), - } + "spore": (SPORE_PATH, 'HEAD', spore), + "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_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), + "tender_document_create": (TENDERS_PATH + "//documents", 'POST', tender_document_create), + "tender_subpage": (TENDERS_PATH + "//", 'GET', tender_subpage), + "tender_subpage_item_create": (TENDERS_PATH + "//", 'POST', resource_subpage_item_create), + "tender_award_documents": (TENDERS_PATH + "//awards//documents", 'GET', tender_award_documents), + "tender_qualification_documents": (TENDERS_PATH + "//qualifications//documents", 'GET', tender_qualification_documents), + "tender_subpage_document_create": (TENDERS_PATH + "////", 'POST', tender_subpage_document_create), + "tender_subpage_document_update": (TENDERS_PATH + "/////", 'PUT', tender_subpage_document_update), + "tender_subpage_document_patch": (TENDERS_PATH + "/////", 'PATCH', tender_subpage_document_patch), + "tender_subpage_item": (TENDERS_PATH + "///", 'GET', tender_subpage_item), + "tender_subpage_item_patch": (API_PATH.format('') + '///', 'PATCH', object_subpage_item_patch), + "tender_subpage_item_delete": (TENDERS_PATH + "///", 'DELETE', tender_subpage_item_delete), + "tender_patch_credentials": (API_PATH.format('') + '//credentials', 'PATCH', patch_credentials), + "redirect": ('/redirect/', 'GET', get_file), + "download": ('/download/', 'GET', download_file), + "plans": (API_PATH.format(''), 'GET', resource_page_get), + "plan_create": (PLANS_PATH, 'POST', resource_create), + "plan_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), + "plan": (API_PATH.format('') + '/', 'GET', resource_page), + "plan_offset_error": (API_PATH.format(''), 'GET', offset_error), + "contracts": (API_PATH.format(''), 'GET', resource_page_get), + "contract_create": (CONTRACTS_PATH, 'POST', resource_create), + "contract_document_create": (CONTRACTS_PATH + "//documents", 'POST', contract_document_create), + "contract": (API_PATH.format('') + '/', 'GET', resource_page), + "contract_subpage_item_create": (CONTRACTS_PATH + "//", 'POST', resource_subpage_item_create), + "contract_subpage_item_patch": (API_PATH.format('') + '///', 'PATCH', object_subpage_item_patch), + "contract_change_patch": (API_PATH.format('contracts') + '//changes/', 'PATCH', contract_change_patch), + "contract_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), + "contract_patch_credentials": (API_PATH.format('') + '//credentials', 'PATCH', patch_credentials), + "assets": (API_PATH.format(''), 'GET', resource_page_get), + "asset": (API_PATH.format('') + '/', 'GET', resource_page), + "asset_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), + "lots": (API_PATH.format(''), 'GET', resource_page_get), + "lot": (API_PATH.format('') + '/', 'GET', resource_page), + "lot_patch": (API_PATH.format('') + "/", 'PATCH', resource_patch), +} def file_info(file_): diff --git a/openprocurement_client/tests/data/asset_668c3156c8cb496fb28359909cde6e96.json b/openprocurement_client/tests/data/asset_668c3156c8cb496fb28359909cde6e96.json new file mode 100644 index 0000000..27e133b --- /dev/null +++ b/openprocurement_client/tests/data/asset_668c3156c8cb496fb28359909cde6e96.json @@ -0,0 +1,272 @@ +{ + "access": { + "token": "0fc58c83963d42a692db4987df86640a" + }, + "data": { + "procurementMethod": "limited", + "status": "cancelled", + "documents": [ + { + "format": "text/plain", + "url": "https://api-sandbox.openprocurement.org/api/0.12/tenders/668c3156c8cb496fb28359909cde6e96/documents/3f82c7ef80a745119b6a6dec2670e5bc?download=0fc58c83963d42a692db4987df86640a", + "title": "/tmp/tmp5k4QEL", + "documentOf": "tender", + "datePublished": "2016-02-18T14:45:40.348569+02:00", + "dateModified": "2016-02-18T14:45:40.348612+02:00", + "id": "3f82c7ef80a745119b6a6dec2670e5bc" + } + ], + "title": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0448\u043a\u0456\u043b\u044c\u043d\u0438\u0445 \u0457\u0434\u0430\u043b\u0435\u043d\u044c", + "contracts": [ + { + "status": "pending", + "awardID": "70eaaceee81e4f378e333c1ed8258652", + "id": "c65d3526cdc44f75b2457b4489970a7c" + } + ], + "items": [ + { + "description": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0448\u043a\u0456\u043b\u044c\u043d\u0438\u0445 \u0457\u0434\u0430\u043b\u0435\u043d\u044c", + "classification": { + "scheme": "CPV", + "description": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0437 \u0445\u0430\u0440\u0447\u0443\u0432\u0430\u043d\u043d\u044f \u0443 \u0448\u043a\u043e\u043b\u0430\u0445", + "id": "55523100-3" + }, + "additionalClassifications": [ + { + "scheme": "\u0414\u041a\u041f\u041f", + "id": "55.51.10.300", + "description": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0448\u043a\u0456\u043b\u044c\u043d\u0438\u0445 \u0457\u0434\u0430\u043b\u0435\u043d\u044c" + } + ], + "deliveryLocation": { + "latitude": 49.85, + "longitude": 24.0167 + }, + "deliveryAddress": { + "locality": "\u043c. \u041a\u0438\u0457\u0432", + "region": "\u043c. \u041a\u0438\u0457\u0432", + "countryName_en": "Ukraine", + "countryName": "\u0423\u043a\u0440\u0430\u0457\u043d\u0430", + "streetAddress": "481 John Pine Suite 803", + "countryName_ru": "\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "postalCode": "58662" + }, + "deliveryDate": { + "endDate": "2016-02-23T14:42:28.297330+02:00" + }, + "id": "2dc54675d6364e2baffbc0f8e74432ac", + "unit": { + "code": "MON", + "name": "month" + }, + "quantity": 9 + } + ], + "complaints": [ + { + "status": "resolved", + "documents": [ + { + "author": "complaint_owner", + "format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "url": "https://lb.api-sandbox.openprocurement.org/api/0.12/tenders/cdec9f009be44f53817754b12d292ef2/complaints/22b086222a7a4bc6a9cc8adeaa91a57f/documents/129f4013b33a45b8bc70699a62a81499?download=7a6787929a624dd8b7c067974b9e8a96", + "title": "/tmp/tmpcFrLvz.docx", + "documentOf": "tender", + "datePublished": "2016-02-29T15:05:54.557614+02:00", + "dateModified": "2016-02-29T15:05:54.557656+02:00", + "id": "129f4013b33a45b8bc70699a62a81499" + } + ], + "description": "Consequuntur sint cupiditate impedit et magnam aut odit eligendi dicta magnam dicta eveniet.", + "tendererActionDate": "2016-02-29T15:06:05.212783+02:00", + "title": "Hic laboriosam magni cupiditate enim placeat in dolorem.", + "resolutionType": "resolved", + "resolution": "Non ipsa accusantium iste et dolorum quo temporibus deserunt et provident suscipit.", + "satisfied": true, + "dateAnswered": "2016-02-29T15:06:05.212741+02:00", + "tendererAction": "Labore adipisci sequi corporis neque delectus ea aut dicta molestiae omnis voluptatem qui et.", + "dateSubmitted": "2016-02-29T15:06:01.956017+02:00", + "date": "2016-02-29T15:05:53.084688+02:00", + "type": "claim", + "id": "22b086222a7a4bc6a9cc8adeaa91a57f" + }, + { + "status": "cancelled", + "description": "Sit cumque iste eius corporis exercitationem voluptate.", + "title": "Commodi voluptatem earum sapiente doloremque.", + "cancellationReason": "prosto tak :)", + "dateCanceled": "2016-02-29T15:06:19.281225+02:00", + "date": "2016-02-29T15:06:17.694889+02:00", + "type": "claim", + "id": "d26fc2f475514652a53c0c551d984be3" + }, + { + "status": "cancelled", + "description": "Eos officia vel aliquid id qui provident suscipit eos nam amet.", + "title": "Voluptas debitis cupiditate deserunt inventore fugit quo nemo dicta.", + "cancellationReason": "prosto tak :)", + "dateCanceled": "2016-02-29T15:06:28.419955+02:00", + "dateSubmitted": "2016-02-29T15:06:26.927454+02:00", + "date": "2016-02-29T15:06:24.962751+02:00", + "type": "claim", + "id": "816eb0529d6d41148dac0edabc119407" + }, + { + "status": "cancelled", + "dateCanceled": "2016-02-29T15:06:37.390093+02:00", + "tendererActionDate": "2016-02-29T15:06:35.767209+02:00", + "description": "Non voluptas ipsa ex quibusdam dolorem rem dolores sequi.", + "title": "Et esse eos unde molestiae cum sunt.", + "resolutionType": "resolved", + "resolution": "Cumque magni vitae officiis odit et mollitia neque dolores reprehenderit.", + "cancellationReason": "prosto tak :)", + "dateAnswered": "2016-02-29T15:06:35.767171+02:00", + "tendererAction": "Architecto dolor dolorem ut labore esse possimus odit a unde hic quas error alias nulla.", + "dateSubmitted": "2016-02-29T15:06:34.170608+02:00", + "date": "2016-02-29T15:06:32.488624+02:00", + "type": "claim", + "id": "ec053962ce334bdaaab8b3bf2e6b3173" + }, + { + "status": "cancelled", + "dateCanceled": "2016-02-29T15:06:55.181473+02:00", + "tendererActionDate": "2016-02-29T15:06:45.067477+02:00", + "description": "Et autem reiciendis eaque dolorem adipisci aut.", + "title": "Ipsum cupiditate ipsam ut non.", + "resolutionType": "resolved", + "resolution": "Qui vitae placeat rerum officia quia aperiam illo nisi velit et fuga ducimus dolor.", + "author": { + "contactPoint": { + "name": "Варвара Талан", + "telephone": "+38 (268) 501-16-45" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00007460", + "uri": "http://dummyimage.com/506x880" + }, + "name": "Терещук-Приймак", + "address": { + "locality": "м. Вінниця", + "region": "Вінницька область", + "countryName_en": "Ukraine", + "countryName": "Україна", + "streetAddress": "73741 John Pass", + "countryName_ru": "Украина", + "postalCode": "21100" + } + }, + "satisfied": false, + "dateEscalated": "2016-02-29T15:06:46.828487+02:00", + "dateAnswered": "2016-02-29T15:06:45.067440+02:00", + "tendererAction": "Ex optio cumque assumenda iure consectetur unde iste et consequatur at quaerat dolor.", + "dateSubmitted": "2016-02-29T15:06:43.326331+02:00", + "date": "2016-02-29T15:06:41.513902+02:00", + "cancellationReason": "prosto tak :)", + "type": "complaint", + "id": "76f0f99cb643434da21b6a6350cc2d0e" + } + ], + "procurementMethodType": "reporting", + "cancellations": [ + { + "status": "active", + "documents": [ + { + "description": "test description", + "format": "text/plain", + "url": "https://api-sandbox.openprocurement.org/api/0.12/tenders/668c3156c8cb496fb28359909cde6e96/cancellations/0dd6f9e8cc4f4d1c9c404d842b56d0d7/documents/1afca9faaf2b4f9489ee264b136371c6?download=57e135c2f8d342c3a506be20940a3f3c", + "title": "/tmp/tmpdhdoax", + "documentOf": "tender", + "datePublished": "2016-02-18T14:46:05.348204+02:00", + "dateModified": "2016-02-18T14:46:05.348246+02:00", + "id": "1afca9faaf2b4f9489ee264b136371c6" + }, + { + "description": "test description", + "format": "text/plain", + "url": "https://api-sandbox.openprocurement.org/api/0.12/tenders/668c3156c8cb496fb28359909cde6e96/cancellations/0dd6f9e8cc4f4d1c9c404d842b56d0d7/documents/1afca9faaf2b4f9489ee264b136371c6?download=9208a5fa7cf140a2bb4751a42a1f1ea9", + "title": "/tmp/tmpOBUlWP", + "documentOf": "tender", + "datePublished": "2016-02-18T14:46:05.348204+02:00", + "dateModified": "2016-02-18T14:46:08.516130+02:00", + "id": "1afca9faaf2b4f9489ee264b136371c6" + } + ], + "reason": "prost :))", + "date": "2016-02-18T14:46:04.344125+02:00", + "cancellationOf": "tender", + "id": "0dd6f9e8cc4f4d1c9c404d842b56d0d7" + } + ], + "value": { + "currency": "UAH", + "amount": 500000.0, + "valueAddedTaxIncluded": true + }, + "id": "668c3156c8cb496fb28359909cde6e96", + "awards": [ + { + "status": "active", + "complaintPeriod": { + "startDate": "2016-02-18T14:45:41.212240+02:00", + "endDate": "2016-02-19T14:45:42.178932+02:00" + }, + "suppliers": [ + { + "contactPoint": { + "telephone": "+380 (432) 21-69-30", + "name": "\u0421\u0435\u0440\u0433\u0456\u0439 \u041e\u043b\u0435\u043a\u0441\u044e\u043a", + "email": "soleksuk@gmail.com" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "13313462", + "uri": "http://sch10.edu.vn.ua/", + "legalName": "\u0414\u0435\u0440\u0436\u0430\u0432\u043d\u0435 \u043a\u043e\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u0435 \u043f\u0456\u0434\u043f\u0440\u0438\u0454\u043c\u0441\u0442\u0432\u043e \u0433\u0440\u043e\u043c\u0430\u0434\u0441\u044c\u043a\u043e\u0433\u043e \u0445\u0430\u0440\u0447\u0443\u0432\u0430\u043d\u043d\u044f \u00ab\u0428\u043a\u043e\u043b\u044f\u0440\u00bb" + }, + "name": "\u0414\u041a\u041f \u00ab\u0428\u043a\u043e\u043b\u044f\u0440\u00bb", + "address": { + "postalCode": "21100", + "countryName": "\u0423\u043a\u0440\u0430\u0457\u043d\u0430", + "streetAddress": "\u0432\u0443\u043b. \u041e\u0441\u0442\u0440\u043e\u0432\u0441\u044c\u043a\u043e\u0433\u043e, 33", + "region": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f", + "locality": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f" + } + } + ], + "value": { + "currency": "UAH", + "amount": 475000.0, + "valueAddedTaxIncluded": true + }, + "date": "2016-02-18T14:45:41.210928+02:00", + "id": "70eaaceee81e4f378e333c1ed8258652" + } + ], + "tenderID": "UA-2016-02-18-000031", + "owner": "test.quintagroup.com", + "dateModified": "2016-02-18T14:46:09.940361+02:00", + "procuringEntity": { + "contactPoint": { + "url": "http://sch10.edu.vn.ua/", + "name": "\u041a\u0443\u0446\u0430 \u0421\u0432\u0456\u0442\u043b\u0430\u043d\u0430 \u0412\u0430\u043b\u0435\u043d\u0442\u0438\u043d\u0456\u0432\u043d\u0430", + "telephone": "+380 (432) 46-53-02" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "21725150", + "legalName": "\u0417\u0430\u043a\u043b\u0430\u0434 \"\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u043e\u0441\u0432\u0456\u0442\u043d\u044f \u0448\u043a\u043e\u043b\u0430 \u0406-\u0406\u0406\u0406 \u0441\u0442\u0443\u043f\u0435\u043d\u0456\u0432 \u2116 10 \u0412\u0456\u043d\u043d\u0438\u0446\u044c\u043a\u043e\u0457 \u043c\u0456\u0441\u044c\u043a\u043e\u0457 \u0440\u0430\u0434\u0438\"" + }, + "name": "\u0417\u041e\u0421\u0428 #10 \u043c.\u0412\u0456\u043d\u043d\u0438\u0446\u0456", + "address": { + "postalCode": "21027", + "countryName": "\u0423\u043a\u0440\u0430\u0457\u043d\u0430", + "streetAddress": "\u0432\u0443\u043b. \u0421\u0442\u0430\u0445\u0443\u0440\u0441\u044c\u043a\u043e\u0433\u043e. 22", + "region": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f", + "locality": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f" + } + } + } +} diff --git a/openprocurement_client/tests/data/asset_823d50b3236247adad28a5a66f74db42.json b/openprocurement_client/tests/data/asset_823d50b3236247adad28a5a66f74db42.json new file mode 100644 index 0000000..27b7f37 --- /dev/null +++ b/openprocurement_client/tests/data/asset_823d50b3236247adad28a5a66f74db42.json @@ -0,0 +1,504 @@ +{ + "data":{ + "procurementMethod":"open", + "status":"complete", + "tenderPeriod":{ + "startDate":"2015-12-30T03:05:00+02:00", + "endDate":"2015-12-31T03:05:00+02:00" + }, + "qualifications": [ + { + "status": "pending", + "id": "cec4b82d2708465291fb4af79f8a3e52", + "bidID": "c41709780fa8434399c62a1facf369cb", + "documents": [ + { + "confidentiality": "public", + "language": "uk", + "title": "Proposal.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/documents/a05aff4e91c942bbad7e49e551399ec7?download=d0984048f149465699fd402bfe20881c", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.127526+02:00", + "id": "a05aff4e91c942bbad7e49e551399ec7", + "dateModified": "2016-02-24T14:13:36.127580+02:00" + }] + }, + { + "status": "pending", + "id": "9d562bbbf05e44f8a6a1f07ddb0415b1", + "bidID": "dda102997a4d4d5d8b5dac6ce2225e1c" + }, + { + "status": "pending", + "id": "36ac59607c434c8198730cc372672c8c", + "bidID": "6e0d218c8e964b42ac4a8ca086c100f9" + } + ], + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders/823d50b3236247adad28a5a66f74db42/documents/330822cbbd724671a1d2ff7c3a51dd52?download=5ff4917f29954e5f8dfea1feed8f7455", + "title":"test1.txt", + "documentOf":"tender", + "datePublished":"2015-12-12T03:06:53.209628+02:00", + "dateModified":"2015-12-12T03:06:53.209670+02:00", + "id":"330822cbbd724671a1d2ff7c3a51dd52" + }, + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders/823d50b3236247adad28a5a66f74db42/documents/6badc6f313d2499794bd39a3a7182fc0?download=ab2d7cdd346342a6a0d7b40af39145e2", + "title":"test3.txt", + "documentOf":"tender", + "datePublished":"2015-12-12T03:06:53.724784+02:00", + "dateModified":"2015-12-12T03:06:53.724826+02:00", + "id":"6badc6f313d2499794bd39a3a7182fc0" + }, + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders/823d50b3236247adad28a5a66f74db42/documents/08bcd56de9aa43faa684f8f8e7ab1c98?download=c7afbf3686cc441db7735d5ec284a0de", + "title":"test2.txt", + "documentOf":"tender", + "datePublished":"2015-12-12T03:06:54.280311+02:00", + "dateModified":"2015-12-12T03:06:54.280354+02:00", + "id":"08bcd56de9aa43faa684f8f8e7ab1c98" + } + ], + "description":"Multilot 4 descr", + "title":"Multilot 4 title", + "submissionMethodDetails":"quick", + "items":[ + { + "relatedLot":"7d774fbf1e86420484c7d1a005cc283f", + "description":"itelm descr", + "classification":{ + "scheme":"CPV", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u0444\u0435\u0440\u043c\u0435\u0440\u0441\u0442\u0432\u0430, \u0440\u0438\u0431\u0430\u043b\u044c\u0441\u0442\u0432\u0430, \u043b\u0456\u0441\u043d\u0438\u0446\u0442\u0432\u0430 \u0442\u0430 \u0441\u0443\u043c\u0456\u0436\u043d\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f", + "id":"03000000-1" + }, + "additionalClassifications":[ + { + "scheme":"\u0414\u041a\u041f\u041f", + "id":"01", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u043c\u0438\u0441\u043b\u0438\u0432\u0441\u0442\u0432\u0430 \u0442\u0430 \u043f\u043e\u0432\u2019\u044f\u0437\u0430\u043d\u0456 \u0437 \u0446\u0438\u043c \u043f\u043e\u0441\u043b\u0443\u0433\u0438" + } + ], + "deliveryDate":{ + "startDate":"2016-06-01T00:00:00+03:00" + }, + "id":"7caa3587fe9041da98f2744ef531d7ce", + "unit":{ + "code":"LO", + "name":"\u043b\u043e\u0442" + }, + "quantity":4 + }, + { + "relatedLot":"7d774fbf1e86420484c7d1a005cc283f", + "description":"itelm descr", + "classification":{ + "scheme":"CPV", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u0444\u0435\u0440\u043c\u0435\u0440\u0441\u0442\u0432\u0430, \u0440\u0438\u0431\u0430\u043b\u044c\u0441\u0442\u0432\u0430, \u043b\u0456\u0441\u043d\u0438\u0446\u0442\u0432\u0430 \u0442\u0430 \u0441\u0443\u043c\u0456\u0436\u043d\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f", + "id":"03000000-1" + }, + "additionalClassifications":[ + { + "scheme":"\u0414\u041a\u041f\u041f", + "id":"01", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u043c\u0438\u0441\u043b\u0438\u0432\u0441\u0442\u0432\u0430 \u0442\u0430 \u043f\u043e\u0432\u2019\u044f\u0437\u0430\u043d\u0456 \u0437 \u0446\u0438\u043c \u043f\u043e\u0441\u043b\u0443\u0433\u0438" + } + ], + "deliveryDate":{ + "startDate":"2016-06-01T00:00:00+03:00" + }, + "id":"563ef5d999f34d36a5a0e4e4d91d7be1", + "unit":{ + "code":"LO", + "name":"\u043b\u043e\u0442" + }, + "quantity":4 + } + ], + "id":"823d50b3236247adad28a5a66f74db42", + "value":{ + "currency":"UAH", + "amount":1000.0, + "valueAddedTaxIncluded":false + }, + "submissionMethod":"electronicAuction", + "procuringEntity":{ + "contactPoint":{ + "telephone":"", + "name":"Dmitry", + "email":"d.karnaukh+merchant1@smartweb.com.ua" + }, + "identifier":{ + "scheme":"UA-EDR", + "id":"66666692", + "legalName":"\u041d\u0410\u041a \"Testov Test\"" + }, + "name":"\u041d\u0410\u041a \"Testov Test\"", + "address":{ + "postalCode":"02121", + "countryName":"\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "streetAddress":"", + "region":"\u041a\u0438\u0435\u0432\u0441\u043a\u0430\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u044c", + "locality":"\u041a\u0438\u0435\u0432" + } + }, + "minimalStep":{ + "currency":"UAH", + "amount":1.0, + "valueAddedTaxIncluded":false + }, + "tenderID":"UA-2015-12-12-000003-1", + "questions":[ + { + "description":"Test lot question descr", + "title":"Test lot question title", + "relatedItem":"563ef5d999f34d36a5a0e4e4d91d7be1", + "answer":"Test answer", + "date":"2015-12-21T17:31:19.650440+02:00", + "id":"615ff8be8eba4a81b300036d6bec991c", + "questionOf":"lot" + }, + { + "description":"lot 2 question descr?", + "title":"lot 2 question title", + "relatedItem":"563ef5d999f34d36a5a0e4e4d91d7be1", + "answer":"lot 2 answer", + "date":"2015-12-21T17:36:30.680576+02:00", + "id":"278f269ee482434da6615a21f3deccf0", + "questionOf":"lot" + }, + { + "description":"lot 3 q descr?", + "title":"lot 3 q title", + "relatedItem":"563ef5d999f34d36a5a0e4e4d91d7be1", + "answer":"lot 3 answer", + "date":"2015-12-21T17:57:28.108596+02:00", + "id":"da6c1228f81e4c2e8451479d57b685dd", + "questionOf":"lot" + }, + { + "description":"Tender q 1 descr?", + "title":"Tender q 1 title", + "relatedItem":"ad681d050d0e42169a80b86d5b591c20", + "answer":"Tender q 1 answer", + "date":"2015-12-21T17:58:24.489846+02:00", + "id":"937b4b9ef3444782b9a3019fcc5c072a", + "questionOf":"tender" + }, + { + "description":"Item descr?", + "title":"Item title?", + "relatedItem":"7caa3587fe9041da98f2744ef531d7ce", + "answer":"Item answer", + "date":"2015-12-21T18:04:35.349936+02:00", + "id":"a2d14f4faf9b4ba180b40f08a2be3bd9", + "questionOf":"item" + } + ], + "enquiryPeriod":{ + "startDate":"2015-12-12T03:06:52.664385+02:00", + "endDate":"2015-12-30T03:05:00+02:00" + }, + "owner":"prom.ua", + "lots":[ + { + "status":"complete", + "description":"lot descr1", + "title":"lot title", + "minimalStep":{ + "currency":"UAH", + "amount":1.0, + "valueAddedTaxIncluded":false + }, + "value":{ + "currency":"UAH", + "amount":1000.0, + "valueAddedTaxIncluded":false + }, + "id":"7d774fbf1e86420484c7d1a005cc283f" + }, + { + "status":"active", + "description":"lot descr2", + "title":"lot title", + "minimalStep":{ + "currency":"UAH", + "amount":1.0, + "valueAddedTaxIncluded":false + }, + "value":{ + "currency":"UAH", + "amount":2000.0, + "valueAddedTaxIncluded":false + }, + "id":"563ef5d999f34d36a5a0e4e4d91d7be1" + } + ], + "bids":[ + { + "date": "2014-12-16T04:44:23.569815+02:00", + "documents": [ + { + "dateModified": "2014-12-16T04:44:25.010930+02:00", + "datePublished": "2014-12-16T04:44:25.010885+02:00", + "format": "text/plain", + "id": "ff001412c60c4164a0f57101e4eaf8aa", + "title": "Proposal.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0/tenders/6f73bf0f7f734f459f7e37e3787054a0/bids/f7fc1212f9f140bba5c4e3cd4f2b62d9/documents/ff001412c60c4164a0f57101e4eaf8aa?download=4f45bbd414104cd78faf620208efd824" + } + ], + "qualificationDocuments": [ + { + "confidentiality": "public", + "language": "uk", + "title": "qualification_document.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/financial_documents/7519d21b32af432396acd6e2c9e18ee5?download=5db59ae05361427b84c4caddb0b6d92b", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.625252+02:00", + "id": "7519d21b32af432396acd6e2c9e18ee5", + "dateModified": "2016-02-24T14:13:36.625290+02:00" + } + ], + "financial_documens": [ + { + "confidentiality": "public", + "language": "uk", + "title": "financial_doc.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/financial_documents/7519d21b32af432396acd6e2c9e18ee5?download=5db59ae05361427b84c4caddb0b6d92b", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.625252+02:00", + "id": "7519d21b32af432396acd6e2c9e18ee5", + "dateModified": "2016-02-24T14:13:36.625290+02:00" + } + ], + "eligibility_documents": [ + { + "confidentiality": "public", + "language": "uk", + "title": "eligibility_doc.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/financial_documents/7519d21b32af432396acd6e2c9e18ee5?download=5db59ae05361427b84c4caddb0b6d92b", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.625252+02:00", + "id": "7519d21b32af432396acd6e2c9e18ee5", + "dateModified": "2016-02-24T14:13:36.625290+02:00" + } + ], + "id": "f7fc1212f9f140bba5c4e3cd4f2b62d9", + "lotValues": [ + { + "value": {"currency": "UAH", "amount": 7750.0, "valueAddedTaxIncluded": true}, + "reatedLot": "7d774fbf1e86420484c7d1a005cc283f", + "date": "2015-11-01T12:43:12.482645+02:00" + }, { + "value": {"currency": "UAH", "amount": 8125.0, "valueAddedTaxIncluded": true}, + "reatedLot": "563ef5d999f34d36a5a0e4e4d91d7be1", + "date": "2015-11-01T12:43:12.482645+02:00" + } + ], + "tenderers": [ + { + "address": { + "countryName": "Україна", + "locality": "м. Вінниця", + "postalCode": "21100", + "region": "м. Вінниця", + "streetAddress": "вул. Островського, 33" + }, + "contactPoint": { + "email": "soleksuk@gmail.com", + "name": "Сергій Олексюк", + "telephone": "+380 (432) 21-69-30" + }, + "identifier": { + "id": "13313462", + "legalName": "Державне комунальне підприємство громадського харчування «Школяр»", + "scheme": "UA-EDR", + "uri": "http://sch10.edu.vn.ua/" + }, + "name": "ДКП «Школяр»" + } + ] + }, + { + "date": "2014-12-16T04:44:27.976478+02:00", + "id": "7ec725815ef448a9b857129024395638", + "lotValues": [ + { + "value": {"currency": "UAH", "amount": 7750.0, "valueAddedTaxIncluded": true}, + "reatedLot": "7d774fbf1e86420484c7d1a005cc283f", + "date": "2015-11-01T12:43:12.482645+02:00" + }, { + "value": {"currency": "UAH", "amount": 8125.0, "valueAddedTaxIncluded": true}, + "reatedLot": "563ef5d999f34d36a5a0e4e4d91d7be1", + "date": "2015-11-01T12:43:12.482645+02:00" + } + ], + "tenderers": [ + { + "address": { + "countryName": "Україна", + "locality": "м. Вінниця", + "postalCode": "21018", + "region": "м. Вінниця", + "streetAddress": "вул. Юності, 30" + }, + "contactPoint": { + "email": "alla.myhailova@i.ua", + "name": "Алла Михайлова", + "telephone": "+380 (432) 460-665" + }, + "identifier": { + "id": "13306232", + "legalName": "Державне комунальне підприємство громадського харчування «Меридіан»", + "scheme": "UA-EDR", + "uri": "http://sch10.edu.vn.ua/" + }, + "name": "ДКП «Меридіан2»" + } + ] + } + + ], + "awards":[ + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/documents/c7ac894e4ca34e618e03a02d8799dc69?download=8c4053b0942949f4bd6e998a51e177c5", + "title":"2.jpg", + "datePublished":"2015-11-13T16:31:03.691781+02:00", + "dateModified":"2015-11-13T16:31:03.691829+02:00", + "id":"c7ac894e4ca34e618e03a02d8799dc69" + } + ], + "complaintPeriod":{ + "startDate":"2015-11-13T16:17:00.375880+02:00", + "endDate":"2015-11-14T16:31:06.979761+02:00" + }, + "contracts":[ + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/contracts/045c478807c04215950b4d74e6861cb1/documents/14dbf158b9ed4a478a59d19c90dbdf5d?download=9d948173e65441409f06e0b81674b39a", + "title":"3.jpg", + "datePublished":"2015-11-13T19:01:16.748785+02:00", + "dateModified":"2015-11-13T19:01:16.748829+02:00", + "id":"14dbf158b9ed4a478a59d19c90dbdf5d" + } + ], + "awardID":"7054491a5e514699a56e44d32e23edf7", + "id":"045c478807c04215950b4d74e6861cb1" + } + ], + "suppliers":[ + { + "contactPoint":{ + "telephone":"", + "name":"some6", + "email":"some6@mailinator.com" + }, + "identifier":{ + "scheme":"UA-EDR", + "id":"62123463", + "legalName":"some6" + }, + "name":"some6", + "address":{ + "postalCode":"04119", + "countryName":"\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "streetAddress":"", + "region":"\u0410\u0420 \u041a\u0440\u044b\u043c", + "locality":"ffff" + } + } + ], + "bid_id":"ad9ac917ff0049178acc3613bb14a691", + "value":{ + "currency":"UAH", + "amount":300.0, + "valueAddedTaxIncluded":false + }, + "date":"2015-11-13T16:17:00.376251+02:00", + "id":"7054491a5e514699a56e44d32e23edf7", + "lotId": "563ef5d999f34d36a5a0e4e4d91d7be1" + }, + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/documents/c7ac894e4ca34e618e03a02d8799dc69?download=8c4053b0942949f4bd6e998a51e177c5", + "title":"2.jpg", + "datePublished":"2015-11-13T16:31:03.691781+02:00", + "dateModified":"2015-11-13T16:31:03.691829+02:00", + "id":"c7ac894e4ca34e618e03a02d8799dc69" + } + ], + "complaintPeriod":{ + "startDate":"2015-11-13T16:17:00.375880+02:00", + "endDate":"2015-11-14T16:31:06.979761+02:00" + }, + "contracts":[ + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/contracts/045c478807c04215950b4d74e6861cb1/documents/14dbf158b9ed4a478a59d19c90dbdf5d?download=9d948173e65441409f06e0b81674b39a", + "title":"3.jpg", + "datePublished":"2015-11-13T19:01:16.748785+02:00", + "dateModified":"2015-11-13T19:01:16.748829+02:00", + "id":"14dbf158b9ed4a478a59d19c90dbdf5d" + } + ], + "awardID":"7054491a5e514699a56e44d32e23edf7", + "id":"045c478807c04215950b4d74e6861cb1" + } + ], + "suppliers":[ + { + "contactPoint":{ + "telephone":"", + "name":"some6", + "email":"some6@mailinator.com" + }, + "identifier":{ + "scheme":"UA-EDR", + "id":"62123463", + "legalName":"some6" + }, + "name":"some6", + "address":{ + "postalCode":"04119", + "countryName":"\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "streetAddress":"", + "region":"\u0410\u0420 \u041a\u0440\u044b\u043c", + "locality":"ffff" + } + } + ], + "bid_id":"ad9ac917ff0049178acc3613bb14a691", + "value":{ + "currency":"UAH", + "amount":300.0, + "valueAddedTaxIncluded":false + }, + "date":"2015-11-13T16:17:00.376251+02:00", + "id":"7054491a5e514699a56e44d32e23edf7", + "lotId": "7d774fbf1e86420484c7d1a005cc283f" + } + ], + "dateModified":"2015-12-12T03:06:54.370277+02:00", + "awardCriteria":"lowestCost" + } +} diff --git a/openprocurement_client/tests/data/assets.json b/openprocurement_client/tests/data/assets.json new file mode 100644 index 0000000..384ef8a --- /dev/null +++ b/openprocurement_client/tests/data/assets.json @@ -0,0 +1,153 @@ +{ + "next_page":{ + "path":"/api/0.10/tenders?offset=2015-12-25T18%3A04%3A36.264176%2B02%3A00", + "uri":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders?offset=2015-12-25T18%3A04%3A36.264176%2B02%3A00", + "offset":"2015-12-25T18:04:36.264176+02:00" + }, + "data":[ + { + "id":"823d50b3236247adad28a5a66f74db42", + "dateModified":"2015-11-13T18:50:00.753811+02:00" + }, + { + "id":"f3849ade33534174b8402579152a5f41", + "dateModified":"2015-11-16T01:15:00.469896+02:00" + }, + { + "id":"f3849ade33534174b8402579152a5f41", + "dateModified":"2015-11-16T12:00:00.960077+02:00" + }, + { + "id":"3be3664065994d1e8847beb597c014bf", + "dateModified":"2015-11-16T21:15:00.404214+02:00" + }, + { + "id":"711cfa82c54147b686f6755adb22364d", + "dateModified":"2015-11-17T18:02:00.456050+02:00" + }, + { + "id":"730fe73cb7be4f92bce472484236e9ee", + "dateModified":"2015-11-18T18:30:00.410770+02:00" + }, + { + "id":"496ec128323f45679daf9ebcb1ec62f6", + "dateModified":"2015-11-20T16:20:37.977312+02:00" + }, + { + "id":"18d566bed8cc4f2e846ca5d64c6bdfed", + "dateModified":"2015-12-01T11:48:33.784585+02:00" + }, + { + "id":"21733c5ed0be4b259c3345310f425a82", + "dateModified":"2015-12-02T13:54:13.656648+02:00" + }, + { + "id":"b35a1151b7a84562addb47f66193a309", + "dateModified":"2015-12-12T02:45:36.355426+02:00" + }, + { + "id":"ce9751da26a84c2390368ffa5adfd74c", + "dateModified":"2015-12-12T03:04:16.059667+02:00" + }, + { + "id":"823d50b3236247adad28a5a66f74db42", + "dateModified":"2015-12-12T03:06:54.370277+02:00" + }, + { + "id":"0085cf76ad444ad0ae625fc17f366dd2", + "dateModified":"2015-12-12T14:30:38.596246+02:00" + }, + { + "id":"d97613278dd547f2be5523cdc5614880", + "dateModified":"2015-12-12T14:31:51.342309+02:00" + }, + { + "id":"71161344890c4231b1487296f56aa910", + "dateModified":"2015-12-14T13:10:38.771625+02:00" + }, + { + "id":"759a81e58cf04b5ea0587942d0925faa", + "dateModified":"2015-12-16T19:56:53.874070+02:00" + }, + { + "id":"ad681d050d0e42169a80b86d5b591c20", + "dateModified":"2015-12-22T13:14:25.253453+02:00" + }, + { + "id":"a2e4464a08d64addb6c7a63da48ddb87", + "dateModified":"2015-12-23T17:09:15.145867+02:00" + }, + { + "id":"eea0c2f3cf3f4206b8cbba813b9a527b", + "dateModified":"2015-12-23T17:15:23.479628+02:00" + }, + { + "id":"847411c7b03f4f91b97fecb70c6e6275", + "dateModified":"2015-12-23T17:54:39.470396+02:00" + }, + { + "id":"599e63e6fb1349078fb01e4f9e8a812c", + "dateModified":"2015-12-23T17:55:36.317529+02:00" + }, + { + "id":"d63a7302316c4d15b46aeb7bcd4f1dbe", + "dateModified":"2015-12-23T17:56:01.709307+02:00" + }, + { + "id":"6496299a3c8e482f9017d855483a97b5", + "dateModified":"2015-12-23T17:57:57.376623+02:00" + }, + { + "id":"a513848583cc4d1d806c78e5131ad068", + "dateModified":"2015-12-23T17:59:52.101031+02:00" + }, + { + "id":"79aa9fd2bb43471cbcb0b97f6965e5f6", + "dateModified":"2015-12-24T11:39:09.071184+02:00" + }, + { + "id":"b2cdcda417434c7b889c82a81927bb73", + "dateModified":"2015-12-24T11:40:20.172257+02:00" + }, + { + "id":"58eca935e0e24805b561a1588ae64ef3", + "dateModified":"2015-12-24T11:48:57.584694+02:00" + }, + { + "id":"78b889ec869544b9b47c8740549ed20a", + "dateModified":"2015-12-24T11:51:07.149689+02:00" + }, + { + "id":"258ef598759946dbb30e7ed67bfc2346", + "dateModified":"2015-12-24T11:52:16.196052+02:00" + }, + { + "id":"639ecaca9db24228a9fe0eb3494aa0e3", + "dateModified":"2015-12-25T13:28:22.136939+02:00" + }, + { + "id":"ca10599dfcbe416bb513416c13073c5f", + "dateModified":"2015-12-25T15:19:53.913160+02:00" + }, + { + "id":"21e551bf7f194d8aaba29bf2e4949b90", + "dateModified":"2015-12-25T16:00:00.319589+02:00" + }, + { + "id":"dd7b13a956b44a34bbaeb2bfa1910c03", + "dateModified":"2015-12-25T16:07:19.809837+02:00" + }, + { + "id":"f8f21e8b0c104747b8abf5ece4fb5d72", + "dateModified":"2015-12-25T17:06:32.885207+02:00" + }, + { + "id":"48dcb014d003454b8875975bb531147f", + "dateModified":"2015-12-25T17:16:05.208447+02:00" + }, + { + "id":"7f35a555d5c64b34a7cbc454fab5628c", + "dateModified":"2015-12-25T18:04:36.264176+02:00" + } + ] +} diff --git a/openprocurement_client/tests/data/lot_668c3156c8cb496fb28359909cde6e96.json b/openprocurement_client/tests/data/lot_668c3156c8cb496fb28359909cde6e96.json new file mode 100644 index 0000000..27e133b --- /dev/null +++ b/openprocurement_client/tests/data/lot_668c3156c8cb496fb28359909cde6e96.json @@ -0,0 +1,272 @@ +{ + "access": { + "token": "0fc58c83963d42a692db4987df86640a" + }, + "data": { + "procurementMethod": "limited", + "status": "cancelled", + "documents": [ + { + "format": "text/plain", + "url": "https://api-sandbox.openprocurement.org/api/0.12/tenders/668c3156c8cb496fb28359909cde6e96/documents/3f82c7ef80a745119b6a6dec2670e5bc?download=0fc58c83963d42a692db4987df86640a", + "title": "/tmp/tmp5k4QEL", + "documentOf": "tender", + "datePublished": "2016-02-18T14:45:40.348569+02:00", + "dateModified": "2016-02-18T14:45:40.348612+02:00", + "id": "3f82c7ef80a745119b6a6dec2670e5bc" + } + ], + "title": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0448\u043a\u0456\u043b\u044c\u043d\u0438\u0445 \u0457\u0434\u0430\u043b\u0435\u043d\u044c", + "contracts": [ + { + "status": "pending", + "awardID": "70eaaceee81e4f378e333c1ed8258652", + "id": "c65d3526cdc44f75b2457b4489970a7c" + } + ], + "items": [ + { + "description": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0448\u043a\u0456\u043b\u044c\u043d\u0438\u0445 \u0457\u0434\u0430\u043b\u0435\u043d\u044c", + "classification": { + "scheme": "CPV", + "description": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0437 \u0445\u0430\u0440\u0447\u0443\u0432\u0430\u043d\u043d\u044f \u0443 \u0448\u043a\u043e\u043b\u0430\u0445", + "id": "55523100-3" + }, + "additionalClassifications": [ + { + "scheme": "\u0414\u041a\u041f\u041f", + "id": "55.51.10.300", + "description": "\u041f\u043e\u0441\u043b\u0443\u0433\u0438 \u0448\u043a\u0456\u043b\u044c\u043d\u0438\u0445 \u0457\u0434\u0430\u043b\u0435\u043d\u044c" + } + ], + "deliveryLocation": { + "latitude": 49.85, + "longitude": 24.0167 + }, + "deliveryAddress": { + "locality": "\u043c. \u041a\u0438\u0457\u0432", + "region": "\u043c. \u041a\u0438\u0457\u0432", + "countryName_en": "Ukraine", + "countryName": "\u0423\u043a\u0440\u0430\u0457\u043d\u0430", + "streetAddress": "481 John Pine Suite 803", + "countryName_ru": "\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "postalCode": "58662" + }, + "deliveryDate": { + "endDate": "2016-02-23T14:42:28.297330+02:00" + }, + "id": "2dc54675d6364e2baffbc0f8e74432ac", + "unit": { + "code": "MON", + "name": "month" + }, + "quantity": 9 + } + ], + "complaints": [ + { + "status": "resolved", + "documents": [ + { + "author": "complaint_owner", + "format": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "url": "https://lb.api-sandbox.openprocurement.org/api/0.12/tenders/cdec9f009be44f53817754b12d292ef2/complaints/22b086222a7a4bc6a9cc8adeaa91a57f/documents/129f4013b33a45b8bc70699a62a81499?download=7a6787929a624dd8b7c067974b9e8a96", + "title": "/tmp/tmpcFrLvz.docx", + "documentOf": "tender", + "datePublished": "2016-02-29T15:05:54.557614+02:00", + "dateModified": "2016-02-29T15:05:54.557656+02:00", + "id": "129f4013b33a45b8bc70699a62a81499" + } + ], + "description": "Consequuntur sint cupiditate impedit et magnam aut odit eligendi dicta magnam dicta eveniet.", + "tendererActionDate": "2016-02-29T15:06:05.212783+02:00", + "title": "Hic laboriosam magni cupiditate enim placeat in dolorem.", + "resolutionType": "resolved", + "resolution": "Non ipsa accusantium iste et dolorum quo temporibus deserunt et provident suscipit.", + "satisfied": true, + "dateAnswered": "2016-02-29T15:06:05.212741+02:00", + "tendererAction": "Labore adipisci sequi corporis neque delectus ea aut dicta molestiae omnis voluptatem qui et.", + "dateSubmitted": "2016-02-29T15:06:01.956017+02:00", + "date": "2016-02-29T15:05:53.084688+02:00", + "type": "claim", + "id": "22b086222a7a4bc6a9cc8adeaa91a57f" + }, + { + "status": "cancelled", + "description": "Sit cumque iste eius corporis exercitationem voluptate.", + "title": "Commodi voluptatem earum sapiente doloremque.", + "cancellationReason": "prosto tak :)", + "dateCanceled": "2016-02-29T15:06:19.281225+02:00", + "date": "2016-02-29T15:06:17.694889+02:00", + "type": "claim", + "id": "d26fc2f475514652a53c0c551d984be3" + }, + { + "status": "cancelled", + "description": "Eos officia vel aliquid id qui provident suscipit eos nam amet.", + "title": "Voluptas debitis cupiditate deserunt inventore fugit quo nemo dicta.", + "cancellationReason": "prosto tak :)", + "dateCanceled": "2016-02-29T15:06:28.419955+02:00", + "dateSubmitted": "2016-02-29T15:06:26.927454+02:00", + "date": "2016-02-29T15:06:24.962751+02:00", + "type": "claim", + "id": "816eb0529d6d41148dac0edabc119407" + }, + { + "status": "cancelled", + "dateCanceled": "2016-02-29T15:06:37.390093+02:00", + "tendererActionDate": "2016-02-29T15:06:35.767209+02:00", + "description": "Non voluptas ipsa ex quibusdam dolorem rem dolores sequi.", + "title": "Et esse eos unde molestiae cum sunt.", + "resolutionType": "resolved", + "resolution": "Cumque magni vitae officiis odit et mollitia neque dolores reprehenderit.", + "cancellationReason": "prosto tak :)", + "dateAnswered": "2016-02-29T15:06:35.767171+02:00", + "tendererAction": "Architecto dolor dolorem ut labore esse possimus odit a unde hic quas error alias nulla.", + "dateSubmitted": "2016-02-29T15:06:34.170608+02:00", + "date": "2016-02-29T15:06:32.488624+02:00", + "type": "claim", + "id": "ec053962ce334bdaaab8b3bf2e6b3173" + }, + { + "status": "cancelled", + "dateCanceled": "2016-02-29T15:06:55.181473+02:00", + "tendererActionDate": "2016-02-29T15:06:45.067477+02:00", + "description": "Et autem reiciendis eaque dolorem adipisci aut.", + "title": "Ipsum cupiditate ipsam ut non.", + "resolutionType": "resolved", + "resolution": "Qui vitae placeat rerum officia quia aperiam illo nisi velit et fuga ducimus dolor.", + "author": { + "contactPoint": { + "name": "Варвара Талан", + "telephone": "+38 (268) 501-16-45" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "00007460", + "uri": "http://dummyimage.com/506x880" + }, + "name": "Терещук-Приймак", + "address": { + "locality": "м. Вінниця", + "region": "Вінницька область", + "countryName_en": "Ukraine", + "countryName": "Україна", + "streetAddress": "73741 John Pass", + "countryName_ru": "Украина", + "postalCode": "21100" + } + }, + "satisfied": false, + "dateEscalated": "2016-02-29T15:06:46.828487+02:00", + "dateAnswered": "2016-02-29T15:06:45.067440+02:00", + "tendererAction": "Ex optio cumque assumenda iure consectetur unde iste et consequatur at quaerat dolor.", + "dateSubmitted": "2016-02-29T15:06:43.326331+02:00", + "date": "2016-02-29T15:06:41.513902+02:00", + "cancellationReason": "prosto tak :)", + "type": "complaint", + "id": "76f0f99cb643434da21b6a6350cc2d0e" + } + ], + "procurementMethodType": "reporting", + "cancellations": [ + { + "status": "active", + "documents": [ + { + "description": "test description", + "format": "text/plain", + "url": "https://api-sandbox.openprocurement.org/api/0.12/tenders/668c3156c8cb496fb28359909cde6e96/cancellations/0dd6f9e8cc4f4d1c9c404d842b56d0d7/documents/1afca9faaf2b4f9489ee264b136371c6?download=57e135c2f8d342c3a506be20940a3f3c", + "title": "/tmp/tmpdhdoax", + "documentOf": "tender", + "datePublished": "2016-02-18T14:46:05.348204+02:00", + "dateModified": "2016-02-18T14:46:05.348246+02:00", + "id": "1afca9faaf2b4f9489ee264b136371c6" + }, + { + "description": "test description", + "format": "text/plain", + "url": "https://api-sandbox.openprocurement.org/api/0.12/tenders/668c3156c8cb496fb28359909cde6e96/cancellations/0dd6f9e8cc4f4d1c9c404d842b56d0d7/documents/1afca9faaf2b4f9489ee264b136371c6?download=9208a5fa7cf140a2bb4751a42a1f1ea9", + "title": "/tmp/tmpOBUlWP", + "documentOf": "tender", + "datePublished": "2016-02-18T14:46:05.348204+02:00", + "dateModified": "2016-02-18T14:46:08.516130+02:00", + "id": "1afca9faaf2b4f9489ee264b136371c6" + } + ], + "reason": "prost :))", + "date": "2016-02-18T14:46:04.344125+02:00", + "cancellationOf": "tender", + "id": "0dd6f9e8cc4f4d1c9c404d842b56d0d7" + } + ], + "value": { + "currency": "UAH", + "amount": 500000.0, + "valueAddedTaxIncluded": true + }, + "id": "668c3156c8cb496fb28359909cde6e96", + "awards": [ + { + "status": "active", + "complaintPeriod": { + "startDate": "2016-02-18T14:45:41.212240+02:00", + "endDate": "2016-02-19T14:45:42.178932+02:00" + }, + "suppliers": [ + { + "contactPoint": { + "telephone": "+380 (432) 21-69-30", + "name": "\u0421\u0435\u0440\u0433\u0456\u0439 \u041e\u043b\u0435\u043a\u0441\u044e\u043a", + "email": "soleksuk@gmail.com" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "13313462", + "uri": "http://sch10.edu.vn.ua/", + "legalName": "\u0414\u0435\u0440\u0436\u0430\u0432\u043d\u0435 \u043a\u043e\u043c\u0443\u043d\u0430\u043b\u044c\u043d\u0435 \u043f\u0456\u0434\u043f\u0440\u0438\u0454\u043c\u0441\u0442\u0432\u043e \u0433\u0440\u043e\u043c\u0430\u0434\u0441\u044c\u043a\u043e\u0433\u043e \u0445\u0430\u0440\u0447\u0443\u0432\u0430\u043d\u043d\u044f \u00ab\u0428\u043a\u043e\u043b\u044f\u0440\u00bb" + }, + "name": "\u0414\u041a\u041f \u00ab\u0428\u043a\u043e\u043b\u044f\u0440\u00bb", + "address": { + "postalCode": "21100", + "countryName": "\u0423\u043a\u0440\u0430\u0457\u043d\u0430", + "streetAddress": "\u0432\u0443\u043b. \u041e\u0441\u0442\u0440\u043e\u0432\u0441\u044c\u043a\u043e\u0433\u043e, 33", + "region": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f", + "locality": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f" + } + } + ], + "value": { + "currency": "UAH", + "amount": 475000.0, + "valueAddedTaxIncluded": true + }, + "date": "2016-02-18T14:45:41.210928+02:00", + "id": "70eaaceee81e4f378e333c1ed8258652" + } + ], + "tenderID": "UA-2016-02-18-000031", + "owner": "test.quintagroup.com", + "dateModified": "2016-02-18T14:46:09.940361+02:00", + "procuringEntity": { + "contactPoint": { + "url": "http://sch10.edu.vn.ua/", + "name": "\u041a\u0443\u0446\u0430 \u0421\u0432\u0456\u0442\u043b\u0430\u043d\u0430 \u0412\u0430\u043b\u0435\u043d\u0442\u0438\u043d\u0456\u0432\u043d\u0430", + "telephone": "+380 (432) 46-53-02" + }, + "identifier": { + "scheme": "UA-EDR", + "id": "21725150", + "legalName": "\u0417\u0430\u043a\u043b\u0430\u0434 \"\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u043e\u0441\u0432\u0456\u0442\u043d\u044f \u0448\u043a\u043e\u043b\u0430 \u0406-\u0406\u0406\u0406 \u0441\u0442\u0443\u043f\u0435\u043d\u0456\u0432 \u2116 10 \u0412\u0456\u043d\u043d\u0438\u0446\u044c\u043a\u043e\u0457 \u043c\u0456\u0441\u044c\u043a\u043e\u0457 \u0440\u0430\u0434\u0438\"" + }, + "name": "\u0417\u041e\u0421\u0428 #10 \u043c.\u0412\u0456\u043d\u043d\u0438\u0446\u0456", + "address": { + "postalCode": "21027", + "countryName": "\u0423\u043a\u0440\u0430\u0457\u043d\u0430", + "streetAddress": "\u0432\u0443\u043b. \u0421\u0442\u0430\u0445\u0443\u0440\u0441\u044c\u043a\u043e\u0433\u043e. 22", + "region": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f", + "locality": "\u043c. \u0412\u0456\u043d\u043d\u0438\u0446\u044f" + } + } + } +} diff --git a/openprocurement_client/tests/data/lot_823d50b3236247adad28a5a66f74db42.json b/openprocurement_client/tests/data/lot_823d50b3236247adad28a5a66f74db42.json new file mode 100644 index 0000000..27b7f37 --- /dev/null +++ b/openprocurement_client/tests/data/lot_823d50b3236247adad28a5a66f74db42.json @@ -0,0 +1,504 @@ +{ + "data":{ + "procurementMethod":"open", + "status":"complete", + "tenderPeriod":{ + "startDate":"2015-12-30T03:05:00+02:00", + "endDate":"2015-12-31T03:05:00+02:00" + }, + "qualifications": [ + { + "status": "pending", + "id": "cec4b82d2708465291fb4af79f8a3e52", + "bidID": "c41709780fa8434399c62a1facf369cb", + "documents": [ + { + "confidentiality": "public", + "language": "uk", + "title": "Proposal.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/documents/a05aff4e91c942bbad7e49e551399ec7?download=d0984048f149465699fd402bfe20881c", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.127526+02:00", + "id": "a05aff4e91c942bbad7e49e551399ec7", + "dateModified": "2016-02-24T14:13:36.127580+02:00" + }] + }, + { + "status": "pending", + "id": "9d562bbbf05e44f8a6a1f07ddb0415b1", + "bidID": "dda102997a4d4d5d8b5dac6ce2225e1c" + }, + { + "status": "pending", + "id": "36ac59607c434c8198730cc372672c8c", + "bidID": "6e0d218c8e964b42ac4a8ca086c100f9" + } + ], + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders/823d50b3236247adad28a5a66f74db42/documents/330822cbbd724671a1d2ff7c3a51dd52?download=5ff4917f29954e5f8dfea1feed8f7455", + "title":"test1.txt", + "documentOf":"tender", + "datePublished":"2015-12-12T03:06:53.209628+02:00", + "dateModified":"2015-12-12T03:06:53.209670+02:00", + "id":"330822cbbd724671a1d2ff7c3a51dd52" + }, + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders/823d50b3236247adad28a5a66f74db42/documents/6badc6f313d2499794bd39a3a7182fc0?download=ab2d7cdd346342a6a0d7b40af39145e2", + "title":"test3.txt", + "documentOf":"tender", + "datePublished":"2015-12-12T03:06:53.724784+02:00", + "dateModified":"2015-12-12T03:06:53.724826+02:00", + "id":"6badc6f313d2499794bd39a3a7182fc0" + }, + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders/823d50b3236247adad28a5a66f74db42/documents/08bcd56de9aa43faa684f8f8e7ab1c98?download=c7afbf3686cc441db7735d5ec284a0de", + "title":"test2.txt", + "documentOf":"tender", + "datePublished":"2015-12-12T03:06:54.280311+02:00", + "dateModified":"2015-12-12T03:06:54.280354+02:00", + "id":"08bcd56de9aa43faa684f8f8e7ab1c98" + } + ], + "description":"Multilot 4 descr", + "title":"Multilot 4 title", + "submissionMethodDetails":"quick", + "items":[ + { + "relatedLot":"7d774fbf1e86420484c7d1a005cc283f", + "description":"itelm descr", + "classification":{ + "scheme":"CPV", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u0444\u0435\u0440\u043c\u0435\u0440\u0441\u0442\u0432\u0430, \u0440\u0438\u0431\u0430\u043b\u044c\u0441\u0442\u0432\u0430, \u043b\u0456\u0441\u043d\u0438\u0446\u0442\u0432\u0430 \u0442\u0430 \u0441\u0443\u043c\u0456\u0436\u043d\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f", + "id":"03000000-1" + }, + "additionalClassifications":[ + { + "scheme":"\u0414\u041a\u041f\u041f", + "id":"01", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u043c\u0438\u0441\u043b\u0438\u0432\u0441\u0442\u0432\u0430 \u0442\u0430 \u043f\u043e\u0432\u2019\u044f\u0437\u0430\u043d\u0456 \u0437 \u0446\u0438\u043c \u043f\u043e\u0441\u043b\u0443\u0433\u0438" + } + ], + "deliveryDate":{ + "startDate":"2016-06-01T00:00:00+03:00" + }, + "id":"7caa3587fe9041da98f2744ef531d7ce", + "unit":{ + "code":"LO", + "name":"\u043b\u043e\u0442" + }, + "quantity":4 + }, + { + "relatedLot":"7d774fbf1e86420484c7d1a005cc283f", + "description":"itelm descr", + "classification":{ + "scheme":"CPV", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u0444\u0435\u0440\u043c\u0435\u0440\u0441\u0442\u0432\u0430, \u0440\u0438\u0431\u0430\u043b\u044c\u0441\u0442\u0432\u0430, \u043b\u0456\u0441\u043d\u0438\u0446\u0442\u0432\u0430 \u0442\u0430 \u0441\u0443\u043c\u0456\u0436\u043d\u0430 \u043f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f", + "id":"03000000-1" + }, + "additionalClassifications":[ + { + "scheme":"\u0414\u041a\u041f\u041f", + "id":"01", + "description":"\u041f\u0440\u043e\u0434\u0443\u043a\u0446\u0456\u044f \u0441\u0456\u043b\u044c\u0441\u044c\u043a\u043e\u0433\u043e \u0433\u043e\u0441\u043f\u043e\u0434\u0430\u0440\u0441\u0442\u0432\u0430, \u043c\u0438\u0441\u043b\u0438\u0432\u0441\u0442\u0432\u0430 \u0442\u0430 \u043f\u043e\u0432\u2019\u044f\u0437\u0430\u043d\u0456 \u0437 \u0446\u0438\u043c \u043f\u043e\u0441\u043b\u0443\u0433\u0438" + } + ], + "deliveryDate":{ + "startDate":"2016-06-01T00:00:00+03:00" + }, + "id":"563ef5d999f34d36a5a0e4e4d91d7be1", + "unit":{ + "code":"LO", + "name":"\u043b\u043e\u0442" + }, + "quantity":4 + } + ], + "id":"823d50b3236247adad28a5a66f74db42", + "value":{ + "currency":"UAH", + "amount":1000.0, + "valueAddedTaxIncluded":false + }, + "submissionMethod":"electronicAuction", + "procuringEntity":{ + "contactPoint":{ + "telephone":"", + "name":"Dmitry", + "email":"d.karnaukh+merchant1@smartweb.com.ua" + }, + "identifier":{ + "scheme":"UA-EDR", + "id":"66666692", + "legalName":"\u041d\u0410\u041a \"Testov Test\"" + }, + "name":"\u041d\u0410\u041a \"Testov Test\"", + "address":{ + "postalCode":"02121", + "countryName":"\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "streetAddress":"", + "region":"\u041a\u0438\u0435\u0432\u0441\u043a\u0430\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u044c", + "locality":"\u041a\u0438\u0435\u0432" + } + }, + "minimalStep":{ + "currency":"UAH", + "amount":1.0, + "valueAddedTaxIncluded":false + }, + "tenderID":"UA-2015-12-12-000003-1", + "questions":[ + { + "description":"Test lot question descr", + "title":"Test lot question title", + "relatedItem":"563ef5d999f34d36a5a0e4e4d91d7be1", + "answer":"Test answer", + "date":"2015-12-21T17:31:19.650440+02:00", + "id":"615ff8be8eba4a81b300036d6bec991c", + "questionOf":"lot" + }, + { + "description":"lot 2 question descr?", + "title":"lot 2 question title", + "relatedItem":"563ef5d999f34d36a5a0e4e4d91d7be1", + "answer":"lot 2 answer", + "date":"2015-12-21T17:36:30.680576+02:00", + "id":"278f269ee482434da6615a21f3deccf0", + "questionOf":"lot" + }, + { + "description":"lot 3 q descr?", + "title":"lot 3 q title", + "relatedItem":"563ef5d999f34d36a5a0e4e4d91d7be1", + "answer":"lot 3 answer", + "date":"2015-12-21T17:57:28.108596+02:00", + "id":"da6c1228f81e4c2e8451479d57b685dd", + "questionOf":"lot" + }, + { + "description":"Tender q 1 descr?", + "title":"Tender q 1 title", + "relatedItem":"ad681d050d0e42169a80b86d5b591c20", + "answer":"Tender q 1 answer", + "date":"2015-12-21T17:58:24.489846+02:00", + "id":"937b4b9ef3444782b9a3019fcc5c072a", + "questionOf":"tender" + }, + { + "description":"Item descr?", + "title":"Item title?", + "relatedItem":"7caa3587fe9041da98f2744ef531d7ce", + "answer":"Item answer", + "date":"2015-12-21T18:04:35.349936+02:00", + "id":"a2d14f4faf9b4ba180b40f08a2be3bd9", + "questionOf":"item" + } + ], + "enquiryPeriod":{ + "startDate":"2015-12-12T03:06:52.664385+02:00", + "endDate":"2015-12-30T03:05:00+02:00" + }, + "owner":"prom.ua", + "lots":[ + { + "status":"complete", + "description":"lot descr1", + "title":"lot title", + "minimalStep":{ + "currency":"UAH", + "amount":1.0, + "valueAddedTaxIncluded":false + }, + "value":{ + "currency":"UAH", + "amount":1000.0, + "valueAddedTaxIncluded":false + }, + "id":"7d774fbf1e86420484c7d1a005cc283f" + }, + { + "status":"active", + "description":"lot descr2", + "title":"lot title", + "minimalStep":{ + "currency":"UAH", + "amount":1.0, + "valueAddedTaxIncluded":false + }, + "value":{ + "currency":"UAH", + "amount":2000.0, + "valueAddedTaxIncluded":false + }, + "id":"563ef5d999f34d36a5a0e4e4d91d7be1" + } + ], + "bids":[ + { + "date": "2014-12-16T04:44:23.569815+02:00", + "documents": [ + { + "dateModified": "2014-12-16T04:44:25.010930+02:00", + "datePublished": "2014-12-16T04:44:25.010885+02:00", + "format": "text/plain", + "id": "ff001412c60c4164a0f57101e4eaf8aa", + "title": "Proposal.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0/tenders/6f73bf0f7f734f459f7e37e3787054a0/bids/f7fc1212f9f140bba5c4e3cd4f2b62d9/documents/ff001412c60c4164a0f57101e4eaf8aa?download=4f45bbd414104cd78faf620208efd824" + } + ], + "qualificationDocuments": [ + { + "confidentiality": "public", + "language": "uk", + "title": "qualification_document.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/financial_documents/7519d21b32af432396acd6e2c9e18ee5?download=5db59ae05361427b84c4caddb0b6d92b", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.625252+02:00", + "id": "7519d21b32af432396acd6e2c9e18ee5", + "dateModified": "2016-02-24T14:13:36.625290+02:00" + } + ], + "financial_documens": [ + { + "confidentiality": "public", + "language": "uk", + "title": "financial_doc.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/financial_documents/7519d21b32af432396acd6e2c9e18ee5?download=5db59ae05361427b84c4caddb0b6d92b", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.625252+02:00", + "id": "7519d21b32af432396acd6e2c9e18ee5", + "dateModified": "2016-02-24T14:13:36.625290+02:00" + } + ], + "eligibility_documents": [ + { + "confidentiality": "public", + "language": "uk", + "title": "eligibility_doc.pdf", + "url": "http://api-sandbox.openprocurement.org/api/0.12/tenders/e3e16eb63b584378b75d9eb01dc2f8b9/bids/c41709780fa8434399c62a1facf369cb/financial_documents/7519d21b32af432396acd6e2c9e18ee5?download=5db59ae05361427b84c4caddb0b6d92b", + "format": "application/pdf", + "documentOf": "tender", + "datePublished": "2016-02-24T14:13:36.625252+02:00", + "id": "7519d21b32af432396acd6e2c9e18ee5", + "dateModified": "2016-02-24T14:13:36.625290+02:00" + } + ], + "id": "f7fc1212f9f140bba5c4e3cd4f2b62d9", + "lotValues": [ + { + "value": {"currency": "UAH", "amount": 7750.0, "valueAddedTaxIncluded": true}, + "reatedLot": "7d774fbf1e86420484c7d1a005cc283f", + "date": "2015-11-01T12:43:12.482645+02:00" + }, { + "value": {"currency": "UAH", "amount": 8125.0, "valueAddedTaxIncluded": true}, + "reatedLot": "563ef5d999f34d36a5a0e4e4d91d7be1", + "date": "2015-11-01T12:43:12.482645+02:00" + } + ], + "tenderers": [ + { + "address": { + "countryName": "Україна", + "locality": "м. Вінниця", + "postalCode": "21100", + "region": "м. Вінниця", + "streetAddress": "вул. Островського, 33" + }, + "contactPoint": { + "email": "soleksuk@gmail.com", + "name": "Сергій Олексюк", + "telephone": "+380 (432) 21-69-30" + }, + "identifier": { + "id": "13313462", + "legalName": "Державне комунальне підприємство громадського харчування «Школяр»", + "scheme": "UA-EDR", + "uri": "http://sch10.edu.vn.ua/" + }, + "name": "ДКП «Школяр»" + } + ] + }, + { + "date": "2014-12-16T04:44:27.976478+02:00", + "id": "7ec725815ef448a9b857129024395638", + "lotValues": [ + { + "value": {"currency": "UAH", "amount": 7750.0, "valueAddedTaxIncluded": true}, + "reatedLot": "7d774fbf1e86420484c7d1a005cc283f", + "date": "2015-11-01T12:43:12.482645+02:00" + }, { + "value": {"currency": "UAH", "amount": 8125.0, "valueAddedTaxIncluded": true}, + "reatedLot": "563ef5d999f34d36a5a0e4e4d91d7be1", + "date": "2015-11-01T12:43:12.482645+02:00" + } + ], + "tenderers": [ + { + "address": { + "countryName": "Україна", + "locality": "м. Вінниця", + "postalCode": "21018", + "region": "м. Вінниця", + "streetAddress": "вул. Юності, 30" + }, + "contactPoint": { + "email": "alla.myhailova@i.ua", + "name": "Алла Михайлова", + "telephone": "+380 (432) 460-665" + }, + "identifier": { + "id": "13306232", + "legalName": "Державне комунальне підприємство громадського харчування «Меридіан»", + "scheme": "UA-EDR", + "uri": "http://sch10.edu.vn.ua/" + }, + "name": "ДКП «Меридіан2»" + } + ] + } + + ], + "awards":[ + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/documents/c7ac894e4ca34e618e03a02d8799dc69?download=8c4053b0942949f4bd6e998a51e177c5", + "title":"2.jpg", + "datePublished":"2015-11-13T16:31:03.691781+02:00", + "dateModified":"2015-11-13T16:31:03.691829+02:00", + "id":"c7ac894e4ca34e618e03a02d8799dc69" + } + ], + "complaintPeriod":{ + "startDate":"2015-11-13T16:17:00.375880+02:00", + "endDate":"2015-11-14T16:31:06.979761+02:00" + }, + "contracts":[ + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/contracts/045c478807c04215950b4d74e6861cb1/documents/14dbf158b9ed4a478a59d19c90dbdf5d?download=9d948173e65441409f06e0b81674b39a", + "title":"3.jpg", + "datePublished":"2015-11-13T19:01:16.748785+02:00", + "dateModified":"2015-11-13T19:01:16.748829+02:00", + "id":"14dbf158b9ed4a478a59d19c90dbdf5d" + } + ], + "awardID":"7054491a5e514699a56e44d32e23edf7", + "id":"045c478807c04215950b4d74e6861cb1" + } + ], + "suppliers":[ + { + "contactPoint":{ + "telephone":"", + "name":"some6", + "email":"some6@mailinator.com" + }, + "identifier":{ + "scheme":"UA-EDR", + "id":"62123463", + "legalName":"some6" + }, + "name":"some6", + "address":{ + "postalCode":"04119", + "countryName":"\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "streetAddress":"", + "region":"\u0410\u0420 \u041a\u0440\u044b\u043c", + "locality":"ffff" + } + } + ], + "bid_id":"ad9ac917ff0049178acc3613bb14a691", + "value":{ + "currency":"UAH", + "amount":300.0, + "valueAddedTaxIncluded":false + }, + "date":"2015-11-13T16:17:00.376251+02:00", + "id":"7054491a5e514699a56e44d32e23edf7", + "lotId": "563ef5d999f34d36a5a0e4e4d91d7be1" + }, + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/documents/c7ac894e4ca34e618e03a02d8799dc69?download=8c4053b0942949f4bd6e998a51e177c5", + "title":"2.jpg", + "datePublished":"2015-11-13T16:31:03.691781+02:00", + "dateModified":"2015-11-13T16:31:03.691829+02:00", + "id":"c7ac894e4ca34e618e03a02d8799dc69" + } + ], + "complaintPeriod":{ + "startDate":"2015-11-13T16:17:00.375880+02:00", + "endDate":"2015-11-14T16:31:06.979761+02:00" + }, + "contracts":[ + { + "status":"active", + "documents":[ + { + "format":"text/plain", + "url":"https://lb.api-sandbox.openprocurement.org/api/0.9/tenders/3216137f17fb452db202daad1f9e73ad/awards/7054491a5e514699a56e44d32e23edf7/contracts/045c478807c04215950b4d74e6861cb1/documents/14dbf158b9ed4a478a59d19c90dbdf5d?download=9d948173e65441409f06e0b81674b39a", + "title":"3.jpg", + "datePublished":"2015-11-13T19:01:16.748785+02:00", + "dateModified":"2015-11-13T19:01:16.748829+02:00", + "id":"14dbf158b9ed4a478a59d19c90dbdf5d" + } + ], + "awardID":"7054491a5e514699a56e44d32e23edf7", + "id":"045c478807c04215950b4d74e6861cb1" + } + ], + "suppliers":[ + { + "contactPoint":{ + "telephone":"", + "name":"some6", + "email":"some6@mailinator.com" + }, + "identifier":{ + "scheme":"UA-EDR", + "id":"62123463", + "legalName":"some6" + }, + "name":"some6", + "address":{ + "postalCode":"04119", + "countryName":"\u0423\u043a\u0440\u0430\u0438\u043d\u0430", + "streetAddress":"", + "region":"\u0410\u0420 \u041a\u0440\u044b\u043c", + "locality":"ffff" + } + } + ], + "bid_id":"ad9ac917ff0049178acc3613bb14a691", + "value":{ + "currency":"UAH", + "amount":300.0, + "valueAddedTaxIncluded":false + }, + "date":"2015-11-13T16:17:00.376251+02:00", + "id":"7054491a5e514699a56e44d32e23edf7", + "lotId": "7d774fbf1e86420484c7d1a005cc283f" + } + ], + "dateModified":"2015-12-12T03:06:54.370277+02:00", + "awardCriteria":"lowestCost" + } +} diff --git a/openprocurement_client/tests/data/lots.json b/openprocurement_client/tests/data/lots.json new file mode 100644 index 0000000..384ef8a --- /dev/null +++ b/openprocurement_client/tests/data/lots.json @@ -0,0 +1,153 @@ +{ + "next_page":{ + "path":"/api/0.10/tenders?offset=2015-12-25T18%3A04%3A36.264176%2B02%3A00", + "uri":"https://lb.api-sandbox.openprocurement.org/api/0.10/tenders?offset=2015-12-25T18%3A04%3A36.264176%2B02%3A00", + "offset":"2015-12-25T18:04:36.264176+02:00" + }, + "data":[ + { + "id":"823d50b3236247adad28a5a66f74db42", + "dateModified":"2015-11-13T18:50:00.753811+02:00" + }, + { + "id":"f3849ade33534174b8402579152a5f41", + "dateModified":"2015-11-16T01:15:00.469896+02:00" + }, + { + "id":"f3849ade33534174b8402579152a5f41", + "dateModified":"2015-11-16T12:00:00.960077+02:00" + }, + { + "id":"3be3664065994d1e8847beb597c014bf", + "dateModified":"2015-11-16T21:15:00.404214+02:00" + }, + { + "id":"711cfa82c54147b686f6755adb22364d", + "dateModified":"2015-11-17T18:02:00.456050+02:00" + }, + { + "id":"730fe73cb7be4f92bce472484236e9ee", + "dateModified":"2015-11-18T18:30:00.410770+02:00" + }, + { + "id":"496ec128323f45679daf9ebcb1ec62f6", + "dateModified":"2015-11-20T16:20:37.977312+02:00" + }, + { + "id":"18d566bed8cc4f2e846ca5d64c6bdfed", + "dateModified":"2015-12-01T11:48:33.784585+02:00" + }, + { + "id":"21733c5ed0be4b259c3345310f425a82", + "dateModified":"2015-12-02T13:54:13.656648+02:00" + }, + { + "id":"b35a1151b7a84562addb47f66193a309", + "dateModified":"2015-12-12T02:45:36.355426+02:00" + }, + { + "id":"ce9751da26a84c2390368ffa5adfd74c", + "dateModified":"2015-12-12T03:04:16.059667+02:00" + }, + { + "id":"823d50b3236247adad28a5a66f74db42", + "dateModified":"2015-12-12T03:06:54.370277+02:00" + }, + { + "id":"0085cf76ad444ad0ae625fc17f366dd2", + "dateModified":"2015-12-12T14:30:38.596246+02:00" + }, + { + "id":"d97613278dd547f2be5523cdc5614880", + "dateModified":"2015-12-12T14:31:51.342309+02:00" + }, + { + "id":"71161344890c4231b1487296f56aa910", + "dateModified":"2015-12-14T13:10:38.771625+02:00" + }, + { + "id":"759a81e58cf04b5ea0587942d0925faa", + "dateModified":"2015-12-16T19:56:53.874070+02:00" + }, + { + "id":"ad681d050d0e42169a80b86d5b591c20", + "dateModified":"2015-12-22T13:14:25.253453+02:00" + }, + { + "id":"a2e4464a08d64addb6c7a63da48ddb87", + "dateModified":"2015-12-23T17:09:15.145867+02:00" + }, + { + "id":"eea0c2f3cf3f4206b8cbba813b9a527b", + "dateModified":"2015-12-23T17:15:23.479628+02:00" + }, + { + "id":"847411c7b03f4f91b97fecb70c6e6275", + "dateModified":"2015-12-23T17:54:39.470396+02:00" + }, + { + "id":"599e63e6fb1349078fb01e4f9e8a812c", + "dateModified":"2015-12-23T17:55:36.317529+02:00" + }, + { + "id":"d63a7302316c4d15b46aeb7bcd4f1dbe", + "dateModified":"2015-12-23T17:56:01.709307+02:00" + }, + { + "id":"6496299a3c8e482f9017d855483a97b5", + "dateModified":"2015-12-23T17:57:57.376623+02:00" + }, + { + "id":"a513848583cc4d1d806c78e5131ad068", + "dateModified":"2015-12-23T17:59:52.101031+02:00" + }, + { + "id":"79aa9fd2bb43471cbcb0b97f6965e5f6", + "dateModified":"2015-12-24T11:39:09.071184+02:00" + }, + { + "id":"b2cdcda417434c7b889c82a81927bb73", + "dateModified":"2015-12-24T11:40:20.172257+02:00" + }, + { + "id":"58eca935e0e24805b561a1588ae64ef3", + "dateModified":"2015-12-24T11:48:57.584694+02:00" + }, + { + "id":"78b889ec869544b9b47c8740549ed20a", + "dateModified":"2015-12-24T11:51:07.149689+02:00" + }, + { + "id":"258ef598759946dbb30e7ed67bfc2346", + "dateModified":"2015-12-24T11:52:16.196052+02:00" + }, + { + "id":"639ecaca9db24228a9fe0eb3494aa0e3", + "dateModified":"2015-12-25T13:28:22.136939+02:00" + }, + { + "id":"ca10599dfcbe416bb513416c13073c5f", + "dateModified":"2015-12-25T15:19:53.913160+02:00" + }, + { + "id":"21e551bf7f194d8aaba29bf2e4949b90", + "dateModified":"2015-12-25T16:00:00.319589+02:00" + }, + { + "id":"dd7b13a956b44a34bbaeb2bfa1910c03", + "dateModified":"2015-12-25T16:07:19.809837+02:00" + }, + { + "id":"f8f21e8b0c104747b8abf5ece4fb5d72", + "dateModified":"2015-12-25T17:06:32.885207+02:00" + }, + { + "id":"48dcb014d003454b8875975bb531147f", + "dateModified":"2015-12-25T17:16:05.208447+02:00" + }, + { + "id":"7f35a555d5c64b34a7cbc454fab5628c", + "dateModified":"2015-12-25T18:04:36.264176+02:00" + } + ] +} diff --git a/openprocurement_client/tests/data_dict.py b/openprocurement_client/tests/data_dict.py index 2e2938f..f9efa4e 100644 --- a/openprocurement_client/tests/data_dict.py +++ b/openprocurement_client/tests/data_dict.py @@ -7,7 +7,7 @@ "question_id": '615ff8be8eba4a81b300036d6bec991c', "lot_id": '563ef5d999f34d36a5a0e4e4d91d7be1', "bid_id": 'f7fc1212f9f140bba5c4e3cd4f2b62d9', - "bid_document_id":"ff001412c60c4164a0f57101e4eaf8aa", + "bid_document_id": "ff001412c60c4164a0f57101e4eaf8aa", "bid_qualification_document_id": "7519d21b32af432396acd6e2c9e18ee5", "bid_financial_document_id": "7519d21b32af432396acd6e2c9e18ee5", "bid_eligibility_document_id": "7519d21b32af432396acd6e2c9e18ee5", @@ -45,3 +45,19 @@ "patch_change_rationale": u'Друга і третя поставка має бути розфасована', "new_token": 'new0contract0token123412341234' }) + +TEST_LOT_KEYS = munchify({ + "asset_id": '823d50b3236247adad28a5a66f74db42', + "lot_id": '823d50b3236247adad28a5a66f74db42', + "token": 'f6247c315d2744f1aa18c7c3de523bc3', + "new_token": '4fa3e89efbaa46f0a1b86b911bec76e7' + +}) + +TEST_ASSET_KEYS = munchify({ + "asset_id": '823d50b3236247adad28a5a66f74db42', + "lot_id": '823d50b3236247adad28a5a66f74db42', + "token": 'f6247c315d2744f1aa18c7c3de523bc3', + "new_token": '4fa3e89efbaa46f0a1b86b911bec76e7' + +}) diff --git a/openprocurement_client/tests/main.py b/openprocurement_client/tests/main.py index 17c9333..a6e892f 100644 --- a/openprocurement_client/tests/main.py +++ b/openprocurement_client/tests/main.py @@ -1,12 +1,17 @@ import unittest -from openprocurement_client.tests import tests, tests_sync +from openprocurement_client.tests import ( + tests_resources, + tests_sync, + test_registry_client +) 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 new file mode 100644 index 0000000..5579718 --- /dev/null +++ b/openprocurement_client/tests/test_registry_client.py @@ -0,0 +1,157 @@ +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 collections import Iterable +from simplejson import load +from munch import munchify +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, +) +from openprocurement_client.tests._server import \ + 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): + self.app = Bottle() + self.app.router.add_filter('resource_filter', resource_filter) + setup_routing(self.app) + self.server = WSGIServer(('localhost', PORT), self.app, log=None) + try: + self.server.start() + except Exception as error: + print(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], + file=sys.stderr) + raise error + + ds_config = { + 'host_url': DS_HOST_URL, + 'auth_ds': AUTH_DS_FAKE + } + self.client = client('', host_url=HOST_URL, api_version=API_VERSION, + ds_config=ds_config) + + @classmethod + def setting_up_ds(cls): + cls.app_ds = Bottle() + cls.server_ds \ + = WSGIServer(('localhost', DS_PORT), cls.app_ds, log=None) + try: + cls.server_ds.start() + except Exception as error: + print(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2], + file=sys.stderr) + raise error + + setup_routing_ds(cls.app_ds) + + @classmethod + def setUpClass(cls): + cls.setting_up_ds() + + @classmethod + def tearDownClass(cls): + cls.server_ds.stop() + + +class AssetsRegistryTestCase(BaseTestClass): + def setUp(self): + self.setting_up(client=AssetsClient) + + with open(ROOT + 'assets.json') as assets: + self.assets = munchify(load(assets)) + with open(ROOT + 'asset_{}.json'.format( + TEST_ASSET_KEYS.asset_id)) as asset: + self.asset = munchify(load(asset)) + + def tearDown(self): + self.server.stop() + + def test_get_assets(self): + setup_routing(self.app, routes=["assets"]) + assets = self.client.get_assets() + self.assertIsInstance(assets, Iterable) + self.assertEqual(assets, self.assets.data) + + @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): + self.client.get_assets(params={'offset': 'offset_value'}) + + def test_get_asset(self): + setup_routing(self.app, routes=["asset"]) + asset = self.client.get_asset(TEST_ASSET_KEYS.asset_id) + self.assertEqual(asset, self.asset) + + def test_patch_asset(self): + setup_routing(self.app, routes=["asset_patch"]) + 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, + patch_data['data']['description']) + + +class LotsRegistryTestCase(BaseTestClass): + def setUp(self): + self.setting_up(client=LotsClient) + + with open(ROOT + 'lots.json') as lots: + self.lots = munchify(load(lots)) + with open(ROOT + 'lot_{}.json'.format(TEST_LOT_KEYS.lot_id)) as lot: + self.lot = munchify(load(lot)) + + def tearDown(self): + self.server.stop() + + def test_get_lots(self): + setup_routing(self.app, routes=["lots"]) + lots = self.client.get_lots() + self.assertIsInstance(lots, Iterable) + self.assertEqual(lots, self.lots.data) + + @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): + self.client.get_lots(params={'offset': 'offset_value'}) + + def test_get_lot(self): + setup_routing(self.app, routes=["lot"]) + lot = self.client.get_lot(TEST_LOT_KEYS.lot_id) + self.assertEqual(lot, self.lot) + + def test_patch_lot(self): + setup_routing(self.app, routes=["lot_patch"]) + 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, + patch_data['data']['description']) + + +def suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(AssetsRegistryTestCase)) + suite.addTest(unittest.makeSuite(LotsRegistryTestCase)) + return suite + + +if __name__ == '__main__': + unittest.main(defaultTest='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..e37f791 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, @@ -165,7 +178,8 @@ def test_instance_initialization(self): }) self.assertIsInstance(self.resource_feeder.queue, Queue) - def test_init_api_clients(self): + @mock.patch('openprocurement_client.templates.Session') + def test_init_api_clients(self, mock_session): self.resource_feeder = ResourceFeeder() self.resource_feeder.init_api_clients() self.assertEqual(self.resource_feeder.backward_params, { @@ -189,39 +203,51 @@ 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.templates.Session') + @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_session): 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.templates.Session') + @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_session): 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.templates.Session') + @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_session): + 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 +257,16 @@ 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.templates.Session') + @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_session): + 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 +276,16 @@ 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.templates.Session') + @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_session): 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 +295,15 @@ 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.templates.Session') + @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_session): 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 +313,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,19 +321,23 @@ 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') - def test_retriever_backward(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_backward(self, mock_get_response, mock_session): 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') - def test_retriever_backward_wrong_cookies(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_backward_wrong_cookies(self, mock_get_response, mock_session): mock_get_response.return_value = self.response self.resource_feeder = ResourceFeeder() self.resource_feeder.init_api_clients() @@ -307,8 +349,9 @@ 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') - def test_retriever_forward_wrong_cookies(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_forward_wrong_cookies(self, mock_get_response, mock_session): mock_get_response.return_value = self.response self.resource_feeder = ResourceFeeder() self.resource_feeder.init_api_clients() @@ -320,8 +363,9 @@ 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') - def test_retriever_forward(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_forward(self, mock_get_response, mock_session): mock_get_response.side_effect = [ self.response, self.response, @@ -331,14 +375,17 @@ 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') - def test_retriever_forward_no_data(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_forward_no_data(self, mock_get_response, mock_session): mock_get_response.side_effect = [ munchify({'data': {}, 'next_page': {'offset': 'next_page'}}), ConnectionError('connection error') @@ -347,14 +394,17 @@ 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') - def test_retriever_forward_adaptive(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_forward_adaptive(self, mock_get_response, mock_session): mock_get_response.side_effect = [ self.response, munchify({'data': {}, 'next_page': {'offset': 'next_page'}}), @@ -365,14 +415,17 @@ 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') - def test_retriever_forward_no_data_adaptive(self, mock_get_response): + @mock.patch('openprocurement_client.templates.Session') + @mock.patch('openprocurement_client.resources.sync.get_response') + def test_retriever_forward_no_data_adaptive(self, mock_get_response, mock_session): mock_get_response.side_effect = [ munchify({'data': {}, 'next_page': {'offset': 'next_page'}}), munchify({'data': {}, 'next_page': {'offset': 'next_page'}}), @@ -391,25 +444,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..5e50dc8 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,8 @@ from setuptools import find_packages, setup -import os -version = '2.0b7' +version = '2.1.4+sale' -tests_require = { +extras_require = { 'test': [ 'bottle', 'mock', @@ -41,13 +40,15 @@ 'retrying', 'simplejson', 'requests', - 'python-magic' + 'python-magic', + 'bottle', + 'mock', + 'nose' # -*- Extra requirements: -*- ], - tests_require=tests_require, - extras_require = tests_require, + extras_require=extras_require, entry_points=""" # -*- Entry points: -*- """, - test_suite="openprocurement_client.tests" + test_suite="openprocurement_client.tests.main:suite" )