Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
52 changes: 52 additions & 0 deletions openprocurement/auction/bridge_utils/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import os
from datetime import time
from pytz import timezone

from datetime import timedelta, datetime


def read_json(name):
import inspect
import os.path
from json import loads
caller_file = inspect.stack()[1][1]
caller_dir = os.path.dirname(os.path.realpath(caller_file))
file_path = os.path.join(caller_dir, name)
with open(file_path) as lang_file:
data = lang_file.read()
return loads(data)


STREAMS_ID = 'streams'
WORKING_DAY_START = time(11, 0)
INSIDER_WORKING_DAY_START = time(9, 30)
TEXAS_WORKING_DAY_START = time(10, 0)
WORKING_DAY_END = time(16, 0)
INSIDER_WORKING_DAY_DURATION = timedelta(minutes=480)
TEXAS_WORKING_DAY_DURATION = timedelta(hours=7) # Does not affect anything
DEFAULT_STREAMS_DOC = {
'_id': STREAMS_ID,
'streams': 10,
'dutch_streams': 15,
'texas_streams': 20
}


CALENDAR_ID = 'calendar'
STREAMS_ID = 'streams'
ROUNDING = timedelta(minutes=29)
MIN_PAUSE = timedelta(minutes=3)
BIDDER_TIME = timedelta(minutes=6)
SERVICE_TIME = timedelta(minutes=9)
STAND_STILL_TIME = timedelta(days=1)
SMOOTHING_MIN = 10
SMOOTHING_REMIN = 60
# value should be greater than SMOOTHING_MIN and SMOOTHING_REMIN
SMOOTHING_MAX = 300
NOT_CLASSIC_AUCTIONS = ['dgfInsider', 'sellout.insider']
STREAMS_KEYS = ['streams', 'dutch_streams', 'texas_streams']


TZ = timezone(os.environ['TZ'] if 'TZ' in os.environ else 'Europe/Kiev')
# True means holiday
WORKING_DAYS = read_json('working_days.json')
43 changes: 43 additions & 0 deletions openprocurement/auction/bridge_utils/design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from couchdb.design import ViewDefinition
from openprocurement.auction.bridge_utils.constants import (
INSIDER_WORKING_DAY_START, TEXAS_WORKING_DAY_START
)


def sync_design(db):
views = [j for i, j in globals().items() if "_view" in i]
ViewDefinition.sync_many(db, views)


plan_auctions_view = ViewDefinition('plan', 'auctions', '''function(doc) {
if(doc.streams || doc.dutch_streams || doc.texas_streams) {
for (var i in doc) {
if (i.indexOf('stream_') == 0) {
for (var t in doc[i]) {
if (doc[i][t]) {
var x = doc[i][t].split('_')
if (x.length == 2) {
emit(x, doc._id.split('_')[1] + 'T' + t);
} else {
emit([x[0], null],
doc._id.split('_')[1] + 'T' + t);
}
}
}
}
if (i.indexOf('dutch_streams') == 0) {
for (var aid in doc[i]) {
emit([doc[i][aid], null],
doc._id.split('_')[1] + 'T' + '%s');
}
}
if (i.indexOf('texas_streams') == 0) {
for (var aid in doc[i]) {
emit([doc[i][aid], null],
doc._id.split('_')[1] + 'T' + '%s');
}
}
};
}
}''' % (INSIDER_WORKING_DAY_START.isoformat(), TEXAS_WORKING_DAY_START.isoformat()))
237 changes: 237 additions & 0 deletions openprocurement/auction/bridge_utils/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# -*- coding: utf-8 -*-
import logging
import requests
from copy import deepcopy

from couchdb import ResourceConflict
from datetime import timedelta, datetime
from iso8601 import parse_date

from gevent.pool import Pool

from openprocurement.auction.bridge_utils.constants import (
DEFAULT_STREAMS_DOC,
TEXAS_WORKING_DAY_DURATION,
TEXAS_WORKING_DAY_START,
INSIDER_WORKING_DAY_DURATION,
INSIDER_WORKING_DAY_START,
WORKING_DAY_START,
WORKING_DAY_END,
STREAMS_ID,
TZ
)

POOL = Pool(1)

LOGGER = logging.getLogger('Openprocurement Auction')

ADAPTER = requests.adapters.HTTPAdapter(pool_connections=3, pool_maxsize=3)
SESSION = requests.Session()
SESSION.mount('http://', ADAPTER)
SESSION.mount('https://', ADAPTER)


def find_free_slot(plan):
streams = plan.get('streams', 0)
for cur_stream in range(1, streams + 1):
stream_id = 'stream_{}'.format(cur_stream)
for slot in plan[stream_id]:
if plan[stream_id].get(slot) is None:
plan_date = parse_date(plan['_id'].split('_')[1] + 'T' + slot, None)
plan_date = plan_date.astimezone(TZ) if plan_date.tzinfo else TZ.localize(plan_date)
return plan_date, cur_stream


class BaseAuctionsManager(object):
working_day_start = None
streams_key = None

def get_date(self, db, mode, date):
"""

Get hour of auction start and stream in which auction will run for
passed date.

:param db: chronograph database with plan docs
:type db: couchdb.Database
:param mode: value from auction procedure, indicates mode of auction, used in plan doc id
:type mode: str
:param date: preplanned date for current auction
:type date: datetime.date

:returns : (time of auction start, stream for auction, plan document)
:rtype: (datetime.time, int, couchdb.client.Document)
"""
plan_id = 'plan{}_{}'.format(mode, date.isoformat())
plan = db.get(plan_id, {'_id': plan_id})
plan_date_end, stream = self._get_hours_and_stream(plan)
plan_date = parse_date(date.isoformat() + 'T' + plan_date_end, None)
plan_date = plan_date.astimezone(TZ) if plan_date.tzinfo else TZ.localize(plan_date)
return plan_date.time(), stream, plan

def get_streams(self, db, streams_id=STREAMS_ID):
"""

Get allowed amount of streams for auction of particular type per day.

:param db: chronograph database
:type db: couchdb.Database
:param streams_id: id of document with defined stream amounts
:type streams_id: str

:return: amount of streams for auction per day
:rtype: int
"""
streams = db.get(streams_id, deepcopy(DEFAULT_STREAMS_DOC))
return streams.get(self.streams_key, DEFAULT_STREAMS_DOC[self.streams_key])

def _get_hours_and_stream(self, plan):
"""
Return time of auction start and first available stream from plan doc

:param plan: document for planned auctions
:type plan: couchdb.client.Document

:return: (time of auction start, first available stream)
:rtype: (str, int)
"""
raise NotImplementedError

def set_end_of_auction(self, *args, **kwargs):
"""
Try to find end of auction for passed date, time and plan doc if such
action is possible and return it's value for further auction planning
"""
raise NotImplementedError

def set_date(self, *args, **kwargs):
"""
Actually plan auction for particular date, time, stream and slot and
save it to chronograph database.
"""
raise NotImplementedError

def free_slot(self, *args, **kwargs):
"""
Remove particular auction from stream slot in particular plan document.
"""
raise NotImplementedError


class ClassicAuctionsManager(BaseAuctionsManager):
working_day_start = WORKING_DAY_START
working_day_end = WORKING_DAY_END
streams_key = 'streams'

def _get_hours_and_stream(self, plan):
plan_date_end = plan.get('time', self.working_day_start.isoformat())
stream = plan.get(self.streams_key, 1)
return plan_date_end, stream

def set_date(self, db, plan, auction_id, end_time,
cur_stream, start_time, new_slot=True):
if new_slot:
plan['time'] = end_time.isoformat()
plan[self.streams_key] = cur_stream
stream_id = 'stream_{}'.format(cur_stream)
stream = plan.get(stream_id, {})
stream[start_time.isoformat()] = auction_id
plan[stream_id] = stream
db.save(plan)

def free_slot(self, db, plan_id, auction_id, plan_time):
slot = plan_time.time().isoformat()
done = False
while not done:
try:
plan = db.get(plan_id)
streams = plan[self.streams_key]
for cur_stream in range(1, streams + 1):
stream_id = 'stream_{}'.format(cur_stream)
if plan[stream_id].get(slot) == auction_id:
plan[stream_id][slot] = None
db.save(plan)
done = True
except ResourceConflict:
done = False
except:
done = True

def set_end_of_auction(self, stream, streams, nextDate, dayStart, plan):
freeSlot = find_free_slot(plan)
if freeSlot:
startDate, stream = freeSlot
start, end, dayStart, new_slot = startDate, startDate, startDate.time(), False
return start, end, dayStart, stream, new_slot
if dayStart >= self.working_day_end and stream < streams:
stream += 1
dayStart = self.working_day_start

start = TZ.localize(datetime.combine(nextDate, dayStart))
end = start + timedelta(minutes=30)

if end <= TZ.localize(datetime.combine(nextDate, self.working_day_end)) and stream <= streams:
return start, end, dayStart, stream, True


class NonClassicAuctionsManager(BaseAuctionsManager):
working_day_start = None
working_day_duration = None
streams_key = None

def _get_hours_and_stream(self, plan):
plan_date_end = self.working_day_start.isoformat()
stream = len(plan.get(self.streams_key, []))
return plan_date_end, stream

def set_date(self, db, plan, auction_id, *args, **kwargs):
streams = plan.get(self.streams_key, [])
streams.append(auction_id)
plan[self.streams_key] = streams
db.save(plan)

def free_slot(self, db, plan_id, auction_id, *args, **kwargs):
done = False
while not done:
try:
plan = db.get(plan_id)
slots = plan.get(self.streams_key, [])
pops = []
for i in xrange(0, len(slots)):
if slots[i] == auction_id:
pops.append(i)
pops.sort(reverse=True)
for p in pops:
slots.pop(p)
plan[self.streams_key] = slots
db.save(plan)
done = True
except ResourceConflict:
done = False
except:
done = True

def set_end_of_auction(self, stream, streams, nextDate, dayStart, *args, **kwargs):
if stream < streams:
start = TZ.localize(datetime.combine(nextDate, dayStart))
end = start + self.working_day_duration
return start, end, dayStart, stream, False


class TexasAuctionsManager(NonClassicAuctionsManager):
working_day_start = TEXAS_WORKING_DAY_START
working_day_duration = TEXAS_WORKING_DAY_DURATION
streams_key = 'texas_streams'


class InsiderAuctionsManager(NonClassicAuctionsManager):
working_day_start = INSIDER_WORKING_DAY_START
working_day_duration = INSIDER_WORKING_DAY_DURATION
streams_key = 'dutch_streams'


MANAGERS_MAPPING = {
'texas': TexasAuctionsManager,
'dutch': InsiderAuctionsManager,
'english': ClassicAuctionsManager
}
Loading