Compare commits

..

9 Commits

Author SHA1 Message Date
7fadeb1a49 Bump version: 0.21.3 → 0.21.4 2020-09-21 11:26:51 +03:00
b723660f23 Fixups and type checks 2020-09-21 11:26:32 +03:00
f10eeec498 requirement update 2020-09-21 11:24:01 +03:00
230167f93d mypy bugfix 2020-09-21 11:23:43 +03:00
d5ed989e80 Bump version: 0.21.2.2 → 0.21.3 2020-08-18 16:37:27 +03:00
6fc24b8adf requirements 2020-08-18 16:37:07 +03:00
cf797f2f60 Fixes and updates 2020-08-18 13:14:41 +03:00
ad29045ace Bump version: 0.21.2.1 → 0.21.2.2 2020-07-29 11:40:41 +03:00
c919e46af5 PoliticsAPI extended and bugfixed 2020-07-29 11:40:32 +03:00
9 changed files with 121 additions and 96 deletions

View File

@ -88,9 +88,3 @@ dist: clean ## builds source and wheel package
install: clean ## install the package to the active Python's site-packages
python setup.py install
setcommit:
bash set_commit_id.sh
# commit=`git log -1 --pretty=format:%h`
# sed -i.bak -E "s|COMMIT_ID = \".+\"|COMMIT_ID = \"$(commit)\"|g" erepublik/utils.py
# mv erepublik/utils.py.bak erepublik/utils.py

View File

@ -4,7 +4,7 @@
__author__ = """Eriks Karls"""
__email__ = 'eriks@72.lv'
__version__ = '0.21.2.1'
__version__ = '0.21.4'
from erepublik import classes, utils, constants
from erepublik.citizen import Citizen

View File

@ -15,24 +15,26 @@ class SlowRequests(Session):
timeout = datetime.timedelta(milliseconds=500)
uas = [
# Chrome
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36', # noqa
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36', # noqa
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36', # noqa
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', # noqa
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', # noqa
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
# FireFox
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:76.0) Gecko/20100101 Firefox/76.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:73.0) Gecko/20100101 Firefox/73.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:76.0) Gecko/20100101 Firefox/76.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:74.0) Gecko/20100101 Firefox/74.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:73.0) Gecko/20100101 Firefox/73.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:82.0) Gecko/20100101 Firefox/82.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:81.0) Gecko/20100101 Firefox/81.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:81.0) Gecko/20100101 Firefox/81.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:79.0) Gecko/20100101 Firefox/79.0',
'Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0',
]
debug = False
@ -113,12 +115,13 @@ class SlowRequests(Session):
class CitizenBaseAPI:
url: str = "https://www.erepublik.com/en"
_req: SlowRequests = None
token: str = ""
_req: SlowRequests
token: str
def __init__(self):
""" Class for unifying eRepublik known endpoints and their required/optional parameters """
self._req = SlowRequests()
self.token = ""
def post(self, url: str, data=None, json=None, **kwargs) -> Response:
return self._req.post(url, data, json, **kwargs)
@ -433,7 +436,7 @@ class ErepublikMilitaryAPI(CitizenBaseAPI):
class ErepublikPoliticsAPI(CitizenBaseAPI):
def _get_candidate_party(self, party_slug: str) -> Response:
return self.post(f"{self.url}/candidate/{party_slug}")
return self.get(f"{self.url}/candidate/{party_slug}")
def _get_main_party_members(self, party_id: int) -> Response:
return self.get(f"{self.url}/main/party-members/{party_id}")
@ -448,6 +451,13 @@ class ErepublikPoliticsAPI(CitizenBaseAPI):
def _get_presidential_elections(self, country_id: int, timestamp: int) -> Response:
return self.get(f"{self.url}/main/presidential-elections/{country_id}/{timestamp}")
def _post_propose_president_candidate(self, party_slug: str, citizen_id: int) -> Response:
return self.post(f"{self.url}/propose-president-candidate/{party_slug}",
data=dict(_token=self.token, citizen=citizen_id))
def _get_auto_propose_president_candidate(self, party_slug: str) -> Response:
return self.get(f"{self.url}/auto-propose-president-candidate/{party_slug}")
class ErepublikPresidentAPI(CitizenBaseAPI):
def _post_wars_attack_region(self, war_id: int, region_id: int, region_name: str) -> Response:

View File

@ -833,23 +833,21 @@ class CitizenCompanies(BaseCitizen):
def work_as_manager_in_holding(self, holding: classes.Holding) -> Optional[Dict[str, Any]]:
return self._work_as_manager(holding)
def _work_as_manager(self, wam_holding: classes.Holding = None) -> Optional[Dict[str, Any]]:
def _work_as_manager(self, wam_holding: classes.Holding) -> Optional[Dict[str, Any]]:
if self.restricted_ip:
return None
self.update_companies()
self.update_inventory()
data = {"action_type": "production"}
extra = {}
wam_list = []
if wam_holding:
raw_factories = wam_holding.get_wam_companies(raw_factory=True)
fin_factories = wam_holding.get_wam_companies(raw_factory=False)
raw_factories = wam_holding.get_wam_companies(raw_factory=True)
fin_factories = wam_holding.get_wam_companies(raw_factory=False)
free_inventory = self.inventory["total"] - self.inventory["used"]
wam_list = raw_factories + fin_factories
wam_list = wam_list[:self.energy.food_fights]
while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list):
wam_list.pop(-1)
free_inventory = self.inventory["total"] - self.inventory["used"]
wam_list = raw_factories + fin_factories
wam_list = wam_list[:self.energy.food_fights]
while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list):
wam_list.pop(-1)
if wam_list:
data.update(extra)
@ -1016,7 +1014,7 @@ class CitizenEconomy(CitizenTravel):
self.write_log(f"Trying to sell unsupported industry {industry}")
data = {
"country_id": self.details.citizenship,
"country_id": self.details.citizenship.id,
"industry": industry,
"quality": quality,
"amount": amount,
@ -1071,7 +1069,7 @@ class CitizenEconomy(CitizenTravel):
start_dt = self.now
iterable = [countries, [quality] if quality else range(1, max_quality + 1)]
for country, q in product(*iterable):
r = self._post_economy_marketplace(country, constants.INDUSTRIES[product_name], q).json()
r = self._post_economy_marketplace(country.id, constants.INDUSTRIES[product_name], q).json()
obj = offers[f"q{q}"]
if not r.get("error", False):
for offer in r["offers"]:
@ -1525,7 +1523,7 @@ class CitizenMilitary(CitizenTravel):
if not division.terrain and is_start_ok and not division.div_end:
if division.is_air and self.config.air:
division_medals = self.get_battle_round_data(division)
medal = division_medals[self.details.citizenship == division.battle.defender.country]
medal = division_medals[self.details.citizenship == division.battle.is_defender.country]
if not medal:
air_divs.append((0, division))
else:
@ -1534,14 +1532,14 @@ class CitizenMilitary(CitizenTravel):
if not division.div == self.division and not self.maverick:
continue
division_medals = self.get_battle_round_data(division)
medal = division_medals[self.details.citizenship == division.battle.defender.country]
medal = division_medals[self.details.citizenship == division.battle.is_defender.country]
if not medal:
ground_divs.append((0, division))
else:
ground_divs.append((medal.get('1').get('raw_value'), division))
air_divs.sort(key=lambda z: (z[0],z[1].battle.start))
ground_divs.sort(key=lambda z:(z[0],z[1].battle.start))
air_divs.sort(key=lambda z: (z[0], z[1].battle.start))
ground_divs.sort(key=lambda z: (z[0], z[1].battle.start))
return {'air': air_divs, 'ground': ground_divs}
@property
@ -1609,7 +1607,8 @@ class CitizenMilitary(CitizenTravel):
self.travel_to_residence()
break
def fight(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide = None, count: int = None) -> int:
def fight(self, battle: classes.Battle, division: classes.BattleDivision, side: classes.BattleSide = None,
count: int = None) -> int:
"""Fight in a battle.
Will auto activate booster and travel if allowed to do it.
@ -1655,6 +1654,9 @@ class CitizenMilitary(CitizenTravel):
self.write_log("Hits: {:>4} | Damage: {}".format(total_hits, total_damage))
ok_to_fight = False
if total_damage:
self.reporter.report_action('FIGHT', dict(battle_id=battle.id, side=side, dmg=total_damage,
air=battle.has_air, hits=total_hits,
round=battle.zone_id))
self.reporter.report_action("FIGHT", dict(battle=str(battle), side=str(side), dmg=total_damage,
air=battle.has_air, hits=total_hits))
return error_count
@ -1763,7 +1765,8 @@ class CitizenMilitary(CitizenTravel):
if resp.json().get('error'):
self.write_log(resp.json().get('message'))
return False
self._report_action("MILITARY_DIV_SWITCH", f"Switched to d{division.div} in battle {battle.id}", kwargs=resp.json())
self._report_action("MILITARY_DIV_SWITCH", f"Switched to d{division.div} in battle {battle.id}",
kwargs=resp.json())
return True
def get_ground_hit_dmg_value(self, rang: int = None, strength: float = None, elite: bool = None, ne: bool = False,
@ -1903,7 +1906,7 @@ class CitizenMilitary(CitizenTravel):
battleZoneId=division.id, type="damage")
r_json = r.json()
return (r_json.get(str(battle.invader.id)).get("fighterData"),
r_json.get(str(battle.defender.id)).get("fighterData"))
r_json.get(str(battle.is_defender.id)).get("fighterData"))
def schedule_attack(self, war_id: int, region_id: int, region_name: str, at_time: datetime):
if at_time:
@ -1963,6 +1966,13 @@ class CitizenMilitary(CitizenTravel):
return member.get('panelContents', {}).get('members', [{}])[0].get('dailyOrdersCompleted')
return 0
def get_possibly_empty_medals(self):
self.update_war_info()
for battle in self.all_battles.values():
for division in battle.div.values():
if division.wall['dom'] == 50 or division.wall['dom'] > 98:
yield division, division.wall['for'] == battle.invader.country.id
class CitizenPolitics(BaseCitizen):
def get_country_parties(self, country: constants.Country = None) -> dict:
@ -2335,11 +2345,11 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
self.reporter.report_action("NEW_MEDAL", info)
def set_debug(self, debug: bool):
self.debug = debug
self._req.debug = debug
self.debug = bool(debug)
self._req.debug = bool(debug)
def set_pin(self, pin: int):
self.details.pin = pin
self.details.pin = int(pin)
def update_all(self, force_update=False):
# Do full update max every 5 min
@ -2512,7 +2522,8 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
else:
if not start_place == (self.details.current_country, self.details.current_region):
self.travel_to_holding(holding)
return self._wam(holding)
self._wam(holding)
return
elif response.get("message") == "not_enough_health_food":
self.buy_food()
self._wam(holding)
@ -2528,7 +2539,7 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
:rtype: bool
"""
if self.restricted_ip:
self._report_action("IP_BLACKLISTED", "Fighting is not allowed from restricted IP!")
self._report_action("IP_BLACKLISTED", "Work as manager is not allowed from restricted IP!")
return False
self.update_citizen_info()
self.update_companies()
@ -2545,6 +2556,10 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
self.update_companies()
for holding in regions.values():
raw_usage = holding.get_wam_raw_usage()
if (raw_usage['frm'] + raw_usage['wrm']) * 100 + self.inventory['used'] > self.inventory['total']:
self._report_action('WAM_UNAVAILABLE', 'Not enough storage!')
continue
self.travel_to_holding(holding)
self._wam(holding)
self.update_companies()

View File

@ -294,7 +294,7 @@ class MyCompanies:
def __clear_data(self):
for holding in self.holdings.values():
for company in holding.companies:
for company in holding.companies: # noqa
del company
holding.companies.clear()
self.companies.clear()
@ -549,7 +549,10 @@ class Reporter:
self._citizen = weakref.ref(citizen)
self._req = Session()
self.url = "https://api.erep.lv"
self._req.headers.update({"user-agent": "Bot reporter v2"})
self._req.headers.update({"user-agent": "eRepublik Script Reporter v3",
'erep-version': utils.__version__,
'erep-user-id': self.citizen_id,
'erep-user-name': self.citizen.name})
self.__to_update = []
self.__registered: bool = False
@ -569,8 +572,8 @@ class Reporter:
def __bot_update(self, data: dict) -> Response:
if self.__to_update:
for unreported_data in self.__to_update:
unreported_data.update(player_id=self.citizen.id, key=self.key)
unreported_data = utils.json.load(utils.json.dumps(unreported_data, cls=MyJSONEncoder))
unreported_data.update(player_id=self.citizen_id, key=self.key)
unreported_data = utils.json.loads(utils.json.dumps(unreported_data, cls=MyJSONEncoder))
self._req.post("{}/bot/update".format(self.url), json=unreported_data)
self.__to_update.clear()
data = utils.json.loads(utils.json.dumps(data, cls=MyJSONEncoder))
@ -626,13 +629,13 @@ class Reporter:
except: # noqa
return []
def fetch_tasks(self) -> Optional[Tuple[str, Tuple[Any]]]:
def fetch_tasks(self) -> List[str, Tuple[Any]]:
try:
task_response = self._req.get(f'{self.url}/api/v1/command',
params=dict(citizen=self.citizen_id, key=self.key))
return task_response.json().get('task_collection')
except: # noqa
return
return []
class MyJSONEncoder(utils.json.JSONEncoder):
@ -669,7 +672,7 @@ class BattleSide:
battle: "Battle"
_battle: weakref.ReferenceType
country: constants.Country
defender: bool
is_defender: bool
def __init__(self, battle: "Battle", country: constants.Country, points: int, allies: List[constants.Country],
deployed: List[constants.Country], defender: bool):
@ -678,18 +681,18 @@ class BattleSide:
self.points = points
self.allies = allies
self.deployed = deployed
self.defender = defender
self.is_defender = defender
@property
def id(self) -> int:
return self.country.id
def __repr__(self):
side_text = "Defender" if self.defender else "Invader "
side_text = "Defender" if self.is_defender else "Invader "
return f"<BattleSide: {side_text} {self.country.name}|{self.points:02d}p>"
def __str__(self):
side_text = "Defender" if self.defender else "Invader "
side_text = "Defender" if self.is_defender else "Invader "
return f"{side_text} {self.country.name} - {self.points:02d} points"
def __format__(self, format_spec):
@ -697,8 +700,8 @@ class BattleSide:
@property
def as_dict(self):
return dict(points=self.points, country=self.country, defender=self.defender, allies=self.allies,
deployed=self.deployed, battle=repr(self.battle))
return dict(points=self.points, country=self.country, is_defender=self.is_defender, allies=self.allies,
deployed=self.deployed)
@property
def battle(self):
@ -721,7 +724,7 @@ class BattleDivision:
@property
def as_dict(self):
return dict(id=self.id, division=self.div, terrain=(self.terrain, self.terrain_display), wall=self.wall,
epic=self.epic, battle=str(self.battle), end=self.div_end)
epic=self.epic, end=self.div_end)
@property
def is_air(self):
@ -788,7 +791,7 @@ class Battle:
def as_dict(self):
return dict(id=self.id, war_id=self.war_id, divisions=self.div, zone=self.zone_id, rw=self.is_rw,
dict_lib=self.is_dict_lib, start=self.start, sides={'inv': self.invader, 'def': self.defender},
region=[self.region_id, self.region_name])
region=[self.region_id, self.region_name], link=self.link)
@property
def has_air(self) -> bool:
@ -797,6 +800,10 @@ class Battle:
return True
return not bool(self.zone_id % 4)
@property
def has_started(self) -> bool:
return self.start <= utils.now()
@property
def has_ground(self) -> bool:
for div in self.div.values():
@ -920,7 +927,7 @@ class TelegramBot:
def as_dict(self):
return {'chat_id': self.chat_id, 'api_url': self.api_url, 'player': self.player_name,
'last_time': self._last_time, 'next_time': self._next_time, 'queue': self.__queue,
'initialized': self.__initialized, 'has_threads': bool(len(self._threads))}
'initialized': self.__initialized, 'has_threads': not self._threads}
def do_init(self, chat_id: int, token: str, player_name: str = ""):
self.chat_id = chat_id

View File

@ -1,4 +1,3 @@
import copy
import datetime
import inspect
import os
@ -10,7 +9,7 @@ import traceback
import unicodedata
from decimal import Decimal
from pathlib import Path
from typing import Any, List, Mapping, Optional, Union, Dict
from typing import Any, List, Optional, Union, Dict
import requests
@ -43,12 +42,11 @@ def localize_timestamp(timestamp: int) -> datetime.datetime:
def localize_dt(dt: Union[datetime.date, datetime.datetime]) -> datetime.datetime:
try:
try:
return constants.erep_tz.localize(dt)
except AttributeError:
return constants.erep_tz.localize(datetime.datetime.combine(dt, datetime.time(0, 0, 0)))
except ValueError:
if isinstance(dt, datetime.datetime):
return constants.erep_tz.localize(dt)
elif isinstance(dt, datetime.date):
return constants.erep_tz.localize(datetime.datetime.combine(dt, datetime.time(0, 0, 0)))
else:
return dt.astimezone(constants.erep_tz)
@ -117,10 +115,12 @@ def _write_log(msg, timestamp: bool = True, should_print: bool = False):
def write_interactive_log(*args, **kwargs):
kwargs.pop("should_print", None)
_write_log(should_print=True, *args, **kwargs)
def write_silent_log(*args, **kwargs):
kwargs.pop("should_print", None)
_write_log(should_print=False, *args, **kwargs)
@ -278,16 +278,17 @@ def process_error(log_info: str, name: str, exc_info: tuple, citizen=None, commi
elif interactive is not None:
write_silent_log(log_info)
trace = inspect.trace()
local_vars = None
if trace:
trace = trace[-1][0].f_locals
if trace.get('__name__') == '__main__':
trace = {'commit_id': trace.get('COMMIT_ID'),
'interactive': trace.get('INTERACTIVE'),
'version': trace.get('__version__'),
'config': trace.get('CONFIG')}
trace_local_vars = trace[-1][0].f_locals
if trace_local_vars.get('__name__') == '__main__':
local_vars = {'commit_id': trace_local_vars.get('COMMIT_ID'),
'interactive': trace_local_vars.get('INTERACTIVE'),
'version': trace_local_vars.get('__version__'),
'config': trace_local_vars.get('CONFIG')}
else:
trace = dict()
send_email(name, content, citizen, local_vars=trace)
local_vars = dict()
send_email(name, content, citizen, local_vars=local_vars)
def process_warning(log_info: str, name: str, exc_info: tuple, citizen=None, commit_id: str = None):
@ -307,10 +308,10 @@ def process_warning(log_info: str, name: str, exc_info: tuple, citizen=None, com
trace = inspect.trace()
if trace:
trace = trace[-1][0].f_locals
local_vars = trace[-1][0].f_locals
else:
trace = dict()
send_email(name, content, citizen, local_vars=trace)
local_vars = dict()
send_email(name, content, citizen, local_vars=local_vars)
def slugify(value, allow_unicode=False) -> str:
@ -371,8 +372,6 @@ def get_air_hit_dmg_value(citizen_id: int, natural_enemy: bool = False, true_pat
def _clear_up_battle_memory(battle):
from . import classes
battle: classes.Battle
del battle.invader._battle, battle.defender._battle
for div_id, division in battle.div.items():
del division._battle

View File

@ -1,17 +1,18 @@
bump2version==1.0.0
coverage==5.2
coverage==5.3
edx-sphinx-theme==1.5.0
flake8==3.8.3
ipython==7.16.1
isort==5.0.9
pip==20.1.1
PyInstaller==3.6
ipython==7.18.1
isort==5.5.3
pip==20.2.3
PyInstaller==4.0
pytz==2020.1
pytest==5.4.3
responses==0.10.15
setuptools==49.2.0
Sphinx==3.1.2
tox==3.16.1
pytest==6.0.2
responses==0.12.0
setuptools==50.3.0
Sphinx==3.2.1
requests==2.24.0
tox==3.20.0
twine==3.2.0
watchdog==0.10.3
wheel==0.34.2
wheel==0.35.1

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.21.2.1
current_version = 0.21.4
commit = True
tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)?
@ -33,7 +33,6 @@ ignore_missing_imports = False
warn_unused_ignores = True
warn_redundant_casts = True
warn_unused_configs = True
plugins = mypy_django_plugin.main
[isort]
multi_line_output = 2

View File

@ -43,6 +43,6 @@ setup(
test_suite='tests',
tests_require=test_requirements,
url='https://github.com/eeriks/erepublik/',
version='0.21.2.1',
version='0.21.4',
zip_safe=False,
)