Skip to content

Commit de6383d

Browse files
committed
bot refactor API
1 parent ba3aeb6 commit de6383d

File tree

2 files changed

+236
-115
lines changed

2 files changed

+236
-115
lines changed

bot/core.py

+207
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import asyncio
2+
import logging
3+
4+
import aiohttp
5+
import random
6+
import string
7+
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class User:
13+
"""
14+
Client for simulating user behaviour handling server API.
15+
Username and password is necessary for refreshing token.
16+
"""
17+
18+
API_HOST = 'http://localhost/v1.0/'
19+
SIGNUP_URL = API_HOST + 'signup/'
20+
LOGIN_URL = API_HOST + 'token/'
21+
POST_URL = API_HOST + 'posts/'
22+
23+
LIKE_URL = API_HOST + 'posts/{id}/like/'
24+
25+
CONTENT = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
26+
eiusmod tempor incididunt ut labore et dolore magna aliqua.
27+
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
28+
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
29+
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
30+
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
31+
officia deserunt mollit anim id est laborum."""
32+
33+
_total_posts = []
34+
35+
def __init__(self, session, username, password, max_posts=5, max_likes=5, token=None):
36+
"""
37+
38+
"""
39+
40+
self.username = username
41+
self.password = password
42+
self.session = session
43+
self.max_posts = max_posts
44+
self.max_likes = max_likes
45+
self.posts, self.likes = 0, 0
46+
self.token = token
47+
48+
@staticmethod
49+
def __generate_string(length=8):
50+
return ''.join(
51+
random.choices(string.ascii_uppercase + string.digits, k=length)
52+
)
53+
54+
@classmethod
55+
async def generate(cls, session, **kwargs):
56+
"""
57+
Generate's new random user.
58+
:param session:
59+
:param kwargs: All other user parameters
60+
:return:
61+
"""
62+
user = User(
63+
session,
64+
username=User.__generate_string(),
65+
password=User.__generate_string(length=20),
66+
**kwargs
67+
)
68+
69+
# return asyncio.ensure_future(user.signup())
70+
return await user.signup()
71+
72+
def __exhausted_limit(self, attr):
73+
74+
if getattr(self, attr) >= getattr(self, 'max_' + attr):
75+
logger.warning(f'Number of {attr} for user {self.username} exhausted')
76+
return True
77+
78+
return False
79+
80+
async def _request(self, method, url, *args, **kwargs):
81+
82+
exc = None
83+
if self.token:
84+
kwargs.setdefault('headers', {}).update(self._auth_header())
85+
86+
# server drops connection so we use 20 attempts to not miss any req.
87+
for i in range(20):
88+
try:
89+
async with self.session.request(method, url, *args, **kwargs) as response:
90+
91+
# TODO: check if status 401 refresh token
92+
return await response.json(), response.status
93+
94+
except aiohttp.ClientError as e:
95+
exc = e
96+
# print(f'{i} Server disconnect... Try again {url}')
97+
logger.error(f'Host error after {i} times: {exc}')
98+
raise Exception(f'Host error after {i} times: {exc}')
99+
100+
def _auth_header(self):
101+
102+
return {
103+
"Authorization": "Bearer %s" % self.token
104+
}
105+
106+
async def _get_token(self):
107+
108+
# we can load users from some file or external resources as example
109+
110+
response, status = await self._request('POST', User.LOGIN_URL,
111+
data={
112+
"username": self.username,
113+
"password": self.password
114+
})
115+
if status != 200:
116+
logger.warning('Error ', response)
117+
raise Exception('Invalid token')
118+
self.token = response['access']
119+
120+
@property
121+
def left_likes(self):
122+
return self.max_likes - self.likes
123+
124+
@property
125+
def left_posts(self):
126+
return self.max_posts - self.posts
127+
128+
async def create_content(self, content=CONTENT):
129+
130+
if self.__exhausted_limit('posts'):
131+
return
132+
133+
post, status = await self._request('POST', User.POST_URL,
134+
data={"content": content})
135+
if status == 200:
136+
self.posts += 1
137+
return post
138+
139+
async def like(self, post_id):
140+
if self.__exhausted_limit('likes'):
141+
return
142+
143+
message, status = await self._request(
144+
'POST', User.LIKE_URL.format(id=post_id)
145+
)
146+
147+
if status == 200:
148+
self.likes += 1
149+
150+
return status
151+
152+
async def signup(self):
153+
_, status = await self._request('POST', User.SIGNUP_URL,
154+
data={
155+
"username": self.username,
156+
"password": self.password
157+
})
158+
159+
if status != 200:
160+
raise Exception('signup failed')
161+
162+
await self._get_token()
163+
return self
164+
165+
async def generate_content(self):
166+
"""
167+
Generate posts for amount of quota left
168+
:return:
169+
"""
170+
# generate content
171+
if self.__exhausted_limit('posts'):
172+
return
173+
174+
posts = await asyncio.gather(*[
175+
self.create_content(f"{self.username} content #{i}: {User.CONTENT} ")
176+
for i in range(self.left_posts)
177+
])
178+
179+
# save generated posts to not waste server resources.
180+
User._total_posts.extend(map(lambda i: i['id'], posts))
181+
182+
return posts
183+
184+
async def generate_likes(self):
185+
"""
186+
187+
Generate likes for amount of quota left
188+
:return:
189+
"""
190+
if self.__exhausted_limit('likes'):
191+
return
192+
193+
await asyncio.gather(*[
194+
195+
self.like(pid)
196+
for pid in random.choices(User._total_posts, k=self.left_likes)
197+
])
198+
199+
async def simulate(self):
200+
"""
201+
Let bot simulate fake behaviour with best scenario.
202+
:return:
203+
"""
204+
205+
# TODO: Declarative way of simulation
206+
await self.generate_content()
207+
await self.generate_likes()

0 commit comments

Comments
 (0)