Skip to content

Commit a457293

Browse files
committed
token refresh bot, refactor api, compose, readme
1 parent de6383d commit a457293

20 files changed

+96
-36
lines changed

README.MD

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## timabilov / django-social-sample [![Build Status](https://travis-ci.org/timabilov/django-social-sample.svg?branch=master)](https://travis-ci.org/timabilov/django-social-sample)
22

3-
Install `docker` and `docker-compose`
3+
Required `docker` and `docker-compose`
44
###### docker-compose-prod.yml is not production ready. used for `uwsgi bot setup` ONLY.
55

66

@@ -11,7 +11,7 @@ Add your `CLEARBIT_KEY` to configuration `env.secret` if you want.
1111

1212
Open `http://localhost/`
1313

14-
For spamming/testing API in `bot/` directory
14+
For spamming/testing API in `bot/` directory:
1515

1616
`python social_bot.py -u 100 -p 20 -l 20`
1717
`python social_bot.py --config config.json`
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 2.0.8 on 2018-12-06 15:46
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0002_userprofile_bio'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='userprofile',
15+
name='last_name',
16+
field=models.CharField(blank=True, max_length=100, verbose_name='last_name'),
17+
),
18+
]

api/urls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
urlpatterns = [
5-
path('v1.0/', include('api.views.v1.urls')),
5+
path('v1.0/', include('api.v1.urls')),
66
# There are many methods for versioning, for simple one:
77
# Next(v1.1) api version refer to previous one with overriding particular
88
# feature
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

api/views/v1/urls.py api/v1/urls.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
# TODO pretend that there is swagger route.
1111

1212

13-
from api.views.v1.views.profile import UserSignUpAPI
14-
from api.views.v1.views.social import PostViewSet
13+
from api.v1.views.profile import UserSignUpAPI
14+
from api.v1.views.social import PostViewSet
1515

1616
router = SimpleRouter()
1717
router.register(
File renamed without changes.

api/views/v1/views/profile.py api/v1/views/profile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from rest_framework.views import APIView
33
from django.utils.translation import gettext_lazy as _
44

5-
from api.views.v1.serializers.profile import UserRegistrationSerializer
5+
from api.v1.serializers.profile import UserRegistrationSerializer
66

77

88
class UserSignUpAPI(APIView):

api/views/v1/views/social.py api/v1/views/social.py

+23-18
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
from rest_framework.views import APIView
1111
from api.models import UserProfile, Post, PostLike
1212
from django.utils.translation import ugettext_lazy as tr
13-
from api.views.v1.serializers.social import PostSerializer
13+
from api.v1.serializers.social import PostSerializer
14+
from django.db import transaction
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -61,25 +62,29 @@ def like(self, *args, **kwargs):
6162
like = PostLike.objects.filter(
6263
post_id=post.id, reacted_id=self.request.user.id
6364
)
64-
deleted, _ = like.delete()
65-
if not deleted:
66-
return JsonResponse({
67-
"message": "Not found"
68-
}, status=404)
69-
Post.objects.filter(id=post.id).update(likes=F('likes') - 1)
65+
66+
with transaction.atomic():
67+
deleted, _ = like.delete()
68+
if not deleted:
69+
return JsonResponse({
70+
"message": "Not found"
71+
}, status=404)
72+
Post.objects.filter(id=post.id).update(likes=F('likes') - 1)
7073
elif self.request.method == 'POST':
7174
try:
72-
like, created = PostLike.objects.get_or_create(
73-
post_id=post.id, reacted_id=self.request.user.id
74-
)
75-
76-
if created:
77-
Post.objects.filter(id=post.id).update(likes=F('likes') + 1)
78-
else:
79-
# we can omit this response - depends on arch and client
80-
return JsonResponse({
81-
"message": tr("You've already liked this post"),
82-
}, status=400)
75+
with transaction.atomic():
76+
like, created = PostLike.objects.get_or_create(
77+
post_id=post.id, reacted_id=self.request.user.id
78+
)
79+
80+
if created:
81+
Post.objects.filter(id=post.id).update(likes=F('likes') + 1)
82+
else:
83+
# we can omit this response - depends on arch and client
84+
return JsonResponse({
85+
"message": tr("You've already liked this post"),
86+
}, status=400)
87+
8388
except IntegrityError:
8489
# get or create handles race condition problem
8590
# with getting already created object

bot/core.py

+30-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
class User:
1313
"""
1414
Client for simulating user behaviour handling server API.
15+
Token refreshed automatically.
1516
Username and password is necessary for refreshing token.
1617
"""
1718

@@ -77,18 +78,39 @@ def __exhausted_limit(self, attr):
7778

7879
return False
7980

80-
async def _request(self, method, url, *args, **kwargs):
81+
async def _token_lock(self):
82+
83+
while not self.token:
84+
await asyncio.sleep(0.1)
85+
86+
return True
87+
88+
async def _request(self, method, url, auth_header=True, *args, **kwargs):
8189

8290
exc = None
83-
if self.token:
84-
kwargs.setdefault('headers', {}).update(self._auth_header())
91+
# attempts for disconnect or token expiration.
92+
for i in range(10):
93+
94+
if self.token:
95+
kwargs.setdefault('headers', {}).update(self._auth_header())
8596

86-
# server drops connection so we use 20 attempts to not miss any req.
87-
for i in range(20):
8897
try:
8998
async with self.session.request(method, url, *args, **kwargs) as response:
9099

91-
# TODO: check if status 401 refresh token
100+
if response.status == 401:
101+
if auth_header and not self.token:
102+
103+
# other tasks for that user most likely will get 401
104+
# too, lock until new token issued.
105+
# TODO: implement proper lock/pause
106+
await self._token_lock()
107+
continue
108+
response.close()
109+
110+
self.token = None
111+
await self._get_token()
112+
113+
continue
92114
return await response.json(), response.status
93115

94116
except aiohttp.ClientError as e:
@@ -108,6 +130,7 @@ async def _get_token(self):
108130
# we can load users from some file or external resources as example
109131

110132
response, status = await self._request('POST', User.LOGIN_URL,
133+
auth_header=False,
111134
data={
112135
"username": self.username,
113136
"password": self.password
@@ -151,6 +174,7 @@ async def like(self, post_id):
151174

152175
async def signup(self):
153176
_, status = await self._request('POST', User.SIGNUP_URL,
177+
auth_header=False,
154178
data={
155179
"username": self.username,
156180
"password": self.password

bot/social_bot.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import json
33
import logging
4+
import random
45

56
import sys
67
from json import JSONDecodeError
@@ -72,12 +73,16 @@ async def simulate_when_done(coro):
7273

7374
async def main():
7475

75-
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector()) as session:
76+
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(force_close=True)) as session:
7677

7778
logger.info('Start generating users...')
78-
tasks = [asyncio.ensure_future(User.generate(session)) for _ in range(RULES['users'])]
79+
tasks = [asyncio.ensure_future(User.generate(
80+
session,
81+
max_posts=random.randint(0, RULES['max_user_posts']),
82+
max_likes=random.randint(0, RULES['max_user_likes']),
83+
)) for _ in range(RULES['users'])]
7984

80-
logger.info('Waiting for generating content...')
85+
logger.info('Waiting for content generation...')
8186
# Start simulation right after release of each task
8287
await asyncio.gather(*[
8388
asyncio.ensure_future(simulate_when_done(awaited_user))
@@ -90,3 +95,4 @@ async def main():
9095
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
9196
loop = asyncio.get_event_loop()
9297
loop.run_until_complete(main())
98+

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: '3'
33
services:
44

55
db:
6-
image: postgres
6+
image: postgres:10
77
volumes:
88
- pgdata:/var/lib/postgresql/data
99

entry.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ set -e
33
python manage.py makemigrations --noinput
44
python manage.py migrate
55
python manage.py collectstatic --noinput
6-
uwsgi --http 0.0.0.0:8000 --module socialplatform.wsgi:application --processes 1 --threads 4
6+
uwsgi --http 0.0.0.0:8000 --module socialplatform.wsgi:application --processes 2 --threads 4

socialplatform/settings/base.py

+7
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,10 @@
145145

146146
CLEARBIT_API_URL = 'https://person.clearbit.com/v2/'
147147
CLEARBIT_API_KEY = os.environ.get('CLEARBIT_KEY')
148+
149+
150+
from datetime import timedelta
151+
152+
SIMPLE_JWT = {
153+
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
154+
}

socialplatform/views.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.shortcuts import render_to_response
66

77
from api.models import Post, PostLike
8-
from api.views.v1.serializers.social import PostSerializer
8+
from api.v1.serializers.social import PostSerializer
99

1010

1111
def index(request):

0 commit comments

Comments
 (0)