Compare commits

...

53 Commits

Author SHA1 Message Date
81b1069cf0 Bump version: 0.22.3 → 0.22.3.1 2020-11-20 14:46:04 +02:00
429d43df15 CitizenTasks.work() bugfix: When employer out of money - resign 2020-11-20 14:45:58 +02:00
c81986d65e CitizenEconomy.post_market_offer() bugfix 2020-11-20 14:31:16 +02:00
7bb988f716 PEP8 2020-11-20 14:24:11 +02:00
16cae24712 Bump version: 0.22.2.1 → 0.22.3 2020-11-16 12:12:30 +02:00
3e051fe906 History update 2020-11-16 12:07:20 +02:00
aa9cda9314 eRepublik temp technical difficulties 2020-11-16 12:06:59 +02:00
fc66db8cab Config file generator update 2020-11-16 11:56:37 +02:00
6bbc4f8768 eRepublik has both airplaneRaw and aircraftRaw. Possible future bugs because of this! 2020-11-16 11:56:13 +02:00
160b32a914 Buy market offer directly 2020-11-16 11:52:55 +02:00
a51c3c620e Python round to even bugfix.
`{"stock":10.494533,"consume":10.5}` rounds to 0
2020-11-16 11:52:27 +02:00
a1c26468eb UA updates 2020-11-12 11:47:20 +02:00
4895ae3663 Bump version: 0.22.2 → 0.22.2.1 2020-11-10 15:39:15 +02:00
b8d7cc8d7c Example updates 2020-11-10 15:38:30 +02:00
1d0645b490 Bump version: 0.22.1.5 → 0.22.2 2020-11-09 16:34:52 +02:00
30cf6203b7 History update 2020-11-09 16:34:41 +02:00
a32e88218d CitizenEconomy.get_market_offers now accepts searching tickets and the new aircraft qualities 2020-11-09 16:28:21 +02:00
a031da0ee7 Bump version: 0.22.1.4 → 0.22.1.5 2020-11-04 17:22:44 +02:00
bdb13fa4ae History update, WAM actions are now more verbose 2020-11-04 17:22:35 +02:00
e1e3b33d46 requirement update 2020-11-03 18:10:43 +02:00
e09ca143b1 Bump version: 0.22.1.3 → 0.22.1.4 2020-11-03 14:31:43 +02:00
61d0599295 House renewal bugfix 2020-11-03 14:31:37 +02:00
1ef600492a House renewal bugfix 2020-11-03 14:30:20 +02:00
377eda6445 Inventory booster bugfix 2020-11-03 14:29:42 +02:00
fd13667ca8 Bump version: 0.22.1.2 → 0.22.1.3 2020-10-30 12:58:26 +02:00
0479afcabe Report if out of money to work as manager 2020-10-30 12:58:18 +02:00
486f022f35 Unified Item namig from constants.INDUSTRIES 2020-10-30 12:43:45 +02:00
638373e452 Bump version: 0.22.1.1 → 0.22.1.2 2020-10-27 17:04:40 +02:00
04f357cc70 Bugfix 2020-10-27 17:04:37 +02:00
ed4ffe5af6 Bump version: 0.22.1 → 0.22.1.1 2020-10-22 18:04:13 +03:00
8aa90a7dbf Lint 2020-10-22 18:04:06 +03:00
e798859105 Bump version: 0.22.0 → 0.22.1 2020-10-22 16:27:09 +03:00
241f1642ce Session dump/load, Citizen.inventory update, memory and network optimization, python3.6 support 2020-10-22 16:26:59 +03:00
a4128b5d89 Bump version: 0.21.5.8 → 0.22.0 2020-10-21 14:46:24 +03:00
22c2a0ffd2 New Features!
Dump session to file!
Load session from file!
2020-10-21 14:45:29 +03:00
38f0335354 Bump version: 0.21.5.7 → 0.21.5.8 2020-10-07 09:22:01 +03:00
889435b94e House renewal bugfix 2020-10-07 09:21:36 +03:00
bb16c27674 Bump version: 0.21.5.6 → 0.21.5.7 2020-10-06 12:35:19 +03:00
963d7ca11a bugfix 2020-10-06 12:35:14 +03:00
36c7fefdf7 Bump version: 0.21.5.5 → 0.21.5.6 2020-09-30 08:40:14 +03:00
d9fa30b06e bugfix in default weapon switch 2020-09-30 08:35:35 +03:00
b53dc447f4 switch to deploy 2020-09-30 08:13:36 +03:00
233d8d83f8 Bump version: 0.21.5.4 → 0.21.5.5 2020-09-29 18:02:52 +03:00
ec62d90aa2 wheeloffortune bugfix 2020-09-29 18:02:52 +03:00
0c433a56da Bump version: 0.21.5.3 → 0.21.5.4 2020-09-29 17:38:44 +03:00
ad24338f4d wheeloffortune bugfix 2020-09-29 17:38:26 +03:00
6f4bc65d1b Bump version: 0.21.5.2 → 0.21.5.3 2020-09-29 17:21:00 +03:00
cc09ba7ee7 wheeloffortune argument bugfix 2020-09-29 17:20:53 +03:00
9e1166a460 Bump version: 0.21.5.1 → 0.21.5.2 2020-09-29 17:17:07 +03:00
fb0042c00d wheeloffortune url bugfix 2020-09-29 17:17:00 +03:00
bb800578e7 Bump version: 0.21.5 → 0.21.5.1 2020-09-29 15:06:39 +03:00
7025f750dc PySocks as requirement 2020-09-29 15:06:30 +03:00
bf77f21b60 MyCompanies export as dict optimised 2020-09-29 15:04:51 +03:00
16 changed files with 566 additions and 377 deletions

1
.gitignore vendored
View File

@ -104,3 +104,4 @@ ENV/
debug/ debug/
log/ log/
docs/ docs/
*dump.json

View File

@ -2,6 +2,33 @@
History History
======= =======
0.22.3 (2020-11-16)
-------------------
* Fixed round to even bug when doing wam and not enough raw.
* Added meta industry airplaneRaw
* Added method `Citizen.buy_market_offer(OfferItem, amount=None)` to directly buy market offer with included travel to country and back.
0.22.2 (2020-11-09)
-------------------
* Allow querying market offers for q2-q5 aircrafts
* Added "Ticket" industry
0.22.1 (2020-11-04)
-------------------
* Requirement update
* Unified product naming in inventory and other places based on `erepublik.constants.INDUSTRIES` values
* `erepublik.Citizen` parameter `auto_login` now defaults to `False`
* Continued work on more verbose action and result logging
0.22.0 (2020-10-22)
-------------------
* Ability to dump session and restore from file
* Proxy support
* Inventory updates
* Remove market offers
* Memory and network optimizations
* Python 3.6 supported
0.20.0 (2020-06-15) 0.20.0 (2020-06-15)
------------------- -------------------
* Massive restructuring * Massive restructuring

View File

@ -93,8 +93,8 @@
<label class="custom-control-label" for="wam">Work as manager</label> <label class="custom-control-label" for="wam">Work as manager</label>
</div> </div>
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" onchange="updateJson()" id="employ"> <input type="checkbox" class="custom-control-input" onchange="updateJson()" id="employees">
<label class="custom-control-label" for="employ">Employ employees</label> <label class="custom-control-label" for="employees">Employ employees</label>
</div> </div>
<div class="custom-control custom-switch"> <div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" onchange="updateJson()" id="auto_buy_raw" checked> <input type="checkbox" class="custom-control-input" onchange="updateJson()" id="auto_buy_raw" checked>
@ -236,8 +236,8 @@
config.debug = debug.checked; config.debug = debug.checked;
let wam = document.getElementById('wam'); // Generated let wam = document.getElementById('wam'); // Generated
config.wam = wam.checked; config.wam = wam.checked;
let employ = document.getElementById('employ'); // Generated let employees = document.getElementById('employees'); // Generated
config.employ = employ.checked; config.employees = employees.checked;
let auto_buy_raw = document.getElementById('auto_buy_raw'); // Generated let auto_buy_raw = document.getElementById('auto_buy_raw'); // Generated
let auto_sell_all = document.getElementById('auto_sell_all'); // Generated let auto_sell_all = document.getElementById('auto_sell_all'); // Generated
@ -249,7 +249,7 @@
let auto_sell_house = document.getElementById('auto_sell_house'); // Generated let auto_sell_house = document.getElementById('auto_sell_house'); // Generated
let auto_sell_arm = document.getElementById('auto_sell_arm'); // Generated let auto_sell_arm = document.getElementById('auto_sell_arm'); // Generated
let auto_sell_air = document.getElementById('auto_sell_air'); // Generated let auto_sell_air = document.getElementById('auto_sell_air'); // Generated
if (config.wam || config.employ) { if (config.wam || config.employees) {
auto_buy_raw.disabled = false; auto_buy_raw.disabled = false;
auto_sell_all.disabled = false; auto_sell_all.disabled = false;
auto_sell_frm.disabled = false; auto_sell_frm.disabled = false;
@ -359,7 +359,7 @@
"interactive": true, "interactive": true,
"debug": true, "debug": true,
"wam": true, "wam": true,
"employ": true, "employees": true,
"auto_buy_raw": true, "auto_buy_raw": true,
"auto_sell_all": true, "auto_sell_all": true,
"auto_sell": [ "auto_sell": [

View File

@ -4,9 +4,9 @@
__author__ = """Eriks Karls""" __author__ = """Eriks Karls"""
__email__ = 'eriks@72.lv' __email__ = 'eriks@72.lv'
__version__ = '0.21.5' __version__ = '0.22.3.1'
from erepublik import classes, utils, constants from erepublik import classes, utils, constants
from erepublik.citizen import Citizen from erepublik.citizen import Citizen
__all__ = ["classes", "utils", "Citizen", ] __all__ = ["classes", "utils", "Citizen", 'constants']

View File

@ -15,9 +15,11 @@ class SlowRequests(Session):
timeout: datetime.timedelta = datetime.timedelta(milliseconds=500) timeout: datetime.timedelta = datetime.timedelta(milliseconds=500)
uas: List[str] = [ uas: List[str] = [
# Chrome # Chrome
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36', # noqa
'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/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/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/80.0.3987.106 Safari/537.36', # noqa
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36',
'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/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/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/80.0.3987.106 Safari/537.36',
@ -164,10 +166,10 @@ class ErepublikAnniversaryAPI(CitizenBaseAPI):
return self.post(f"{self.url}/main/map-rewards-claim", data=data) return self.post(f"{self.url}/main/map-rewards-claim", data=data)
def _post_main_wheel_of_fortune_spin(self, cost) -> Response: def _post_main_wheel_of_fortune_spin(self, cost) -> Response:
return self.post(f"{self.url}/wheeloffortune-spin", data={'_token': self.token, "cost": cost}) return self.post(f"{self.url}/main/wheeloffortune-spin", data={'_token': self.token, "_currentCost": cost})
def _post_main_wheel_of_fortune_build(self) -> Response: def _post_main_wheel_of_fortune_build(self) -> Response:
return self.post(f"{self.url}/wheeloffortune-build", data={'_token': self.token}) return self.post(f"{self.url}/main/wheeloffortune-build", data={'_token': self.token})
class ErepublikArticleAPI(CitizenBaseAPI): class ErepublikArticleAPI(CitizenBaseAPI):
@ -344,27 +346,32 @@ class ErepublikEconomyAPI(CitizenBaseAPI):
orderBy="price_asc" if order_asc else "price_desc", _token=self.token) orderBy="price_asc" if order_asc else "price_desc", _token=self.token)
return self.post(f"{self.url}/economy/marketplaceAjax", data=data) return self.post(f"{self.url}/economy/marketplaceAjax", data=data)
def _post_economy_marketplace_actions(self, amount: int, buy: bool = False, **kwargs) -> Response: def _post_economy_marketplace_actions(self, action: str, **kwargs) -> Response:
if buy: if action == 'buy':
data = dict(_token=self.token, offerId=kwargs['offer'], amount=amount, orderBy="price_asc", currentPage=1, data = dict(_token=self.token, offerId=kwargs['offer'], amount=kwargs['amount'],
buyAction=1) orderBy="price_asc", currentPage=1, buyAction=1)
else: elif action == 'sell':
data = dict(_token=self.token, countryId=kwargs["country_id"], price=kwargs["price"], data = dict(_token=self.token, countryId=kwargs["country_id"], price=kwargs["price"],
industryId=kwargs["industry"], quality=kwargs["quality"], amount=amount, sellAction='postOffer') industryId=kwargs["industry"], quality=kwargs["quality"], amount=kwargs['amount'],
sellAction='postOffer')
elif action == 'delete':
data = dict(_token=self.token, offerId=kwargs["offer_id"], sellAction='deleteOffer')
else:
raise ValueError(f"Action '{action}' is not supported! Only 'buy/sell/delete' actions are available")
return self.post(f"{self.url}/economy/marketplaceActions", data=data) return self.post(f"{self.url}/economy/marketplaceActions", data=data)
class ErepublikLeaderBoardAPI(CitizenBaseAPI): class ErepublikLeaderBoardAPI(CitizenBaseAPI):
def _get_main_leaderboards_damage_aircraft_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0) -> Response: def _get_main_leaderboards_damage_aircraft_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-damage-aircraft-rankings/{country_id}/{weeks}/{mu_id}/0") return self.get(f"{self.url}/main/leaderboards-damage-aircraft-rankings/{country_id}/{weeks}/{mu_id}/0")
def _get_main_leaderboards_damage_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0, div: int = 0) -> Response: def _get_main_leaderboards_damage_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0, div: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-damage-rankings/{country_id}/{weeks}/{mu_id}/{div}") return self.get(f"{self.url}/main/leaderboards-damage-rankings/{country_id}/{weeks}/{mu_id}/{div}")
def _get_main_leaderboards_kills_aircraft_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0) -> Response: def _get_main_leaderboards_kills_aircraft_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-kills-aircraft-rankings/{country_id}/{weeks}/{mu_id}/0") return self.get(f"{self.url}/main/leaderboards-kills-aircraft-rankings/{country_id}/{weeks}/{mu_id}/0")
def _get_main_leaderboards_kills_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0, div: int = 0) -> Response: def _get_main_leaderboards_kills_rankings(self, country_id: int, weeks: int = 0, mu_id: int = 0, div: int = 0) -> Response: # noqa
return self.get(f"{self.url}/main/leaderboards-kills-rankings/{country_id}/{weeks}/{mu_id}/{div}") return self.get(f"{self.url}/main/leaderboards-kills-rankings/{country_id}/{weeks}/{mu_id}/{div}")

View File

@ -1,23 +1,27 @@
import re import re
import sys import sys
import warnings import warnings
import weakref
from datetime import datetime, timedelta from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from itertools import product from itertools import product
from threading import Event from threading import Event
from time import sleep from time import sleep
from typing import Any, Callable, Dict, List, NoReturn, Optional, Set, Tuple, Union from typing import Any, Dict, List, NoReturn, Optional, Set, Tuple, Union
from requests import HTTPError, RequestException, Response from requests import HTTPError, RequestException, Response
from . import utils, classes, access_points, constants from . import utils, classes, access_points, constants
from .classes import OfferItem
class BaseCitizen(access_points.CitizenAPI): class BaseCitizen(access_points.CitizenAPI):
_last_full_update: datetime = utils.now().min _last_full_update: datetime = constants.min_datetime
_last_inventory_update: datetime = constants.min_datetime
promos: Dict[str, datetime] = None promos: Dict[str, datetime] = None
inventory: Dict[str, int] = {'used': 0, 'total': 0} inventory: Dict[str, Dict[str, Dict[int, Dict[str, Union[str, int, float]]]]]
inventory_status: Dict[str, int]
boosters: Dict[int, Dict[int, int]] = {50: {}, 100: {}} boosters: Dict[int, Dict[int, int]] = {50: {}, 100: {}}
ot_points: int = 0 ot_points: int = 0
@ -56,6 +60,8 @@ class BaseCitizen(access_points.CitizenAPI):
self.config.email = email self.config.email = email
self.config.password = password self.config.password = password
self.inventory = {}
self.inventory_status = dict(used=0, total=0)
def get_csrf_token(self): def get_csrf_token(self):
""" """
@ -224,153 +230,171 @@ class BaseCitizen(access_points.CitizenAPI):
self.politics.is_party_president = bool(party.get('is_party_president')) self.politics.is_party_president = bool(party.get('is_party_president'))
self.politics.party_slug = f"{party.get('stripped_title')}-{party.get('party_id')}" self.politics.party_slug = f"{party.get('stripped_title')}-{party.get('party_id')}"
def update_inventory(self) -> Dict[str, Any]: def update_inventory(self):
""" """
Updates class properties and returns structured inventory. Updates class properties and returns structured inventory.
Return structure: {status: {used: int, total: int}, items: {active/final/raw: {item_token:{quality: data}}} Return structure: {status: {used: int, total: int}, items: {active/final/raw: {item_token:{quality: data}}}
If item kind is damageBoosters or aircraftDamageBoosters then kind is renamed to kind+quality and duration is If item kind is damageBoosters or aircraftDamageBoosters then kind is renamed to kind+quality and duration is
used as quality. used as quality.
:return: dict
""" """
self._update_inventory_data(self._get_economy_inventory_items().json())
def get_inventory(self, force: bool = False):
if utils.good_timedelta(self._last_inventory_update, timedelta(minutes=2)) < self.now or force:
self.update_inventory()
return self.inventory
def _update_inventory_data(self, inv_data: Dict[str, Any]):
if not isinstance(inv_data, dict):
raise TypeError("Parameter `inv_data` must be dict not '{type(data)}'!")
status = inv_data.get("inventoryStatus", {})
if status:
self.inventory_status.clear()
self.inventory_status.update(used=status.get("usedStorage"), total=status.get("totalStorage"))
data = inv_data.get('inventoryItems', {})
if not data:
return
self._last_inventory_update = self.now
self.food.update({"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0}) self.food.update({"q1": 0, "q2": 0, "q3": 0, "q4": 0, "q5": 0, "q6": 0, "q7": 0})
self.eb_small = self.eb_double = self.eb_normal = 0 self.eb_small = self.eb_double = self.eb_normal = 0
active_items: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
j = self._get_economy_inventory_items().json() if data.get("activeEnhancements", {}).get("items", {}):
active_items = {} for item_data in data.get("activeEnhancements", {}).get("items", {}).values():
if j.get("inventoryItems", {}).get("activeEnhancements", {}).get("items", {}): if item_data.get('token'):
for item in j.get("inventoryItems", {}).get("activeEnhancements", {}).get("items", {}).values(): kind = re.sub(r'_q\d\d*', "", item_data.get('token'))
if item.get('token'):
kind = re.sub(r'_q\d\d*', "", item.get('token'))
else: else:
kind = item.get('type') kind = item_data.get('type')
if constants.INDUSTRIES[kind]:
kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]]
if kind not in active_items: if kind not in active_items:
active_items[kind] = {} active_items[kind] = {}
icon = item['icon'] if item['icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png" icon = item_data['icon'] if item_data[
item_data = dict(name=item.get("name"), time_left=item['active']['time_left'], icon=icon, kind=kind, 'icon'] else "//www.erepublik.net/images/modules/manager/tab_storage.png"
quality=item.get("quality", 0)) item_data = dict(name=item_data.get("name"), time_left=item_data['active']['time_left'], icon=icon,
kind=kind,
quality=item_data.get("quality", 0))
if item.get('isPackBooster'): if item_data.get('isPackBooster'):
active_items[kind].update({0: item_data}) active_items[kind].update({0: item_data})
else: else:
active_items[kind].update({item.get("quality"): item_data}) active_items[kind].update({item_data.get("quality"): item_data})
final_items = {} final_items: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
for item in j.get("inventoryItems", {}).get("finalProducts", {}).get("items", {}).values(): if data.get("finalProducts", {}).get("items", {}):
name = item['name'] for item_data in data.get("finalProducts", {}).get("items", {}).values():
name = item_data['name']
if item.get('type'): if item_data.get('type'):
if item.get('type') in ['damageBoosters', "aircraftDamageBoosters"]: if item_data.get('type') in ['damageBoosters', "aircraftDamageBoosters"]:
kind = f"{item['type']}{item['quality']}" kind = f"{item_data['type']}{item_data['quality']}"
if item['quality'] == 5: if item_data['quality'] == 5:
self.boosters[50].update({item['duration']: item['amount']}) self.boosters[50].update({item_data['duration']: item_data['amount']})
elif item['quality'] == 10: elif item_data['quality'] == 10:
self.boosters[100].update({item['duration']: item['amount']}) self.boosters[100].update({item_data['duration']: item_data['amount']})
delta = item['duration'] delta = item_data['duration']
if delta // 3600: if delta // 3600:
name += f" {delta // 3600}h" name += f" {delta // 3600}h"
if delta // 60 % 60: if delta // 60 % 60:
name += f" {delta // 60 % 60}m" name += f" {delta // 60 % 60}m"
if delta % 60: if delta % 60:
name += f" {delta % 60}s" name += f" {delta % 60}s"
else:
kind = item.get('type')
else:
if item['industryId'] == 1:
amount = item['amount']
q = item['quality']
if 1 <= q <= 7:
self.food.update({f"q{q}": amount})
else: else:
if q == 10: kind = item_data.get('type')
self.eb_normal = amount
elif q == 11:
self.eb_double = amount
elif q == 13:
self.eb_small += amount
elif q == 14:
self.eb_small += amount
elif q == 15:
self.eb_small += amount
kind = re.sub(r'_q\d\d*', "", item.get('token'))
if item.get('token', "") == "house_q100":
self.ot_points = item['amount']
if kind not in final_items:
final_items[kind] = {}
if item['icon']:
icon = item['icon']
else:
if item['type'] == 'damageBoosters':
icon = "/images/modules/pvp/damage_boosters/damage_booster.png"
elif item['type'] == 'aircraftDamageBoosters':
icon = "/images/modules/pvp/damage_boosters/air_damage_booster.png"
elif item['type'] == 'prestigePointsBoosters':
icon = "/images/modules/pvp/prestige_points_boosters/prestige_booster.png"
elif item['type'] == 'speedBoosters':
icon = "/images/modules/pvp/speed_boosters/speed_booster.png"
elif item['type'] == 'catchupBoosters':
icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png"
else: else:
icon = "//www.erepublik.net/images/modules/manager/tab_storage.png" if item_data['industryId'] == 1:
data = dict(kind=kind, quality=item.get('quality', 0), amount=item.get('amount', 0), amount = item_data['amount']
durability=item.get('duration', 0), icon=icon, name=name) q = item_data['quality']
if item.get('type') in ('damageBoosters', "aircraftDamageBoosters"): if 1 <= q <= 7:
data = {data['durability']: data} self.food.update({f"q{q}": amount})
else: else:
if item.get('type') == 'bomb': if q == 10:
firepower = 0 self.eb_normal = amount
try: elif q == 11:
firepower = item.get('attributes').get('firePower').get('value', 0) self.eb_double = amount
except AttributeError: elif q == 13:
pass self.eb_small += amount
finally: elif q == 14:
data.update(fire_power=firepower) self.eb_small += amount
data = {data['quality']: data} elif q == 15:
final_items[kind].update(data) self.eb_small += amount
item_data.update(token='energy_bar')
kind = re.sub(r'_q\d\d*', "", item_data.get('token'))
raw_materials = {} if item_data.get('token', "") == "house_q100":
if j.get("inventoryItems", {}).get("rawMaterials", {}).get("items", {}): self.ot_points = item_data['amount']
for item in j.get("inventoryItems", {}).get("rawMaterials", {}).get("items", {}).values():
if item['isPartial']: if constants.INDUSTRIES[kind]:
kind = constants.INDUSTRIES[constants.INDUSTRIES[kind]]
if kind not in final_items:
final_items[kind] = {}
if item_data['icon']:
icon = item_data['icon']
else:
if item_data['type'] == 'damageBoosters':
icon = "/images/modules/pvp/damage_boosters/damage_booster.png"
elif item_data['type'] == 'aircraftDamageBoosters':
icon = "/images/modules/pvp/damage_boosters/air_damage_booster.png"
elif item_data['type'] == 'prestigePointsBoosters':
icon = "/images/modules/pvp/prestige_points_boosters/prestige_booster.png"
elif item_data['type'] == 'speedBoosters':
icon = "/images/modules/pvp/speed_boosters/speed_booster.png"
elif item_data['type'] == 'catchupBoosters':
icon = "/images/modules/pvp/ghost_boosters/icon_booster_30_60.png"
else:
icon = "//www.erepublik.net/images/modules/manager/tab_storage.png"
_item_data = dict(kind=kind, quality=item_data.get('quality', 0), amount=item_data.get('amount', 0),
durability=item_data.get('duration', 0), icon=icon, name=name)
if item_data.get('type') in ('damageBoosters', "aircraftDamageBoosters"):
_item_data = {_item_data['durability']: _item_data}
else:
if item_data.get('type') == 'bomb':
firepower = 0
try:
firepower = item_data.get('attributes').get('firePower').get('value', 0)
except AttributeError:
pass
finally:
_item_data.update(fire_power=firepower)
_item_data = {_item_data['quality']: _item_data}
final_items[kind].update(_item_data)
raw_materials: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
if data.get("rawMaterials", {}).get("items", {}):
for item_data in data.get("rawMaterials", {}).get("items", {}).values():
if item_data['isPartial']:
continue continue
kind = re.sub(r'_q\d\d*', "", item.get('token')) kind = constants.INDUSTRIES[item_data['industryId']]
if kind == "magnesium":
kind = "raw_aircraft"
elif kind == "sand":
kind = "raw_house"
if kind not in raw_materials: if kind not in raw_materials:
raw_materials[kind] = [] raw_materials[kind] = {}
if item['icon'].startswith('//www.erepublik.net/'): if item_data['icon'].startswith('//www.erepublik.net/'):
icon = item['icon'] icon = item_data['icon']
else: else:
icon = "//www.erepublik.net/" + item['icon'] icon = "//www.erepublik.net/" + item_data['icon']
raw_materials[kind].append( raw_materials[constants.INDUSTRIES[item_data.get('industryId')]].update({
dict(name=item.get("name"), amount=item['amount'] + (item.get('underCostruction', 0) / 100), 0: dict(name=item_data.get("name"),
icon=icon) amount=item_data['amount'] + (item_data.get('underCostruction', 0) / 100),
) icon=icon)
})
offers = {} offers: Dict[str, Dict[int, Dict[str, Union[str, int]]]] = {}
for offer in self._get_economy_my_market_offers().json(): for offer in self._get_economy_my_market_offers().json():
kind = constants.INDUSTRIES[offer['industryId']] kind = constants.INDUSTRIES[offer['industryId']]
data = dict(quality=offer.get('quality', 0), amount=offer.get('amount', 0), icon=offer.get('icon'), offer_data = dict(quality=offer.get('quality', 0), amount=offer.get('amount', 0), icon=offer.get('icon'),
kind=kind, name=kind) kind=kind, name=kind)
data = {data['quality']: data} offer_data = {offer_data['quality']: offer_data}
if kind not in offers: if kind not in offers:
offers[kind] = {} offers[kind] = {}
offers[kind].update(data) offers[kind].update(offer_data)
self.inventory.clear()
self.inventory.update({"used": j.get("inventoryStatus").get("usedStorage"), self.inventory.update(active=active_items, final=final_items, raw=raw_materials, offers=offers)
"total": j.get("inventoryStatus").get("totalStorage")})
inventory = dict(items=dict(active=active_items, final=final_items,
raw=raw_materials, offers=offers), status=self.inventory)
self.food["total"] = sum([self.food[q] * constants.FOOD_ENERGY[q] for q in constants.FOOD_ENERGY]) self.food["total"] = sum([self.food[q] * constants.FOOD_ENERGY[q] for q in constants.FOOD_ENERGY])
return inventory
def write_log(self, *args, **kwargs): def write_log(self, *args, **kwargs):
if self.config.interactive: if self.config.interactive:
@ -403,6 +427,43 @@ class BaseCitizen(access_points.CitizenAPI):
return_set.add(constants.COUNTRIES[country_data['id']]) return_set.add(constants.COUNTRIES[country_data['id']])
return return_set return return_set
def dump_instance(self):
filename = f"{self.__class__.__name__}__dump.json"
with open(filename, 'w') as f:
utils.json.dump(dict(config=self.config, cookies=self._req.cookies.get_dict(),
user_agent=self._req.headers.get("User-Agent")), f, cls=classes.MyJSONEncoder)
self.write_log(f"Session saved to: '{filename}'")
@classmethod
def load_from_dump(cls, dump_name: str):
with open(dump_name) as f:
data = utils.json.load(f)
player = cls(data['config']['email'], "")
player._req.cookies.update(data['cookies'])
player._req.headers.update({"User-Agent": data['user_agent']})
for k, v in data.get('config', {}).items():
if hasattr(player.config, k):
setattr(player.config, k, v)
player._resume_session()
return player
def _resume_session(self):
resp = self._req.get(self.url)
re_name_id = re.search(r'<a data-fblog="profile_avatar" href="/en/citizen/profile/(\d+)" '
r'class="user_avatar" title="(.*?)">', resp.text)
if re_name_id:
self.name = re_name_id.group(2)
self.details.citizen_id = re_name_id.group(1)
self.write_log(f"Resumed as: {self.name}")
if re.search('<div id="accountSecurity" class="it-hurts-when-ip">', resp.text):
self.restricted_ip = True
self.report_error("eRepublik has blacklisted IP. Limited functionality!", True)
self.logged_in = True
self.get_csrf_token()
else:
self._login()
def __str__(self) -> str: def __str__(self) -> str:
return f"Citizen {self.name}" return f"Citizen {self.name}"
@ -628,9 +689,15 @@ class BaseCitizen(access_points.CitizenAPI):
if re.search(r'Occasionally there are a couple of things which we need to check or to implement in order make ' if re.search(r'Occasionally there are a couple of things which we need to check or to implement in order make '
r'your experience in eRepublik more pleasant. <strong>Don\'t worry about ongoing battles, timer ' r'your experience in eRepublik more pleasant. <strong>Don\'t worry about ongoing battles, timer '
r'will be stopped during maintenance.</strong>', response.text): r'will be stopped during maintenance.</strong>', response.text):
self.write_log("eRepublik ss having maintenance. Sleeping for 5 minutes") self.write_log("eRepublik is having maintenance. Sleeping for 5 minutes")
self.sleep(5 * 60) self.sleep(5 * 60)
return True return True
if re.search('We are experiencing some tehnical dificulties', response.text):
self.write_log("eRepublik is having technical difficulties. Sleeping for 5 minutes")
self.sleep(5 * 60)
return True
return bool(re.search(r'body id="error"|Internal Server Error|' return bool(re.search(r'body id="error"|Internal Server Error|'
r'CSRF attack detected|meta http-equiv="refresh"|' r'CSRF attack detected|meta http-equiv="refresh"|'
r'not_authenticated', response.text)) r'not_authenticated', response.text))
@ -683,9 +750,7 @@ class CitizenAnniversary(BaseCitizen):
_write_spin_data(current_cost, r.get('account'), _write_spin_data(current_cost, r.get('account'),
base.get('prizes').get('prizes').get(str(r.get('result'))).get('tooltip')) base.get('prizes').get('prizes').get(str(r.get('result'))).get('tooltip'))
else: else:
is_cost: Callable[[], bool] = lambda: (max_cost != current_cost if max_cost else True) while max_cost >= current_cost if max_cost else spin_count >= current_count if spin_count else False:
is_count: Callable[[], bool] = lambda: (spin_count != current_count if spin_count else True)
while is_cost() or is_count():
r = self._spin_wheel_of_loosing(current_cost) r = self._spin_wheel_of_loosing(current_cost)
current_count += 1 current_count += 1
current_cost = r.get('cost') current_cost = r.get('cost')
@ -694,8 +759,8 @@ class CitizenAnniversary(BaseCitizen):
def _spin_wheel_of_loosing(self, current_cost: int) -> Dict[str, Any]: def _spin_wheel_of_loosing(self, current_cost: int) -> Dict[str, Any]:
r = self._post_main_wheel_of_fortune_spin(current_cost).json() r = self._post_main_wheel_of_fortune_spin(current_cost).json()
self.details.cc = r.get('account') self.details.cc = float(Decimal(r.get('account')))
return r.get('result') return r
class CitizenTravel(BaseCitizen): class CitizenTravel(BaseCitizen):
@ -845,15 +910,19 @@ class CitizenCompanies(BaseCitizen):
if self.restricted_ip: if self.restricted_ip:
return None return None
self.update_companies() self.update_companies()
self.update_inventory()
data = {"action_type": "production"} data = {"action_type": "production"}
extra = {} extra = {}
raw_factories = wam_holding.get_wam_companies(raw_factory=True) raw_factories = wam_holding.get_wam_companies(raw_factory=True)
fin_factories = wam_holding.get_wam_companies(raw_factory=False) fin_factories = wam_holding.get_wam_companies(raw_factory=False)
free_inventory = self.inventory["total"] - self.inventory["used"] free_inventory = self.inventory_status["total"] - self.inventory_status["used"]
wam_list = raw_factories + fin_factories wam_list = raw_factories + fin_factories
wam_list = wam_list[:self.energy.food_fights] wam_list = wam_list[:self.energy.food_fights]
if int(free_inventory * 0.75) < self.my_companies.get_needed_inventory_usage(wam_list):
self.update_inventory()
free_inventory = self.inventory_status["total"] - self.inventory_status["used"]
while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list): while wam_list and free_inventory < self.my_companies.get_needed_inventory_usage(wam_list):
wam_list.pop(-1) wam_list.pop(-1)
@ -882,13 +951,12 @@ class CitizenCompanies(BaseCitizen):
self.my_companies.prepare_holdings(utils.json.loads(have_holdings.group(1))) self.my_companies.prepare_holdings(utils.json.loads(have_holdings.group(1)))
self.my_companies.prepare_companies(utils.json.loads(have_companies.group(1))) self.my_companies.prepare_companies(utils.json.loads(have_companies.group(1)))
self.reporter.report_action('COMPANIES', json_val=self.my_companies.as_dict)
def assign_company_to_holding(self, company: classes.Company, holding: classes.Holding) -> Response: def assign_company_to_holding(self, company: classes.Company, holding: classes.Holding) -> Response:
""" """
Assigns factory to new holding Assigns factory to new holding
""" """
self.write_log(f"{company} moved to {holding}") self.write_log(f"{company} moved to {holding}")
company._holding = weakref.ref(holding)
return self._post_economy_assign_to_holding(company.id, holding.id) return self._post_economy_assign_to_holding(company.id, holding.id)
def create_factory(self, industry_id: int, building_type: int = 1) -> Response: def create_factory(self, industry_id: int, building_type: int = 1) -> Response:
@ -921,27 +989,29 @@ class CitizenEconomy(CitizenTravel):
def check_house_durability(self) -> Dict[int, datetime]: def check_house_durability(self) -> Dict[int, datetime]:
ret = {} ret = {}
inv = self.update_inventory() inv = self.get_inventory()
for house_quality, active_house in inv['items']['active'].get('house', {}).items(): for house_quality, active_house in inv['active'].get('House', {}).items():
till = utils.good_timedelta(self.now, timedelta(seconds=active_house['time_left'])) till = utils.good_timedelta(self.now, timedelta(seconds=active_house['time_left']))
ret.update({house_quality: till}) ret.update({house_quality: till})
return ret return ret
def buy_and_activate_house(self, q: int) -> Dict[int, datetime]: def buy_and_activate_house(self, q: int) -> Dict[int, datetime]:
inventory = self.update_inventory()
original_region = self.details.current_country, self.details.current_region original_region = self.details.current_country, self.details.current_region
ok_to_activate = False ok_to_activate = False
if not inventory['items']['final'].get('house', {}).get(q, {}): inv = self.get_inventory()
if not inv['final'].get('House', {}).get(q, {}):
countries = [self.details.citizenship, ] countries = [self.details.citizenship, ]
if self.details.current_country != self.details.citizenship: if self.details.current_country != self.details.citizenship:
countries.append(self.details.current_country) countries.append(self.details.current_country)
offers = [self.get_market_offers("house", q, country)[f"q{q}"] for country in countries] offers = [self.get_market_offers("House", q, country)[f"q{q}"] for country in countries]
local_cheapest = sorted(offers, key=lambda o: o.price)[0] local_cheapest = sorted(offers, key=lambda o: o.price)[0]
global_cheapest = self.get_market_offers("house", q)[f"q{q}"] global_cheapest = self.get_market_offers("House", q)[f"q{q}"]
if global_cheapest.price + 200 < local_cheapest.price: if global_cheapest.price + 200 < local_cheapest.price:
self._travel(global_cheapest.country) if self.travel_to_country(global_cheapest.country):
buy = self.buy_from_market(global_cheapest.offer_id, 1) buy = self.buy_from_market(global_cheapest.offer_id, 1)
else:
buy = {'error': True, 'message': 'Unable to travel!'}
else: else:
buy = self.buy_from_market(local_cheapest.offer_id, 1) buy = self.buy_from_market(local_cheapest.offer_id, 1)
if buy["error"]: if buy["error"]:
@ -969,13 +1039,19 @@ class CitizenEconomy(CitizenTravel):
house_durability = self.buy_and_activate_house(q) house_durability = self.buy_and_activate_house(q)
return house_durability return house_durability
def activate_house(self, quality: int) -> datetime: def activate_house(self, quality: int) -> bool:
active_until = self.now r: Dict[str, Any] = self._post_economy_activate_house(quality).json()
r = self._post_economy_activate_house(quality) self._update_inventory_data(r)
if r.json().get("status") and not r.json().get("error"): if r.get("status") and not r.get("error"):
house = r.json()["inventoryItems"]["activeEnhancements"]["items"]["4_%i_active" % quality] house: Dict[str, Union[int, str]] = self.get_inventory()['active']['House'][quality]
active_until = utils.good_timedelta(active_until, timedelta(seconds=house["active"]["time_left"])) time_left = timedelta(seconds=house["time_left"])
return active_until active_until = utils.good_timedelta(self.now, time_left)
self._report_action(
'ACTIVATE_HOUSE',
f"Activated {house['name']}. Expires at {active_until.strftime('%F %T')} (after {time_left})"
)
return True
return False
def get_game_token_offers(self): def get_game_token_offers(self):
r = self._post_economy_game_tokens_market('retrieve').json() r = self._post_economy_game_tokens_market('retrieve').json()
@ -1019,10 +1095,43 @@ class CitizenEconomy(CitizenTravel):
ret.append(line) ret.append(line)
return ret return ret
def post_market_offer(self, industry: int, quality: int, amount: int, price: float) -> Response: def delete_my_market_offer(self, offer_id: int) -> bool:
offers = self.get_my_market_offers()
for offer in offers:
if offer['id'] == offer_id:
industry = constants.INDUSTRIES[offer['industryId']]
amount = offer['amount']
q = offer['quality']
price = offer['price']
ret = self._post_economy_marketplace_actions('delete', offer_id=offer_id).json()
if ret.get('error'):
self._report_action("ECONOMY_DELETE_OFFER", f"Unable to delete offer: '{ret.get('message')}'",
kwargs=offer)
else:
self._report_action("ECONOMY_DELETE_OFFER",
f"Removed offer for {amount} x {industry} q{q} for {price}cc/each",
kwargs=offer)
return not ret.get('error')
else:
self._report_action("ECONOMY_DELETE_OFFER", f"Unable to find offer id{offer_id}", kwargs={'offers': offers})
return False
def post_market_offer(self, industry: int, quality: int, amount: int, price: float) -> bool:
if not constants.INDUSTRIES[industry]: if not constants.INDUSTRIES[industry]:
self.write_log(f"Trying to sell unsupported industry {industry}") self.write_log(f"Trying to sell unsupported industry {industry}")
_inv_qlt = quality if industry in [1, 2, 3, 4, 23] else 0
_kind = 'final' if industry in [1, 2, 4, 4, 23] else 'raw'
inventory = self.get_inventory()
items = inventory[_kind].get(constants.INDUSTRIES[industry], {_inv_qlt: {'amount': 0}})
if items[_inv_qlt]['amount'] < amount:
inventory = self.get_inventory(True)
items = inventory[_kind].get(constants.INDUSTRIES[industry], {_inv_qlt: {'amount': 0}})
if items[_inv_qlt]['amount'] < amount:
self._report_action("ECONOMY_SELL_PRODUCTS", "Unable to sell! Not enough items in storage!",
kwargs=dict(inventory=items[_inv_qlt], amount=amount))
return False
data = { data = {
"country_id": self.details.citizenship.id, "country_id": self.details.citizenship.id,
"industry": industry, "industry": industry,
@ -1031,29 +1140,46 @@ class CitizenEconomy(CitizenTravel):
"price": price, "price": price,
"buy": False, "buy": False,
} }
ret = self._post_economy_marketplace_actions(**data) ret = self._post_economy_marketplace_actions('sell', **data).json()
message = (f"Posted market offer for {amount}q{quality} " message = (f"Posted market offer for {amount}q{quality} "
f"{constants.INDUSTRIES[industry]} for price {price}cc") f"{constants.INDUSTRIES[industry]} for price {price}cc")
self._report_action("ECONOMY_SELL_PRODUCTS", message, kwargs=ret.json()) self._report_action("ECONOMY_SELL_PRODUCTS", message, kwargs=ret)
return ret return not bool(ret.get('error', True))
def buy_from_market(self, offer: int, amount: int) -> dict: def buy_from_market(self, offer: int, amount: int) -> dict:
ret = self._post_economy_marketplace_actions(amount, True, offer=offer) ret = self._post_economy_marketplace_actions('buy', offer=offer, amount=amount)
json_ret = ret.json() json_ret = ret.json()
if json_ret.get('error'): if not json_ret.get('error', True):
return json_ret
else:
self.details.cc = ret.json()['currency'] self.details.cc = ret.json()['currency']
self.details.gold = ret.json()['gold'] self.details.gold = ret.json()['gold']
json_ret.pop("offerUpdate", None) json_ret.pop("offerUpdate", None)
self._report_action("BOUGHT_PRODUCTS", "", kwargs=json_ret) self._report_action("BOUGHT_PRODUCTS", json_ret.get('message'), kwargs=json_ret)
return json_ret
def buy_market_offer(self, offer: OfferItem, amount: int = None) -> dict:
if amount is None or amount > offer.amount:
amount = offer.amount
traveled = False
if not self.details.current_country == offer.country:
traveled = True
self.travel_to_country(offer.country)
ret = self._post_economy_marketplace_actions('buy', offer=offer.offer_id, amount=amount)
json_ret = ret.json()
if not json_ret.get('error', True):
self.details.cc = ret.json()['currency']
self.details.gold = ret.json()['gold']
json_ret.pop("offerUpdate", None)
self._report_action("BOUGHT_PRODUCTS", json_ret.get('message'), kwargs=json_ret)
if traveled:
self.travel_to_residence()
return json_ret return json_ret
def get_market_offers( def get_market_offers(
self, product_name: str, quality: int = None, country: constants.Country = None self, product_name: str, quality: int = None, country: constants.Country = None
) -> Dict[str, classes.OfferItem]: ) -> Dict[str, classes.OfferItem]:
raw_short_names = dict(frm="foodRaw", wrm="weaponRaw", hrm="houseRaw", arm="airplaneRaw") raw_short_names = dict(frm="foodRaw", wrm="weaponRaw", hrm="houseRaw", arm="airplaneRaw")
q1_industries = ["aircraft"] + list(raw_short_names.values()) q1_industries = list(raw_short_names.values())
q5_industries = ['house', 'aircraft', 'ticket']
if product_name in raw_short_names: if product_name in raw_short_names:
quality = 1 quality = 1
product_name = raw_short_names[product_name] product_name = raw_short_names[product_name]
@ -1067,7 +1193,7 @@ class CitizenEconomy(CitizenTravel):
if quality: if quality:
offers[f"q{quality}"] = classes.OfferItem() offers[f"q{quality}"] = classes.OfferItem()
else: else:
max_quality = 1 if product_name in q1_industries else 5 if product_name == 'house' else 7 max_quality = 1 if product_name in q1_industries else 5 if product_name.lower() in q5_industries else 7
for q in range(max_quality): for q in range(max_quality):
offers[f"q{q + 1}"] = classes.OfferItem() offers[f"q{q + 1}"] = classes.OfferItem()
@ -1345,10 +1471,10 @@ class CitizenMilitary(CitizenTravel):
all_battles = {} all_battles = {}
for battle_data in r_json.get("battles", {}).values(): for battle_data in r_json.get("battles", {}).values():
all_battles[battle_data.get('id')] = classes.Battle(battle_data) all_battles[battle_data.get('id')] = classes.Battle(battle_data)
old_all_battles = self.all_battles # old_all_battles = self.all_battles
self.all_battles = all_battles self.all_battles = all_battles
for battle in old_all_battles.values(): # for battle in old_all_battles.values():
utils._clear_up_battle_memory(battle) # utils._clear_up_battle_memory(battle)
def get_battle_for_war(self, war_id: int) -> Optional[classes.Battle]: def get_battle_for_war(self, war_id: int) -> Optional[classes.Battle]:
self.update_war_info() self.update_war_info()
@ -1389,6 +1515,7 @@ class CitizenMilitary(CitizenTravel):
try: try:
if weapon['weaponQuantity'] > 30 and weapon['weaponInfluence'] > weapon_damage: if weapon['weaponQuantity'] > 30 and weapon['weaponInfluence'] > weapon_damage:
weapon_quality = int(weapon['weaponId']) weapon_quality = int(weapon['weaponId'])
weapon_damage = weapon['weaponInfluence']
except ValueError: except ValueError:
pass pass
return self.change_weapon(battle, weapon_quality, division) return self.change_weapon(battle, weapon_quality, division)
@ -1400,51 +1527,6 @@ class CitizenMilitary(CitizenTravel):
f" new influence {influence}", kwargs=r.json()) f" new influence {influence}", kwargs=r.json())
return influence return influence
# def check_epic_battles(self):
# active_fs = False
# for battle_id in self.sorted_battles(self.config.sort_battles_time):
# battle = self.all_battles.get(battle_id)
# if not battle.is_air:
# my_div: BattleDivision = battle.div.get(self.division)
# if my_div.epic and my_div.end > self.now:
# if self.energy.food_fights > 50:
# inv_allies = battle.invader.deployed + [battle.invader.id]
# def_allies = battle.defender.deployed + [battle.defender.id]
# all_allies = inv_allies + def_allies
# if self.details.current_country not in all_allies:
# if self.details.current_country in battle.invader.allies:
# allies = battle.invader.deployed
# side = battle.invader.id
# else:
# allies = battle.defender.deployed
# side = battle.defender.id
#
# self.travel_to_battle(battle.id, allies)
#
# else:
# if self.details.current_country in inv_allies:
# side = battle.invader.id
# elif self.details.current_country in def_allies:
# side = battle.defender.id
# else:
# self.write_log(
# f"Country {self.details.current_country} not in all allies list ({all_allies}) and "
# f"also not in inv allies ({inv_allies}) nor def allies ({def_allies})")
# break
# error_count = 0
# while self.energy.food_fights > 5 and error_count < 20:
# errors = self.fight(battle_id, side_id=side, count=self.energy.food_fights - 5)
# if errors:
# error_count += errors
# if self.config.epic_hunt_ebs:
# self._eat('orange')
# self.travel_to_residence()
# break
# elif bool(my_div.epic):
# active_fs = True
#
# self.active_fs = active_fs
def sorted_battles(self, sort_by_time: bool = True, only_tp=False) -> List[classes.Battle]: def sorted_battles(self, sort_by_time: bool = True, only_tp=False) -> List[classes.Battle]:
cs_battles_priority_air: List[classes.Battle] = [] cs_battles_priority_air: List[classes.Battle] = []
cs_battles_priority_ground: List[classes.Battle] = [] cs_battles_priority_ground: List[classes.Battle] = []
@ -1687,14 +1769,17 @@ class CitizenMilitary(CitizenTravel):
damage = 0 damage = 0
err = False err = False
if r_json.get("error"): if r_json.get("error"):
if r_json.get("message") == "SHOOT_LOCKOUT" or r_json.get("message") == "ZONE_INACTIVE": if r_json.get("message") == "SHOOT_LOCKOUT":
pass pass
elif r_json.get("message") == "NOT_ENOUGH_WEAPONS": elif r_json.get("message") == "NOT_ENOUGH_WEAPONS":
self.set_default_weapon(battle, division) self.set_default_weapon(battle, division)
elif r_json.get("message") == "Cannot activate a zone with a non-native division": elif r_json.get("message") == "Cannot activate a zone with a non-native division":
self.write_log("Wrong division!!") self.write_log("Wrong division!!")
return 0, 10, 0 return 0, 10, 0
elif r_json.get("message") == "FIGHT_DISABLED": elif r_json.get("message") == "ZONE_INACTIVE":
self.write_log("Wrong division!!")
return 0, 10, 0
elif r_json.get("message") in ["FIGHT_DISABLED", "DEPLOYMENT_MODE"]:
self._post_main_profile_update('options', self._post_main_profile_update('options',
params='{"optionName":"enable_web_deploy","optionValue":"off"}') params='{"optionName":"enable_web_deploy","optionValue":"off"}')
self.set_default_weapon(battle, division) self.set_default_weapon(battle, division)
@ -1816,12 +1901,12 @@ class CitizenMilitary(CitizenTravel):
self._report_action("MILITARY_BOOSTER", f"Activated 50% {duration / 60}h damage booster") self._report_action("MILITARY_BOOSTER", f"Activated 50% {duration / 60}h damage booster")
self._post_economy_activate_booster(5, duration, "damage") self._post_economy_activate_booster(5, duration, "damage")
def get_active_ground_damage_booster(self): def get_active_ground_damage_booster(self) -> int:
inventory = self.update_inventory() inventory = self.get_inventory()
quality = 0 quality = 0
if inventory['items']['active'].get('damageBoosters', {}).get(10): if inventory['active'].get('damageBoosters', {}).get(10):
quality = 100 quality = 100
elif inventory['items']['active'].get('damageBoosters', {}).get(5): elif inventory['active'].get('damageBoosters', {}).get(5):
quality = 50 quality = 50
return quality return quality
@ -1999,15 +2084,16 @@ class CitizenPolitics(BaseCitizen):
self._report_action('POLITIC_PARTY_PRESIDENT', 'Applied for party president elections') self._report_action('POLITIC_PARTY_PRESIDENT', 'Applied for party president elections')
return self._get_candidate_party(self.politics.party_slug) return self._get_candidate_party(self.politics.party_slug)
def get_country_president_election_result(self, country: constants.Country, year: int, month: int) -> Dict[str, int]: def get_country_president_election_result(
self, country: constants.Country, year: int, month: int
) -> Dict[str, int]:
timestamp = int(constants.erep_tz.localize(datetime(year, month, 5)).timestamp()) timestamp = int(constants.erep_tz.localize(datetime(year, month, 5)).timestamp())
resp = self._get_presidential_elections(country.id, timestamp) resp = self._get_presidential_elections(country.id, timestamp)
candidates = re.findall(r'class="candidate_info">(.*?)</li>', resp.text, re.S | re.M) candidates = re.findall(r'class="candidate_info">(.*?)</li>', resp.text, re.S | re.M)
ret = {} ret = {}
for candidate in candidates: for candidate in candidates:
name = re.search( name = re.search(r'<a hovercard=1 class="candidate_name" href="//www.erepublik.com/en/citizen/profile/\d+"'
r'<a hovercard=1 class="candidate_name" href="//www.erepublik.com/en/citizen/profile/\d+" title="(.*)">', r' title="(.*)">', candidate)
candidate)
name = name.group(1) name = name.group(1)
votes = re.search(r'<span class="votes">(\d+) votes</span>', candidate).group(1) votes = re.search(r'<span class="votes">(\d+) votes</span>', candidate).group(1)
ret.update({name: int(votes)}) ret.update({name: int(votes)})
@ -2015,17 +2101,7 @@ class CitizenPolitics(BaseCitizen):
class CitizenSocial(BaseCitizen): class CitizenSocial(BaseCitizen):
def send_mail_to_owner(self): def send_mail(self, subject: str, msg: str, ids: List[int]):
if not self.details.citizen_id == 1620414:
self.send_mail('Started', f'time {self.now.strftime("%Y-%m-%d %H-%M-%S")}', [1620414, ])
self.sleep(1)
msg_id = re.search(r'<input type="hidden" value="(\d+)" '
r'id="delete_message_(\d+)" name="delete_message\[]">', self.r.text).group(1)
self._post_delete_message([msg_id])
def send_mail(self, subject: str, msg: str, ids: List[int] = None):
if ids is None:
ids = [1620414, ]
for player_id in ids: for player_id in ids:
self._report_action('SOCIAL_MESSAGE', f'Sent a message to {player_id}', self._report_action('SOCIAL_MESSAGE', f'Sent a message to {player_id}',
kwargs=dict(subject=subject, msg=msg, id=player_id)) kwargs=dict(subject=subject, msg=msg, id=player_id))
@ -2080,20 +2156,23 @@ class CitizenSocial(BaseCitizen):
def get_report_notifications(self, page: int = 1) -> List[Dict[str, Any]]: def get_report_notifications(self, page: int = 1) -> List[Dict[str, Any]]:
return self._get_main_notifications_ajax_report(page).json().get('alertsList', []) return self._get_main_notifications_ajax_report(page).json().get('alertsList', [])
def delete_community_notification(self, notification_id: Union[int, List[int]]): def delete_community_notification(self, *notification_ids: int):
if not isinstance(notification_id, list): ids = []
notification_id = [notification_id] for _id in sorted(notification_ids):
self._post_main_notifications_ajax_community(notification_id) ids.append(int(_id))
self._post_main_notifications_ajax_community(ids)
def delete_system_notification(self, notification_id: Union[int, List[int]]): def delete_system_notification(self, *notification_ids: int):
if not isinstance(notification_id, list): ids = []
notification_id = [notification_id] for _id in sorted(notification_ids):
self._post_main_notifications_ajax_system(notification_id) ids.append(int(_id))
self._post_main_notifications_ajax_system(ids)
def delete_report_notification(self, notification_id: Union[int, List[int]]): def delete_report_notification(self, *notification_ids: int):
if not isinstance(notification_id, list): ids = []
notification_id = [notification_id] for _id in sorted(notification_ids):
self._post_main_notifications_ajax_report(notification_id) ids.append(int(_id))
self._post_main_notifications_ajax_report(ids)
def get_all_notifications(self, page: int = 1) -> Dict[str, List[Dict[str, Any]]]: def get_all_notifications(self, page: int = 1) -> Dict[str, List[Dict[str, Any]]]:
return dict(community=self.get_community_notifications(), return dict(community=self.get_community_notifications(),
@ -2104,11 +2183,11 @@ class CitizenSocial(BaseCitizen):
for kind, notifications in self.get_all_notifications(): for kind, notifications in self.get_all_notifications():
if notifications: if notifications:
if kind == "community": if kind == "community":
self.delete_community_notification([n['id'] for n in notifications]) self.delete_community_notification(*[n['id'] for n in notifications])
elif kind == "report": elif kind == "report":
self.delete_report_notification([n['id'] for n in notifications]) self.delete_report_notification(*[n['id'] for n in notifications])
elif kind == "system": elif kind == "system":
self.delete_system_notification([n['id'] for n in notifications]) self.delete_system_notification(*[n['id'] for n in notifications])
else: else:
self.report_error(f"Unsupported notification kind: \"{kind}\"!") self.report_error(f"Unsupported notification kind: \"{kind}\"!")
@ -2142,7 +2221,8 @@ class CitizenTasks(BaseCitizen):
js = response.json() js = response.json()
good_msg = ["already_worked", "captcha"] good_msg = ["already_worked", "captcha"]
if not js.get("status") and not js.get("message") in good_msg: if not js.get("status") and not js.get("message") in good_msg:
if js.get('message') == 'employee': if js.get('message') in ['employee', 'money']:
self.resign_from_employer()
self.find_new_job() self.find_new_job()
self.update_citizen_info() self.update_citizen_info()
self.work() self.work()
@ -2181,10 +2261,9 @@ class CitizenTasks(BaseCitizen):
self._eat("blue") self._eat("blue")
if self.energy.food_fights < len(tgs): if self.energy.food_fights < len(tgs):
large = max(self.energy.reference_time, self.now) large = max(self.energy.reference_time, self.now)
small = min(self.energy.reference_time, self.now) sleep_seconds = utils.get_sleep_seconds(large)
self.write_log("I don't have energy to train. Will sleep for {} seconds".format( self.write_log(f"I don't have energy to train. Will sleep for {sleep_seconds} seconds")
(large - small).seconds)) self.sleep(sleep_seconds)
self.sleep(int((large - small).total_seconds()))
self._eat("blue") self._eat("blue")
self.train() self.train()
@ -2204,14 +2283,14 @@ class CitizenTasks(BaseCitizen):
self._eat("blue") self._eat("blue")
if self.energy.food_fights < 1: if self.energy.food_fights < 1:
large = max(self.energy.reference_time, self.now) large = max(self.energy.reference_time, self.now)
small = min(self.energy.reference_time, self.now) sleep_seconds = utils.get_sleep_seconds(large)
self.write_log("I don't have energy to work OT. Will sleep for {}s".format((large - small).seconds)) self.write_log(f"I don't have energy to work OT. Will sleep for {sleep_seconds}s")
self.sleep(int((large - small).total_seconds())) self.sleep(sleep_seconds)
self._eat("blue") self._eat("blue")
self.work_ot() self.work_ot()
def resign_from_employer(self) -> bool: def resign_from_employer(self) -> bool:
r = self.update_job_info() r = self._get_main_job_data()
if r.json().get("isEmployee"): if r.json().get("isEmployee"):
self._report_action('ECONOMY_RESIGN', 'Resigned from employer!', kwargs=r.json()) self._report_action('ECONOMY_RESIGN', 'Resigned from employer!', kwargs=r.json())
self._post_economy_resign() self._post_economy_resign()
@ -2253,20 +2332,26 @@ class CitizenTasks(BaseCitizen):
if ot: if ot:
self.next_ot_time = utils.localize_timestamp(int(ot.get("nextOverTime", 0))) self.next_ot_time = utils.localize_timestamp(int(ot.get("nextOverTime", 0)))
self.ot_points = ot.get("points", 0) self.ot_points = ot.get("points", 0)
return resp
class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeaderBoard, class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeaderBoard,
CitizenMedia, CitizenMilitary, CitizenPolitics, CitizenSocial, CitizenTasks): CitizenMedia, CitizenMilitary, CitizenPolitics, CitizenSocial, CitizenTasks):
debug: bool = False debug: bool = False
def __init__(self, email: str = "", password: str = "", auto_login: bool = True): def __init__(self, email: str = "", password: str = "", auto_login: bool = False):
super().__init__(email, password) super().__init__(email, password)
self._last_full_update = utils.good_timedelta(self.now, - timedelta(minutes=5)) self._last_full_update = constants.min_datetime
self.set_debug(True) self.set_debug(True)
if auto_login: if auto_login:
self.login() self.login()
@classmethod
def load_from_dump(cls, dump_name: str = ""):
filename = dump_name if dump_name else f"{cls.__name__}__dump.json"
player: Citizen = super().load_from_dump(filename) # noqa
player.login()
return player
def config_setup(self, **kwargs): def config_setup(self, **kwargs):
self.config.reset() self.config.reset()
for key, value in kwargs.items(): for key, value in kwargs.items():
@ -2287,7 +2372,6 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
"" if self.config.telegram_chat_id or self.config.telegram_token else self.name) "" if self.config.telegram_chat_id or self.config.telegram_token else self.name)
self.telegram.send_message(f"*Started* {utils.now():%F %T}") self.telegram.send_message(f"*Started* {utils.now():%F %T}")
self._last_full_update = utils.good_timedelta(self.now, - timedelta(minutes=5))
self.update_all(True) self.update_all(True)
def update_citizen_info(self, html: str = None): def update_citizen_info(self, html: str = None):
@ -2357,23 +2441,21 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
self.debug = bool(debug) self.debug = bool(debug)
self._req.debug = bool(debug) self._req.debug = bool(debug)
def set_pin(self, pin: int): def set_pin(self, pin: str):
self.details.pin = int(pin) self.details.pin = str(pin[:4])
def update_all(self, force_update=False): def update_all(self, force_update=False):
# Do full update max every 5 min # Do full update max every 5 min
if utils.good_timedelta(self._last_full_update, timedelta(minutes=5)) > self.now and not force_update: if utils.good_timedelta(self._last_full_update, timedelta(minutes=5)) < self.now or force_update:
return
else:
self._last_full_update = self.now self._last_full_update = self.now
self.update_citizen_info() self.update_citizen_info()
self.update_war_info() self.update_war_info()
self.update_inventory() self.update_inventory()
self.update_companies() self.update_companies()
self.update_money() self.update_money()
self.update_weekly_challenge() self.update_weekly_challenge()
self.send_state_update() self.send_state_update()
self.check_for_notification_medals() self.check_for_notification_medals()
def update_weekly_challenge(self): def update_weekly_challenge(self):
data = self._get_main_weekly_challenge_data().json() data = self._get_main_weekly_challenge_data().json()
@ -2415,6 +2497,7 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
return count, log_msg, force_fight return count, log_msg, force_fight
def collect_weekly_reward(self): def collect_weekly_reward(self):
utils.deprecation(f"Logic moved to {self.__class__.__name__}.update_weekly_challenge()!")
self.update_weekly_challenge() self.update_weekly_challenge()
def collect_daily_task(self): def collect_daily_task(self):
@ -2434,6 +2517,7 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
start_time = utils.good_timedelta(start_time, timedelta(minutes=30)) start_time = utils.good_timedelta(start_time, timedelta(minutes=30))
self.send_state_update() self.send_state_update()
self.send_inventory_update() self.send_inventory_update()
self.reporter.report_action('COMPANIES', json_val=self.my_companies.as_dict)
sleep_seconds = (start_time - self.now).total_seconds() sleep_seconds = (start_time - self.now).total_seconds()
self.stop_threads.wait(sleep_seconds if sleep_seconds > 0 else 0) self.stop_threads.wait(sleep_seconds if sleep_seconds > 0 else 0)
except: # noqa except: # noqa
@ -2441,13 +2525,13 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
def send_state_update(self): def send_state_update(self):
data = dict(xp=self.details.xp, cc=self.details.cc, gold=self.details.gold, pp=self.details.pp, data = dict(xp=self.details.xp, cc=self.details.cc, gold=self.details.gold, pp=self.details.pp,
inv_total=self.inventory['total'], inv=self.inventory['used'], hp_limit=self.energy.limit, inv_total=self.inventory_status['total'], inv=self.inventory_status['used'],
hp_limit=self.energy.limit,
hp_interval=self.energy.interval, hp_available=self.energy.available, food=self.food['total'], ) hp_interval=self.energy.interval, hp_available=self.energy.available, food=self.food['total'], )
self.reporter.send_state_update(**data) self.reporter.send_state_update(**data)
def send_inventory_update(self): def send_inventory_update(self):
to_report = self.update_inventory() self.reporter.report_action("INVENTORY", json_val=self.get_inventory(True))
self.reporter.report_action("INVENTORY", json_val=to_report)
def eat(self): def eat(self):
""" """
@ -2513,7 +2597,11 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
if raw_kind: if raw_kind:
raw_kind = raw_kind.group(1) raw_kind = raw_kind.group(1)
result = response.get("result", {}) result = response.get("result", {})
amount_needed = round(result.get("consume", 0) - result.get("stock", 0) + 0.49) amount_needed = round(result.get("consume", 0) - result.get("stock", 0) + 0.5)
self._report_action(
'WORK_AS_MANAGER', f"Unable to wam! Missing {amount_needed} {raw_kind}, will try to buy.",
kwargs=response
)
start_place = (self.details.current_country, self.details.current_region) start_place = (self.details.current_country, self.details.current_region)
while amount_needed > 0: while amount_needed > 0:
amount = amount_needed amount = amount_needed
@ -2522,20 +2610,31 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
if not best_offer.country == self.details.current_country: if not best_offer.country == self.details.current_country:
self.travel_to_country(best_offer.country) self.travel_to_country(best_offer.country)
self._report_action("ECONOMY_BUY", f"Attempting to buy {amount} {raw_kind} for {best_offer.price*amount}cc")
rj = self.buy_from_market(amount=amount, offer=best_offer.offer_id) rj = self.buy_from_market(amount=amount, offer=best_offer.offer_id)
if not rj.get('error'): if not rj.get('error'):
amount_needed -= amount amount_needed -= amount
else: else:
self.write_log(rj.get('message', "")) self.write_log(rj.get('message', ""))
self._report_action(
"ECONOMY_BUY", f"Unable to buy products! Reason: {rj.get('message')}", kwargs=rj
)
break break
else: else:
if not start_place == (self.details.current_country, self.details.current_region): if not start_place == (self.details.current_country, self.details.current_region):
self.travel_to_holding(holding) self.travel_to_holding(holding)
self._wam(holding) self._wam(holding)
return return
if not start_place == (self.details.current_country, self.details.current_region):
self.travel_to_residence()
return
elif response.get("message") == "not_enough_health_food": elif response.get("message") == "not_enough_health_food":
self.buy_food() self.buy_food()
self._wam(holding) self._wam(holding)
elif response.get("message") == "tax_money":
self._report_action("WORK_AS_MANAGER", "Not enough money to work as manager!", kwargs=response)
self.write_log("Not enough money to work as manager!")
else: else:
msg = "I was not able to wam and or employ because:\n{}".format(response) msg = "I was not able to wam and or employ because:\n{}".format(response)
self._report_action("WORK_AS_MANAGER", f"Worked as manager failed: {msg}", kwargs=response) self._report_action("WORK_AS_MANAGER", f"Worked as manager failed: {msg}", kwargs=response)
@ -2566,7 +2665,8 @@ class Citizen(CitizenAnniversary, CitizenCompanies, CitizenEconomy, CitizenLeade
for holding in regions.values(): for holding in regions.values():
raw_usage = holding.get_wam_raw_usage() raw_usage = holding.get_wam_raw_usage()
if (raw_usage['frm'] + raw_usage['wrm']) * 100 + self.inventory['used'] > self.inventory['total']: free_storage = self.inventory_status['total'] - self.inventory_status['used']
if (raw_usage['frm'] + raw_usage['wrm']) * 100 > free_storage:
self._report_action('WAM_UNAVAILABLE', 'Not enough storage!') self._report_action('WAM_UNAVAILABLE', 'Not enough storage!')
continue continue
self.travel_to_holding(holding) self.travel_to_holding(holding)

View File

@ -3,7 +3,7 @@ import hashlib
import threading import threading
import weakref import weakref
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, NamedTuple, Optional, Tuple, Union from typing import Any, Dict, List, NamedTuple, Tuple, Union, NoReturn, Generator, Iterable
from requests import Response, Session, post from requests import Response, Session, post
@ -28,27 +28,38 @@ class Holding:
id: int id: int
region: int region: int
companies: List["Company"] companies: List["Company"]
name: str
_citizen = weakref.ReferenceType _citizen = weakref.ReferenceType
def __init__(self, _id: int, region: int, citizen): def __init__(self, _id: int, region: int, citizen, name: str = None):
self._citizen = weakref.ref(citizen) self._citizen = weakref.ref(citizen)
self.id: int = _id self.id: int = _id
self.region: int = region self.region: int = region
self.companies: List["Company"] = list() self.companies: List["Company"] = list()
if name:
self.name = name
else:
comp_sum = len(self.companies)
name = f"Holding (#{self.id}) with {comp_sum} "
if comp_sum == 1:
name += "company"
else:
name += "companies"
self.name = name
@property @property
def wam_count(self) -> int: def wam_count(self) -> int:
return sum([company.wam_enabled and not company.already_worked for company in self.companies]) return len([1 for company in self.companies if company.wam_enabled and not company.already_worked])
@property @property
def wam_companies(self) -> List["Company"]: def wam_companies(self) -> Iterable["Company"]:
return [company for company in self.companies if company.wam_enabled] return [company for company in self.companies if company.wam_enabled]
@property @property
def employable_companies(self) -> List["Company"]: def employable_companies(self) -> Iterable["Company"]:
return [company for company in self.companies if company.preset_works] return [company for company in self.companies if company.preset_works]
def add_company(self, company: "Company"): def add_company(self, company: "Company") -> NoReturn:
self.companies.append(company) self.companies.append(company)
self.companies.sort() self.companies.sort()
@ -62,7 +73,7 @@ class Holding:
wrm += company.raw_usage wrm += company.raw_usage
return dict(frm=frm, wrm=wrm) return dict(frm=frm, wrm=wrm)
def get_wam_companies(self, raw_factory: bool = None): def get_wam_companies(self, raw_factory: bool = None) -> List["Company"]:
raw = [] raw = []
factory = [] factory = []
for company in self.wam_companies: for company in self.wam_companies:
@ -71,18 +82,15 @@ class Holding:
raw.append(company) raw.append(company)
else: else:
factory.append(company) factory.append(company)
if raw_factory is not None and not raw_factory: if raw_factory is None:
return factory
elif raw_factory is not None and raw_factory:
return raw
elif raw_factory is None:
return raw + factory return raw + factory
else: else:
raise ErepublikException("raw_factory should be True/False/None") return raw if raw_factory else factory
def __str__(self): def __str__(self) -> str:
name = f"Holding (#{self.id}) with {len(self.companies)} " comp = len(self.companies)
if len(self.companies) % 10 == 1: name = f"Holding (#{self.id}) with {comp} "
if comp == 1:
name += "company" name += "company"
else: else:
name += "companies" name += "companies"
@ -92,8 +100,9 @@ class Holding:
return str(self) return str(self)
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[str, int, List[Dict[str, Union[str, int, bool, float, Decimal]]]]]:
return dict(name=str(self), id=self.id, region=self.region, companies=self.companies, wam_count=self.wam_count) return dict(name=self.name, id=self.id, region=self.region,
companies=[c.as_dict for c in self.companies], wam_count=self.wam_count)
@property @property
def citizen(self): def citizen(self):
@ -170,7 +179,7 @@ class Company:
@property @property
def _sort_keys(self): def _sort_keys(self):
return not self.is_raw, self._internal_industry, -self.quality, self.id return not self.is_raw, self._internal_industry, self.quality, self.id
def __hash__(self): def __hash__(self):
return hash(self._sort_keys) return hash(self._sort_keys)
@ -203,7 +212,7 @@ class Company:
return str(self) return str(self)
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[str, int, bool, float, Decimal]]:
return dict(name=str(self), holding=self.holding.id, id=self.id, quality=self.quality, is_raw=self.is_raw, return dict(name=str(self), holding=self.holding.id, id=self.id, quality=self.quality, is_raw=self.is_raw,
raw_usage=self.raw_usage, products_made=self.products_made, wam_enabled=self.wam_enabled, raw_usage=self.raw_usage, products_made=self.products_made, wam_enabled=self.wam_enabled,
can_wam=self.can_wam, cannot_wam_reason=self.cannot_wam_reason, industry=self.industry, can_wam=self.can_wam, cannot_wam_reason=self.cannot_wam_reason, industry=self.industry,
@ -219,7 +228,7 @@ class Company:
return self.holding.citizen._post_economy_upgrade_company(self.id, level, self.holding.citizen.details.pin) return self.holding.citizen._post_economy_upgrade_company(self.id, level, self.holding.citizen.details.pin)
@property @property
def holding(self): def holding(self) -> Holding:
return self._holding() return self._holding()
@ -229,13 +238,14 @@ class MyCompanies:
ff_lockdown: int = 0 ff_lockdown: int = 0
holdings: Dict[int, Holding] holdings: Dict[int, Holding]
companies: weakref.WeakSet _companies: weakref.WeakSet
_citizen: weakref.ReferenceType _citizen: weakref.ReferenceType
companies: Generator[Company, None, None]
def __init__(self, citizen): def __init__(self, citizen):
self._citizen = weakref.ref(citizen) self._citizen = weakref.ref(citizen)
self.holdings: Dict[int, Holding] = dict() self.holdings = dict()
self.companies: weakref.WeakSet = weakref.WeakSet() self._companies = weakref.WeakSet()
self.next_ot_time = utils.now() self.next_ot_time = utils.now()
def prepare_holdings(self, holdings: Dict[str, Dict[str, Any]]): def prepare_holdings(self, holdings: Dict[str, Dict[str, Any]]):
@ -245,10 +255,10 @@ class MyCompanies:
for holding in holdings.values(): for holding in holdings.values():
if holding.get('id') not in self.holdings: if holding.get('id') not in self.holdings:
self.holdings.update({ self.holdings.update({
int(holding.get('id')): Holding(holding['id'], holding['region_id'], self.citizen) int(holding.get('id')): Holding(holding['id'], holding['region_id'], self.citizen, holding['name'])
}) })
if not self.holdings.get(0): if not self.holdings.get(0):
self.holdings.update({0: Holding(0, 0, self.citizen)}) # unassigned self.holdings.update({0: Holding(0, 0, self.citizen, 'Unassigned')}) # unassigned
def prepare_companies(self, companies: Dict[str, Dict[str, Any]]): def prepare_companies(self, companies: Dict[str, Dict[str, Any]]):
""" """
@ -270,7 +280,7 @@ class MyCompanies:
company_dict.get('can_work_as_manager'), company_dict.get('cannot_work_as_manager_reason'), company_dict.get('can_work_as_manager'), company_dict.get('cannot_work_as_manager_reason'),
company_dict.get('industry_id'), company_dict.get('already_worked'), company_dict.get('preset_works') company_dict.get('industry_id'), company_dict.get('already_worked'), company_dict.get('preset_works')
) )
self.companies.add(company) self._companies.add(company)
holding.add_company(company) holding.add_company(company)
def get_employable_factories(self) -> Dict[int, int]: def get_employable_factories(self) -> Dict[int, int]:
@ -280,14 +290,18 @@ class MyCompanies:
return sum([holding.wam_count for holding in self.holdings.values()]) return sum([holding.wam_count for holding in self.holdings.values()])
@staticmethod @staticmethod
def get_needed_inventory_usage(companies: Union[Company, List[Company]]) -> Decimal: def get_needed_inventory_usage(companies: Union[Company, Iterable[Company]]) -> Decimal:
if isinstance(companies, list): if isinstance(companies, list):
return sum([company.products_made * 100 if company.is_raw else 1 for company in companies]) return sum(company.products_made * 100 if company.is_raw else 1 for company in companies)
else: else:
return companies.products_made return companies.products_made
@property
def companies(self) -> Generator[Company, None, None]:
return (c for c in self._companies)
def __str__(self): def __str__(self):
return f"MyCompanies: {len(self.companies)} companies in {len(self.holdings)} holdings" return f"MyCompanies: {sum(1 for _ in self.companies)} companies in {len(self.holdings)} holdings"
def __repr__(self): def __repr__(self):
return str(self) return str(self)
@ -297,12 +311,16 @@ class MyCompanies:
for company in holding.companies: # noqa for company in holding.companies: # noqa
del company del company
holding.companies.clear() holding.companies.clear()
self.companies.clear() self._companies.clear()
@property @property
def as_dict(self): def as_dict(self) -> Dict[str, Union[str, int, datetime.datetime, Dict[str, Dict[str, Union[
str, int, List[Dict[str, Union[str, int, bool, float, Decimal]]]]
]]]]:
return dict(name=str(self), work_units=self.work_units, next_ot_time=self.next_ot_time, return dict(name=str(self), work_units=self.work_units, next_ot_time=self.next_ot_time,
ff_lockdown=self.ff_lockdown, holdings=self.holdings, company_count=len(self.companies)) ff_lockdown=self.ff_lockdown,
holdings={str(hi): h.as_dict for hi, h in self.holdings.items()},
company_count=sum(1 for _ in self.companies))
@property @property
def citizen(self): def citizen(self):
@ -438,24 +456,25 @@ class Energy:
class Details: class Details:
xp = 0 xp: int = 0
cc = 0 cc: float = 0
pp = 0 pp: int = 0
pin = None pin: str = None
gold = 0 gold: float = 0
next_pp: List[int] = None next_pp: List[int] = None
citizen_id = 0 citizen_id: int = 0
citizenship: constants.Country citizenship: constants.Country
current_region = 0 current_region: int = 0
current_country: constants.Country current_country: constants.Country
residence_region = 0 residence_region: int = 0
residence_country: constants.Country residence_country: constants.Country
daily_task_done = False daily_task_done: bool = False
daily_task_reward = False daily_task_reward: bool = False
mayhem_skills = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0, } mayhem_skills: Dict[int, int]
def __init__(self): def __init__(self):
self.next_pp = [] self.next_pp = []
self.mayhem_skills = {1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0, 10: 0, 11: 0, 12: 0, 13: 0, 14: 0}
@property @property
def xp_till_level_up(self): def xp_till_level_up(self):
@ -840,13 +859,15 @@ class Battle:
self.invader = BattleSide( self.invader = BattleSide(
self, constants.COUNTRIES[battle.get('inv', {}).get('id')], battle.get('inv', {}).get('points'), self, constants.COUNTRIES[battle.get('inv', {}).get('id')], battle.get('inv', {}).get('points'),
[constants.COUNTRIES[row.get('id')] for row in battle.get('inv', {}).get('ally_list')], [constants.COUNTRIES[row.get('id')] for row in battle.get('inv', {}).get('ally_list')],
[constants.COUNTRIES[row.get('id')] for row in battle.get('inv', {}).get('ally_list') if row['deployed']], False [constants.COUNTRIES[row.get('id')] for row in battle.get('inv', {}).get('ally_list') if row['deployed']],
False
) )
self.defender = BattleSide( self.defender = BattleSide(
self, constants.COUNTRIES[battle.get('def', {}).get('id')], battle.get('def', {}).get('points'), self, constants.COUNTRIES[battle.get('def', {}).get('id')], battle.get('def', {}).get('points'),
[constants.COUNTRIES[row.get('id')] for row in battle.get('def', {}).get('ally_list')], [constants.COUNTRIES[row.get('id')] for row in battle.get('def', {}).get('ally_list')],
[constants.COUNTRIES[row.get('id')] for row in battle.get('def', {}).get('ally_list') if row['deployed']], True [constants.COUNTRIES[row.get('id')] for row in battle.get('def', {}).get('ally_list') if row['deployed']],
True
) )
self.div = {} self.div = {}
@ -855,7 +876,7 @@ class Battle:
if data.get('end'): if data.get('end'):
end = datetime.datetime.fromtimestamp(data.get('end'), tz=constants.erep_tz) end = datetime.datetime.fromtimestamp(data.get('end'), tz=constants.erep_tz)
else: else:
end = utils.localize_dt(datetime.datetime.max - datetime.timedelta(days=1)) end = constants.max_datetime
battle_div = BattleDivision(self, div_id=data.get('id'), div=data.get('div'), end=end, battle_div = BattleDivision(self, div_id=data.get('id'), div=data.get('div'), end=end,
epic=data.get('epic_type') in [1, 5], epic=data.get('epic_type') in [1, 5],
@ -873,7 +894,8 @@ class Battle:
else: else:
time_part = "-{}".format(self.start - time_now) time_part = "-{}".format(self.start - time_now)
return f"Battle {self.id} for {self.region_name[:16]} | {self.invader} : {self.defender} | Round time {time_part}" return (f"Battle {self.id} for {self.region_name[:16]} | "
f"{self.invader} : {self.defender} | Round time {time_part}")
def __repr__(self): def __repr__(self):
return f"<Battle #{self.id} {self.invader}:{self.defender} R{self.zone_id}>" return f"<Battle #{self.id} {self.invader}:{self.defender} R{self.zone_id}>"

View File

@ -1,10 +1,14 @@
import datetime
from typing import Dict, Optional, Union from typing import Dict, Optional, Union
import pytz import pytz
__all__ = ["erep_tz", "Country", "AIR_RANKS", "COUNTRIES", "FOOD_ENERGY", "GROUND_RANKS", "GROUND_RANK_POINTS", "INDUSTRIES", "TERRAINS"] __all__ = ["erep_tz", 'min_datetime', "max_datetime", "Country", "AIR_RANKS", "COUNTRIES", "FOOD_ENERGY",
"GROUND_RANKS", "GROUND_RANK_POINTS", "INDUSTRIES", "TERRAINS"]
erep_tz = pytz.timezone('US/Pacific') erep_tz = pytz.timezone('US/Pacific')
min_datetime = erep_tz.localize(datetime.datetime(2007, 11, 20))
max_datetime = erep_tz.localize(datetime.datetime(2281, 9, 4))
class Country: class Country:
@ -49,17 +53,17 @@ class Country:
class Industries: class Industries:
__by_name = {'food': 1, 'weapon': 2, 'house': 4, 'aircraft': 23, __by_name = {'food': 1, 'weapon': 2, 'ticket': 3, 'house': 4, 'aircraft': 23,
'foodraw': 7, 'weaponraw': 12, 'houseraw': 18, 'aircraftraw': 24, 'foodraw': 7, 'weaponraw': 12, 'houseraw': 18, 'aircraftraw': 24, 'airplaneraw': 24,
'frm': 7, 'wrm': 12, 'hrm': 18, 'arm': 24, 'frm': 7, 'wrm': 12, 'hrm': 18, 'arm': 24,
'frm q1': 7, 'frm q2': 8, 'frm q3': 9, 'frm q4': 10, 'frm q5': 11, 'frm q1': 7, 'frm q2': 8, 'frm q3': 9, 'frm q4': 10, 'frm q5': 11,
'wrm q1': 12, 'wrm q2': 13, 'wrm q3': 14, 'wrm q4': 15, 'wrm q5': 16, 'wrm q1': 12, 'wrm q2': 13, 'wrm q3': 14, 'wrm q4': 15, 'wrm q5': 16,
'hrm q1': 18, 'hrm q2': 19, 'hrm q3': 20, 'hrm q4': 21, 'hrm q5': 22, 'hrm q1': 18, 'hrm q2': 19, 'hrm q3': 20, 'hrm q4': 21, 'hrm q5': 22,
'arm q1': 24, 'arm q2': 25, 'arm q3': 26, 'arm q4': 27, 'arm q5': 28} 'arm q1': 24, 'arm q2': 25, 'arm q3': 26, 'arm q4': 27, 'arm q5': 28}
__by_id = {1: "Food", 2: "Weapon", 4: "House", 23: "Aircraft", __by_id = {1: "Food", 2: "Weapon", 3: "Ticket", 4: "House", 23: "Aircraft",
7: "foodRaw", 8: "FRM q2", 9: "FRM q3", 10: "FRM q4", 11: "FRM q5", 7: "foodRaw", 8: "FRM q2", 9: "FRM q3", 10: "FRM q4", 11: "FRM q5",
12: "weaponRaw", 13: "WRM q2", 14: "WRM q3", 15: "WRM q4", 16: "WRM q5", 12: "weaponRaw", 13: "WRM q2", 14: "WRM q3", 15: "WRM q4", 16: "WRM q5",
18: "houseRaw", 19: "HRM q2", 20: "HRM q3", 21: "HRM q4", 22: "HRM q5", 17: "houseRaw", 18: "houseRaw", 19: "HRM q2", 20: "HRM q3", 21: "HRM q4", 22: "HRM q5",
24: "aircraftRaw", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5"} 24: "aircraftRaw", 25: "ARM q2", 26: "ARM q3", 27: "ARM q4", 28: "ARM q5"}
def __getitem__(self, item) -> Optional[Union[int, str]]: def __getitem__(self, item) -> Optional[Union[int, str]]:

View File

@ -7,6 +7,7 @@ import textwrap
import time import time
import traceback import traceback
import unicodedata import unicodedata
import warnings
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional, Union, Dict from typing import Any, List, Optional, Union, Dict
@ -26,8 +27,8 @@ __all__ = ['VERSION', 'calculate_hit', 'caught_error', 'date_from_eday', 'eday_f
'process_error', 'process_warning', 'send_email', 'silent_sleep', 'slugify', 'write_file', 'process_error', 'process_warning', 'send_email', 'silent_sleep', 'slugify', 'write_file',
'write_interactive_log', 'write_silent_log'] 'write_interactive_log', 'write_silent_log']
if not sys.version_info >= (3, 7): if not sys.version_info >= (3, 6):
raise AssertionError('This script requires Python version 3.7 and higher\n' raise AssertionError('This script requires Python version 3.6 and higher\n'
'But Your version is v{}.{}.{}'.format(*sys.version_info)) 'But Your version is v{}.{}.{}'.format(*sys.version_info))
VERSION: str = __version__ VERSION: str = __version__
@ -47,7 +48,7 @@ def localize_dt(dt: Union[datetime.date, datetime.datetime]) -> datetime.datetim
elif isinstance(dt, datetime.date): elif isinstance(dt, datetime.date):
return constants.erep_tz.localize(datetime.datetime.combine(dt, datetime.time(0, 0, 0))) return constants.erep_tz.localize(datetime.datetime.combine(dt, datetime.time(0, 0, 0)))
else: else:
return dt.astimezone(constants.erep_tz) raise TypeError(f"Argument dt must be and instance of datetime.datetime or datetime.date not {type(dt)}")
def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime: def good_timedelta(dt: datetime.datetime, td: datetime.timedelta) -> datetime.datetime:
@ -370,7 +371,11 @@ def get_air_hit_dmg_value(citizen_id: int, natural_enemy: bool = False, true_pat
return calculate_hit(0, rang, true_patriot, elite, natural_enemy, booster, weapon_power) return calculate_hit(0, rang, true_patriot, elite, natural_enemy, booster, weapon_power)
def _clear_up_battle_memory(battle): # def _clear_up_battle_memory(battle):
del battle.invader._battle, battle.defender._battle # del battle.invader._battle, battle.defender._battle
for div_id, division in battle.div.items(): # for div_id, division in battle.div.items():
del division._battle # del division._battle
def deprecation(message):
warnings.warn(message, DeprecationWarning, stacklevel=2)

View File

@ -29,7 +29,7 @@ def _battle_launcher(player: Citizen):
""" """
global CONFIG global CONFIG
finished_war_ids = {*[]} finished_war_ids = {*[]}
war_data = CONFIG.get('start_battles', {}) war_data = CONFIG.get('battle_launcher', {})
war_ids = {int(war_id) for war_id in war_data.keys()} war_ids = {int(war_id) for war_id in war_data.keys()}
next_attack_time = player.now next_attack_time = player.now
next_attack_time = next_attack_time.replace(minute=next_attack_time.minute // 5 * 5, second=0) next_attack_time = next_attack_time.replace(minute=next_attack_time.minute // 5 * 5, second=0)
@ -45,7 +45,7 @@ def _battle_launcher(player: Citizen):
status = player.get_war_status(war_id) status = player.get_war_status(war_id)
if status.get('ended', False): if status.get('ended', False):
CONFIG['start_battles'].pop(war_id, None) CONFIG['battle_launcher'].pop(war_id, None)
finished_war_ids.add(war_id) finished_war_ids.add(war_id)
continue continue
elif not status.get('can_attack'): elif not status.get('can_attack'):
@ -64,7 +64,15 @@ def _battle_launcher(player: Citizen):
player.update_war_info() player.update_war_info()
battle_id = player.get_war_status(war_id).get("battle_id") battle_id = player.get_war_status(war_id).get("battle_id")
if battle_id is not None and battle_id in player.all_battles: if battle_id is not None and battle_id in player.all_battles:
player.fight(battle_id, player.details.citizenship, hits) battle = player.all_battles.get(battle_id)
for division in battle.div.values():
if division.div == player.division:
div = division
break
else:
player.report_error("Players division not found in the first round!")
break
player.fight(battle, div, battle.invader, hits)
break break
player.sleep(1) player.sleep(1)
if attacked: if attacked:

View File

@ -1,12 +1,16 @@
from datetime import timedelta from datetime import timedelta
from erepublik import Citizen, utils from erepublik import Citizen, utils, constants
CONFIG = { CONFIG = {
'email': 'player@email.com', 'email': 'player@email.com',
'password': 'Pa$5w0rd!', 'password': 'Pa$5w0rd!',
'interactive': True, 'interactive': True,
'debug': True 'debug': True,
'work': True,
'ot': True, # Work OverTime
'wam': True, # WorkAsManager
'train': True
} }
@ -14,11 +18,14 @@ CONFIG = {
def main(): def main():
player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False) player = Citizen(email=CONFIG['email'], password=CONFIG['password'], auto_login=False)
player.config.interactive = CONFIG['interactive'] player.config.interactive = CONFIG['interactive']
player.config.fight = CONFIG['fight'] player.config.work = CONFIG['work']
player.config.train = CONFIG['train']
player.config.ot = CONFIG['ot']
player.config.wam = CONFIG['wam']
player.set_debug(CONFIG.get('debug', False)) player.set_debug(CONFIG.get('debug', False))
player.login() player.login()
now = player.now.replace(second=0, microsecond=0) now = player.now.replace(second=0, microsecond=0)
dt_max = now.replace(year=9999) dt_max = constants.max_datetime
tasks = { tasks = {
'eat': now, 'eat': now,
} }
@ -76,7 +83,7 @@ def main():
tasks.update({'eat': next_time}) tasks.update({'eat': next_time})
if tasks.get('ot', dt_max) <= now: if tasks.get('ot', dt_max) <= now:
player.write_log("Doing task: ot") player.write_log("Doing task: work overtime")
if now > player.my_companies.next_ot_time: if now > player.my_companies.next_ot_time:
player.work_ot() player.work_ot()
next_time = now + timedelta(minutes=60) next_time = now + timedelta(minutes=60)

View File

@ -1,18 +1,19 @@
bump2version==1.0.0 bump2version==1.0.1
coverage==5.3 coverage==5.3
edx-sphinx-theme==1.5.0 edx-sphinx-theme==1.5.0
flake8==3.8.3 flake8==3.8.4
ipython==7.18.1 ipython>=7.19.0
isort==5.5.3 isort==5.6.4
pip==20.2.3 pip==20.2.4
PyInstaller==4.0 PyInstaller==4.0
pytz==2020.1 pytz==2020.4
pytest==6.1.0 pytest==6.1.2
responses==0.12.0 responses==0.12.0
setuptools==50.3.0 setuptools==50.3.2
Sphinx==3.2.1 Sphinx==3.3.0
requests[socks]==2.24.0 requests==2.24.0
tox==3.20.0 PySocks==1.7.1
tox==3.20.1
twine==3.2.0 twine==3.2.0
watchdog==0.10.3 watchdog==0.10.3
wheel==0.35.1 wheel==0.35.1

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.21.5 current_version = 0.22.3.1
commit = True commit = True
tag = True tag = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\.?(?P<dev>\d+)?

View File

@ -11,11 +11,18 @@ with open('README.rst') as readme_file:
with open('HISTORY.rst') as history_file: with open('HISTORY.rst') as history_file:
history = history_file.read() history = history_file.read()
requirements = ['pytz==2020.1', 'requests[socks]==2.24.0'] requirements = [
'pytz==2020.4',
'requests==2.24.0',
'PySocks==1.7.1'
]
setup_requirements = [] setup_requirements = []
test_requirements = [] test_requirements = [
"pytest==6.1.2",
"responses==0.12.0"
]
setup( setup(
author="Eriks Karls", author="Eriks Karls",
@ -38,11 +45,11 @@ setup(
keywords='erepublik', keywords='erepublik',
name='eRepublik', name='eRepublik',
packages=find_packages(include=['erepublik']), packages=find_packages(include=['erepublik']),
python_requires='>=3.7, <4', python_requires='>=3.6, <4',
setup_requires=setup_requirements, setup_requires=setup_requirements,
test_suite='tests', test_suite='tests',
tests_require=test_requirements, tests_require=test_requirements,
url='https://github.com/eeriks/erepublik/', url='https://github.com/eeriks/erepublik/',
version='0.21.5', version='0.22.3.1',
zip_safe=False, zip_safe=False,
) )

View File

@ -2,7 +2,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Tests for `erepublik` package.""" """Tests for `erepublik` package."""
from typing import Callable
from erepublik import Citizen from erepublik import Citizen
@ -66,7 +65,8 @@ class TestErepublik(unittest.TestCase):
self.assertEqual(self.citizen.next_reachable_energy, 0) self.assertEqual(self.citizen.next_reachable_energy, 0)
def test_should_fight(self): def test_should_fight(self):
is_wc_close: Callable[[], bool] = lambda: self.citizen.max_time_till_full_ff > self.citizen.time_till_week_change def is_wc_close():
return self.citizen.max_time_till_full_ff > self.citizen.time_till_week_change
self.citizen.config.fight = False self.citizen.config.fight = False
self.assertEqual(self.citizen.should_fight(), (0, "Fighting not allowed!", False)) self.assertEqual(self.citizen.should_fight(), (0, "Fighting not allowed!", False))

View File

@ -1,9 +1,10 @@
[tox] [tox]
envlist = py37, flake8 envlist = py37, py38, flake8
[travis] [travis]
python = python =
3.7: py37 3.7: py37
3.8: py38
[testenv:flake8] [testenv:flake8]
basepython = python basepython = python
@ -15,4 +16,3 @@ setenv =
PYTHONPATH = {toxinidir} PYTHONPATH = {toxinidir}
commands = python setup.py test commands = python setup.py test