├── instagram_api
├── migrations
│ ├── __init__.py
│ ├── 0002_user_follows_count.py
│ ├── 0015_auto_20160711_1646.py
│ ├── 0006_media_filter.py
│ ├── 0014_auto_20160622_1538.py
│ ├── 0003_user_is_private.py
│ ├── 0010_auto_20160212_1602.py
│ ├── 0008_auto_20160212_1345.py
│ ├── 0009_auto_20160212_1454.py
│ ├── 0005_auto_20160212_0204.py
│ ├── 0013_auto_20160607_1602.py
│ ├── 0007_auto_20160212_0346.py
│ ├── 0004_location.py
│ ├── 0012_auto_20160215_0123.py
│ ├── 0011_auto_20160213_0338.py
│ └── 0001_initial.py
├── __init__.py
├── admin.py
├── fields.py
├── factories.py
├── api.py
├── graphql.py
├── decorators.py
├── tests.py
└── models.py
├── MANIFEST.in
├── settings_test.py
├── travis_django_version.py
├── .coveragerc
├── .travis.yml
├── setup.py
├── LICENSE
├── README.md
└── quicktest.py
/instagram_api/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/instagram_api/__init__.py:
--------------------------------------------------------------------------------
1 | VERSION = (0, 3, 7)
2 | __version__ = '.'.join(map(str, VERSION))
3 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md
2 | include LICENSE
3 | include MANIFEST.in
4 | include quicktest.py
5 | include settings_test.py
6 | recursive-include instagram_api *
7 |
--------------------------------------------------------------------------------
/settings_test.py:
--------------------------------------------------------------------------------
1 | INSTALLED_APPS = (
2 | 'm2m_history',
3 | )
4 | SOCIAL_API_TOKENS_STORAGES = []
5 | SOCIAL_API_INSTAGRAM_CLIENT_ID = 'fac34adbc6fd4f56803ec100234bf682'
6 | SOCIAL_API_INSTAGRAM_CLIENT_SECRET = '84a6a4732d31441d8794fd0e9cf6fe01'
7 |
--------------------------------------------------------------------------------
/travis_django_version.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import sys
3 |
4 | version = sys.argv[1]
5 | if version.startswith('http'):
6 | print(version)
7 | else:
8 | next_version = float(version) + 0.1
9 | print('Django>=%s,<%.1f' % (version, next_version))
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | omit =
3 | *migrations*
4 | *admin.py
5 |
6 | [report]
7 | exclude_lines =
8 | def __repr__
9 | def __unicode__
10 | def parse_args
11 | pragma: no cover
12 | raise NotImplementedError
13 | if __name__ == .__main__.:
--------------------------------------------------------------------------------
/instagram_api/migrations/0002_user_follows_count.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0001_initial'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='user',
16 | name='follows_count',
17 | field=models.PositiveIntegerField(null=True),
18 | preserve_default=True,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0015_auto_20160711_1646.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0014_auto_20160622_1538'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='user',
16 | name='full_name',
17 | field=models.CharField(max_length=80),
18 | preserve_default=True,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0006_media_filter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0005_auto_20160212_0204'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='media',
16 | name='filter',
17 | field=models.CharField(default='', max_length=40),
18 | preserve_default=False,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0014_auto_20160622_1538.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0013_auto_20160607_1602'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='user',
16 | name='username',
17 | field=models.CharField(max_length=30, db_index=True),
18 | preserve_default=True,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0003_user_is_private.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0002_user_follows_count'),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name='user',
16 | name='is_private',
17 | field=models.NullBooleanField(verbose_name=b'Account is private'),
18 | preserve_default=True,
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0010_auto_20160212_1602.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | import m2m_history.fields
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ('instagram_api', '0009_auto_20160212_1454'),
12 | ]
13 |
14 | operations = [
15 | migrations.AlterField(
16 | model_name='user',
17 | name='followers',
18 | field=m2m_history.fields.ManyToManyHistoryField(related_name='follows', to='instagram_api.User'),
19 | preserve_default=True,
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0008_auto_20160212_1345.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0007_auto_20160212_0346'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='location',
16 | name='street_address',
17 | ),
18 | migrations.AlterField(
19 | model_name='tag',
20 | name='name',
21 | field=models.CharField(unique=True, max_length=50),
22 | preserve_default=True,
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0009_auto_20160212_1454.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0008_auto_20160212_1345'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='media',
16 | name='locations',
17 | ),
18 | migrations.AddField(
19 | model_name='media',
20 | name='location',
21 | field=models.ForeignKey(related_name='media_feed', to='instagram_api.Location', null=True),
22 | preserve_default=True,
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0005_auto_20160212_0204.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0004_location'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='location',
16 | name='latitude',
17 | field=models.FloatField(null=True),
18 | preserve_default=True,
19 | ),
20 | migrations.AlterField(
21 | model_name='location',
22 | name='longitude',
23 | field=models.FloatField(null=True),
24 | preserve_default=True,
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0013_auto_20160607_1602.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0012_auto_20160215_0123'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='media',
16 | name='comments_count',
17 | field=models.PositiveIntegerField(default=0, null=True),
18 | preserve_default=True,
19 | ),
20 | migrations.AlterField(
21 | model_name='media',
22 | name='likes_count',
23 | field=models.PositiveIntegerField(default=0, null=True),
24 | preserve_default=True,
25 | ),
26 | ]
27 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "2.7"
4 | # - "3.4"
5 | env:
6 | - DJANGO=1.7 DB=postgres
7 | - DJANGO=1.8 DB=postgres
8 | # - DJANGO=1.9 DB=postgres
9 | before_script:
10 | - mysql -e 'create database django;'
11 | - psql -c 'create database django;' -U postgres
12 | install:
13 | - if [[ $DB == mysql ]]; then pip install mysql-python; fi
14 | - if [[ $DB == postgres ]]; then pip install psycopg2; fi
15 | - DJANGO_VER=$(./travis_django_version.py $DJANGO)
16 | - pip install $DJANGO_VER
17 | - pip install simplejson
18 | - pip install factory_boy
19 | - pip install coveralls
20 | - pip install mock
21 | - pip install .
22 | script:
23 | - django-admin.py --version
24 | - coverage run --source=instagram_api quicktest.py instagram_api
25 | after_success:
26 | - coveralls
27 | notifications:
28 | email:
29 | recipients:
30 | - ramusus@gmail.com
31 | on_success: change
32 | on_failure: change
33 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0007_auto_20160212_0346.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0006_media_filter'),
11 | ]
12 |
13 | operations = [
14 | migrations.RemoveField(
15 | model_name='location',
16 | name='media_feed',
17 | ),
18 | migrations.RemoveField(
19 | model_name='tag',
20 | name='media_feed',
21 | ),
22 | migrations.AddField(
23 | model_name='media',
24 | name='locations',
25 | field=models.ManyToManyField(related_name='media_feed', to='instagram_api.Location'),
26 | preserve_default=True,
27 | ),
28 | migrations.AddField(
29 | model_name='media',
30 | name='tags',
31 | field=models.ManyToManyField(related_name='media_feed', to='instagram_api.Tag'),
32 | preserve_default=True,
33 | ),
34 | ]
35 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0004_location.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0003_user_is_private'),
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Location',
16 | fields=[
17 | ('fetched', models.DateTimeField(null=True, verbose_name='Fetched', blank=True)),
18 | ('id', models.BigIntegerField(serialize=False, primary_key=True)),
19 | ('name', models.CharField(max_length=100)),
20 | ('latitude', models.FloatField()),
21 | ('longitude', models.FloatField()),
22 | ('street_address', models.CharField(max_length=100)),
23 | ('media_count', models.PositiveIntegerField(null=True)),
24 | ('media_feed', models.ManyToManyField(related_name='locations', to='instagram_api.Media')),
25 | ],
26 | options={
27 | 'abstract': False,
28 | },
29 | bases=(models.Model,),
30 | ),
31 | ]
32 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | from setuptools import find_packages, setup
4 |
5 | setup(
6 | name='django-instagram-api',
7 | version=__import__('instagram_api').__version__,
8 | description='Django implementation for instagram API',
9 | long_description=open('README.md').read(),
10 | author='krupin.dv',
11 | author_email='krupin.dv19@gmail.com',
12 | url='https://github.com/ramusus/django-instagram-api',
13 | download_url='http://pypi.python.org/pypi/django-instagram-api',
14 | license='BSD',
15 | packages=find_packages(),
16 | include_package_data=True,
17 | zip_safe=False, # because we're including media that Django needs
18 | install_requires=[
19 | 'requests>=2.5.3',
20 | 'python-instagram==1.3.0',
21 | 'django-social-api>=0.0.3',
22 | 'django-m2m-history>=0.2.2',
23 | ],
24 | classifiers=[
25 | 'Development Status :: 4 - Beta',
26 | 'Environment :: Web Environment',
27 | 'Intended Audience :: Developers',
28 | 'License :: OSI Approved :: BSD License',
29 | 'Operating System :: OS Independent',
30 | 'Programming Language :: Python',
31 | 'Topic :: Software Development :: Libraries :: Python Modules',
32 | ],
33 | )
34 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0012_auto_20160215_0123.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0011_auto_20160213_0338'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='user',
16 | name='followers_count',
17 | field=models.PositiveIntegerField(null=True, db_index=True),
18 | preserve_default=True,
19 | ),
20 | migrations.AlterField(
21 | model_name='user',
22 | name='follows_count',
23 | field=models.PositiveIntegerField(null=True, db_index=True),
24 | preserve_default=True,
25 | ),
26 | migrations.AlterField(
27 | model_name='user',
28 | name='is_private',
29 | field=models.NullBooleanField(db_index=True, verbose_name=b'Account is private'),
30 | preserve_default=True,
31 | ),
32 | migrations.AlterField(
33 | model_name='user',
34 | name='media_count',
35 | field=models.PositiveIntegerField(null=True, db_index=True),
36 | preserve_default=True,
37 | ),
38 | ]
39 |
--------------------------------------------------------------------------------
/instagram_api/admin.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.contrib import admin
3 | from models import User, Media, Comment
4 |
5 |
6 | class AllFieldsReadOnly(admin.ModelAdmin):
7 | def get_readonly_fields(self, request, obj=None):
8 | if obj:
9 | return [field.name for field in obj._meta.fields]
10 | return []
11 |
12 |
13 | class UserAdmin(AllFieldsReadOnly):
14 | def instagram_link(self, obj):
15 | return u'%s' % (obj.instagram_link, obj.username)
16 |
17 | instagram_link.allow_tags = True
18 |
19 | list_display = ['id', 'full_name', 'instagram_link']
20 | list_filter = ('is_private',)
21 | search_fields = ('username', 'full_name')
22 |
23 | exclude = ('followers',)
24 |
25 |
26 | class MediaAdmin(AllFieldsReadOnly):
27 | def instagram_link(self, obj):
28 | return u'%s' % (obj.link, obj.link)
29 |
30 | instagram_link.allow_tags = True
31 |
32 | list_display = ['id', 'user', 'caption', 'created_time', 'instagram_link']
33 | search_fields = ('caption',)
34 |
35 |
36 | class CommentAdmin(AllFieldsReadOnly):
37 | list_display = ['id', 'user', 'media', 'text', 'created_time']
38 | search_fields = ('text',)
39 |
40 |
41 | class TagAdmin(AllFieldsReadOnly):
42 | list_display = ['id', 'name', 'media_count', 'created_time']
43 | search_fields = ('name',)
44 |
45 |
46 | admin.site.register(User, UserAdmin)
47 | admin.site.register(Media, MediaAdmin)
48 | admin.site.register(Comment, CommentAdmin)
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, krupin.dv and contributors.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | 1. Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | 2. Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in the
12 | documentation and/or other materials provided with the distribution.
13 |
14 | 3. Neither the name of Django nor the names of its contributors may be used
15 | to endorse or promote products derived from this software without
16 | specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Django Instagram API
2 | ====================
3 |
4 | [](https://travis-ci.org/ramusus/django-instagram-api) [](https://coveralls.io/r/ramusus/django-instagram-api)
5 |
6 | Application for interaction with Instagram API objects using Django ORM
7 |
8 | Installation
9 | ------------
10 |
11 | pip install django-instagram-api
12 |
13 | Add into `settings.py` lines:
14 |
15 | INSTALLED_APPS = (
16 | ...
17 | 'oauth_tokens',
18 | 'm2m_history',
19 | 'taggit',
20 | 'instagram_api',
21 | )
22 |
23 | # oauth-tokens settings
24 | OAUTH_TOKENS_HISTORY = True # to keep in DB expired access tokens
25 | OAUTH_TOKENS_TWITTER_CLIENT_ID = '' # application ID
26 | OAUTH_TOKENS_TWITTER_CLIENT_SECRET = '' # application secret key
27 | OAUTH_TOKENS_TWITTER_USERNAME = '' # user login
28 | OAUTH_TOKENS_TWITTER_PASSWORD = '' # user password
29 |
30 | Usage examples
31 | --------------
32 |
33 | ### Simple API request
34 |
35 | >>>from instagram_api.models import User, Media
36 | >>>u = User.remote.fetch(237074561)
37 | >>>print u
38 | tnt_online
39 |
40 | >>>followers = u.fetch_followers()
41 |
42 | >>>medias = u.fetch_recent_media()
43 | >>>print medias
44 | [, , ...]
45 |
46 |
47 | >>>m = Media.remote.fetch('937539904871536462_190931988')
48 | >>>comments = m.fetch_comments()
49 | >>>likes = m.fetch_likes()
50 |
--------------------------------------------------------------------------------
/instagram_api/fields.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.core import validators
3 | from django.db import models
4 | from django.utils.translation import ugettext_lazy as _
5 | import re
6 | #from annoying.fields import JSONField
7 | #from picklefield.fields import PickledObjectField
8 |
9 |
10 |
11 | class PositiveSmallIntegerRangeField(models.PositiveSmallIntegerField):
12 | '''
13 | Range integer field with max_value and min_value properties
14 | from here http://stackoverflow.com/questions/849142/how-to-limit-the-maximum-value-of-a-numeric-field-in-a-django-model
15 | '''
16 | def __init__(self, verbose_name=None, name=None, min_value=None, max_value=None, **kwargs):
17 | self.min_value, self.max_value = min_value, max_value
18 | models.IntegerField.__init__(self, verbose_name, name, **kwargs)
19 |
20 | def formfield(self, **kwargs):
21 | defaults = {'min_value': self.min_value, 'max_value':self.max_value}
22 | defaults.update(kwargs)
23 | return super(PositiveSmallIntegerRangeField, self).formfield(**defaults)
24 |
25 | comma_separated_string_list_re = re.compile(u'^(?u)[\w#\[\], ]+$')
26 | validate_comma_separated_string_list = validators.RegexValidator(comma_separated_string_list_re, _(u'Enter values separated by commas.'), 'invalid')
27 |
28 | class CommaSeparatedCharField(models.CharField):
29 | '''
30 | Field for comma-separated strings
31 | TODO: added max_number validator
32 | '''
33 | default_validators = [validate_comma_separated_string_list]
34 | description = _("Comma-separated strings")
35 |
36 | def formfield(self, **kwargs):
37 | defaults = {
38 | 'error_messages': {
39 | 'invalid': _(u'Enter values separated by commas.'),
40 | }
41 | }
42 | defaults.update(kwargs)
43 | return super(CommaSeparatedCharField, self).formfield(**defaults)
44 |
45 | try:
46 | from south.modelsinspector import add_introspection_rules
47 | add_introspection_rules([], ["^facebook_api\.fields"])
48 | add_introspection_rules([], ["^annoying\.fields"])
49 | except ImportError:
50 | pass
51 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0011_auto_20160213_0338.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 |
6 |
7 | class Migration(migrations.Migration):
8 |
9 | dependencies = [
10 | ('instagram_api', '0010_auto_20160212_1602'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='media',
16 | name='link',
17 | field=models.URLField(max_length=68),
18 | preserve_default=True,
19 | ),
20 | migrations.AlterField(
21 | model_name='media',
22 | name='remote_id',
23 | field=models.CharField(unique=True, max_length=30),
24 | preserve_default=True,
25 | ),
26 | migrations.AlterField(
27 | model_name='media',
28 | name='type',
29 | field=models.CharField(max_length=5),
30 | preserve_default=True,
31 | ),
32 | migrations.AlterField(
33 | model_name='media',
34 | name='video_low_bandwidth',
35 | field=models.URLField(max_length=130),
36 | preserve_default=True,
37 | ),
38 | migrations.AlterField(
39 | model_name='media',
40 | name='video_low_resolution',
41 | field=models.URLField(max_length=130),
42 | preserve_default=True,
43 | ),
44 | migrations.AlterField(
45 | model_name='media',
46 | name='video_standard_resolution',
47 | field=models.URLField(max_length=130),
48 | preserve_default=True,
49 | ),
50 | migrations.AlterField(
51 | model_name='user',
52 | name='bio',
53 | field=models.CharField(max_length=150),
54 | preserve_default=True,
55 | ),
56 | migrations.AlterField(
57 | model_name='user',
58 | name='full_name',
59 | field=models.CharField(max_length=30),
60 | preserve_default=True,
61 | ),
62 | migrations.AlterField(
63 | model_name='user',
64 | name='profile_picture',
65 | field=models.URLField(max_length=112),
66 | preserve_default=True,
67 | ),
68 | migrations.AlterField(
69 | model_name='user',
70 | name='username',
71 | field=models.CharField(unique=True, max_length=30),
72 | preserve_default=True,
73 | ),
74 | migrations.AlterField(
75 | model_name='user',
76 | name='website',
77 | field=models.URLField(max_length=150),
78 | preserve_default=True,
79 | ),
80 | ]
81 |
--------------------------------------------------------------------------------
/instagram_api/factories.py:
--------------------------------------------------------------------------------
1 | import random
2 | import string
3 |
4 | import factory
5 | from django.utils import timezone
6 |
7 | from . import models
8 |
9 |
10 | class UserFactory(factory.DjangoModelFactory):
11 |
12 | id = factory.Sequence(lambda n: n + 1)
13 | username = factory.Sequence(lambda n: "".join([random.choice(string.letters) for _ in xrange(30)]))
14 | # full_name = factory.Sequence(lambda n: n)
15 | # bio = factory.Sequence(lambda n: n)
16 |
17 | followers_count = factory.LazyAttribute(lambda o: random.randint(0, 1000))
18 |
19 | class Meta:
20 | model = models.User
21 |
22 |
23 | class MediaFactory(factory.DjangoModelFactory):
24 |
25 | remote_id = factory.Sequence(lambda n: "".join([random.choice(string.letters) for _ in xrange(30)]))
26 | user = factory.SubFactory(UserFactory)
27 | comments_count = factory.LazyAttribute(lambda o: random.randint(0, 1000))
28 | likes_count = factory.LazyAttribute(lambda o: random.randint(0, 1000))
29 |
30 | created_time = factory.LazyAttribute(lambda o: timezone.now())
31 |
32 | class Meta:
33 | model = models.Media
34 |
35 |
36 | class CommentFactory(factory.DjangoModelFactory):
37 |
38 | id = factory.Sequence(lambda n: n + 1)
39 | owner = factory.SubFactory(UserFactory)
40 | user = factory.SubFactory(UserFactory)
41 | media = factory.SubFactory(MediaFactory)
42 |
43 | created_time = factory.LazyAttribute(lambda o: timezone.now())
44 |
45 | class Meta:
46 | model = models.Comment
47 |
48 |
49 | class TagFactory(factory.DjangoModelFactory):
50 |
51 | id = factory.Sequence(lambda n: n + 1)
52 | name = factory.Sequence(lambda n: "".join([random.choice(string.letters) for _ in xrange(50)]))
53 | media_count = factory.LazyAttribute(lambda o: random.randint(0, 1000))
54 |
55 | @factory.post_generation
56 | def media_feed(self, create, extracted, **kwargs):
57 | if not create:
58 | # Simple build, do nothing.
59 | return
60 |
61 | if extracted:
62 | # A list of groups were passed in, use them
63 | for media in extracted:
64 | self.media_feed.add(media)
65 |
66 | class Meta:
67 | model = models.Tag
68 |
69 |
70 | class LocationFactory(factory.DjangoModelFactory):
71 |
72 | id = factory.Sequence(lambda n: n + 1)
73 | name = factory.Sequence(lambda n: "".join([random.choice(string.letters) for _ in xrange(50)]))
74 | media_count = factory.LazyAttribute(lambda o: random.randint(0, 1000))
75 |
76 | @factory.post_generation
77 | def media_feed(self, create, extracted, **kwargs):
78 | if not create:
79 | # Simple build, do nothing.
80 | return
81 |
82 | if extracted:
83 | # A list of groups were passed in, use them
84 | for media in extracted:
85 | self.media_feed.add(media)
86 |
87 | class Meta:
88 | model = models.Location
89 |
--------------------------------------------------------------------------------
/instagram_api/api.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from time import sleep
3 |
4 | from django.conf import settings
5 | from instagram import InstagramAPIError as InstagramError, InstagramClientError
6 | from instagram.client import InstagramAPI
7 | from social_api.api import ApiAbstractBase, Singleton
8 |
9 | __all__ = ['get_api', ]
10 |
11 | CLIENT_ID = getattr(settings, 'SOCIAL_API_INSTAGRAM_CLIENT_ID')
12 | CLIENT_SECRET = getattr(settings, 'SOCIAL_API_INSTAGRAM_CLIENT_SECRET')
13 |
14 | log = logging.getLogger('instagram_api')
15 |
16 |
17 | @property
18 | def code(self):
19 | return self.status_code
20 |
21 |
22 | InstagramError.code = code
23 |
24 |
25 | class InstagramApi(ApiAbstractBase):
26 | __metaclass__ = Singleton
27 |
28 | provider = 'instagram'
29 | error_class = (InstagramError, InstagramClientError)
30 |
31 | def get_api(self, token):
32 | context = getattr(settings, 'SOCIAL_API_CALL_CONTEXT', None)
33 | if context and self.provider in context and context[self.provider].get('use_client_id', False) is True:
34 | kwargs = {'client_id': CLIENT_ID}
35 | else:
36 | kwargs = {'access_token': token, 'client_secret': CLIENT_SECRET}
37 |
38 | return InstagramAPI(**kwargs)
39 |
40 | def get_api_response(self, *args, **kwargs):
41 | return getattr(self.api, self.method)(*args, **kwargs)
42 |
43 | def handle_error_code_400(self, e, *args, **kwargs):
44 | # OAuthAccessTokenException-The access_token provided is invalid.
45 | if e.error_type in ['OAuthAccessTokenException', 'OAuthPermissionsException']:
46 | self.used_access_tokens += [self.api.access_token]
47 | return self.repeat_call(*args, **kwargs)
48 | raise e
49 |
50 | def handle_error_code_429(self, e, *args, **kwargs):
51 | # Rate limited-Your client is making too many request per second
52 | return self.handle_rate_limit_error(e, *args, **kwargs)
53 |
54 | def handle_error_code_500(self, e, *args, **kwargs):
55 | # InstagramClientError: (500) Unable to parse response, not valid JSON.
56 | return self.sleep_repeat_call(*args, **kwargs)
57 |
58 | def handle_error_code_502(self, e, *args, **kwargs):
59 | # InstagramClientError: (502) Unable to parse response, not valid JSON.
60 | return self.sleep_repeat_call(*args, **kwargs)
61 |
62 | def handle_error_code_503(self, e, *args, **kwargs):
63 | return self.sleep_repeat_call(*args, **kwargs)
64 |
65 | def handle_rate_limit_error(self, e, *args, **kwargs):
66 | self.used_access_tokens += [self.api.access_token]
67 | if len(self.used_access_tokens) == len(self.tokens):
68 | log.warning("All access tokens are rate limited, need to wait 600 sec")
69 | sleep(600)
70 | self.used_access_tokens = []
71 | return self.sleep_repeat_call(*args, **kwargs)
72 |
73 |
74 | def api_call(*args, **kwargs):
75 | api = InstagramApi()
76 | api.used_access_tokens = []
77 | return api.call(*args, **kwargs)
78 |
--------------------------------------------------------------------------------
/instagram_api/graphql.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import time
3 | import simplejson as json
4 | from oauth_tokens.providers.instagram import InstagramAuthRequest
5 |
6 |
7 | class GraphQL(object):
8 |
9 | url = 'https://www.instagram.com/query/'
10 | cookies = '__utma=227057989.1129888433.1423302270.1433340455.1434559808.20; __utmc=227057989; mid=VpvY_gAEAAHrA7w3K-gGUZNO3gUn; sessionid=IGSC7c09257a24e6a71806e9f9dc952d22e2aeca0b9a471b86c84c75ffa10511a509%3Avb2zOhH21hwjUVG4UItD2ruVR09hq2tF%3A%7B%22_token_ver%22%3A2%2C%22_auth_user_id%22%3A1687258424%2C%22_token%22%3A%221687258424%3AWd6qVgJRS6fbkEhrfKM1D5xo2XM8YFHk%3Abaa3b7fd09a33cea01f659a626fc6cb738c577ca896390beb5960043ec113298%22%2C%22asns%22%3A%7B%22188.40.74.9%22%3A24940%2C%22time%22%3A1466597282%7D%2C%22_auth_user_backend%22%3A%22accounts.backends.CaseInsensitiveModelBackend%22%2C%22last_refreshed%22%3A1466597282.193233%2C%22_platform%22%3A4%7D; ig_pr=2; ig_vw=1618; csrftoken=CSRF_TOKEN; s_network=; ds_user_id=1687258424'
11 |
12 | def related_users(self, endpoint, user):
13 | req = InstagramAuthRequest()
14 | session = requests.Session()
15 | url = 'https://www.instagram.com/%s/' % user.username
16 | # response = req.authorized_request('get', url=url)
17 | response = session.get(url=url)
18 | csrf_token = req.get_csrf_token_from_content(response.content)
19 | headers = {
20 | 'Referer': url,
21 | 'X-CSRFToken': csrf_token,
22 | 'Cookie': self.cookies.replace('CSRF_TOKEN', csrf_token),
23 | }
24 |
25 | user_id = user.id
26 | limit = 1000
27 | method = 'first'
28 | args = limit
29 | next_page = True
30 |
31 | while next_page:
32 | graphql = '''
33 | ig_user(%(user_id)s) {
34 | %(endpoint)s.%(method)s(%(args)s) {
35 | count, page_info { end_cursor, has_next_page },
36 | nodes { id, is_verified, followed_by_viewer, requested_by_viewer, full_name, profile_pic_url, username }
37 | }
38 | }''' % locals()
39 |
40 | # response = req.authorized_request('post', url=self.url, data={'q': graphql}, headers=headers)
41 | response = session.post(url=self.url, data={'q': graphql}, headers=headers)
42 | json_response = json.loads(response.content)
43 |
44 | if json_response['status'] == 'fail' \
45 | and json_response['message'] == 'Sorry, too many requests. Please try again later.':
46 | time.sleep(1)
47 | continue
48 |
49 | method = 'after'
50 | try:
51 | args = '%s, %s' % (json_response[endpoint]['page_info']['end_cursor'], limit)
52 | next_page = json_response[endpoint]['page_info']['has_next_page']
53 | # print json_response[endpoint]['count'], len(json_response[endpoint]['nodes']), next_page
54 | yield json_response[endpoint]['nodes']
55 | except KeyError:
56 | raise Exception('Unexpected response: "%s" of graphql request: "%s"' % (json_response, graphql))
57 |
--------------------------------------------------------------------------------
/instagram_api/decorators.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from django.utils.functional import wraps
3 | from django.db.models.query import QuerySet
4 |
5 | try:
6 | from django.db.transaction import atomic
7 | except ImportError:
8 | from django.db.transaction import commit_on_success as atomic
9 |
10 |
11 | def opt_arguments(func):
12 | '''
13 | Meta-decorator for ablity use decorators with optional arguments
14 | from here http://www.ellipsix.net/blog/2010/08/more-python-voodoo-optional-argument-decorators.html
15 | '''
16 | def meta_wrapper(*args, **kwargs):
17 | if len(args) == 1 and callable(args[0]):
18 | # No arguments, this is the decorator
19 | # Set default values for the arguments
20 | return func(args[0])
21 | else:
22 | def meta_func(inner_func):
23 | return func(inner_func, *args, **kwargs)
24 | return meta_func
25 | return meta_wrapper
26 |
27 | @opt_arguments
28 | def fetch_all(func, return_all=None, always_all=False):
29 | """
30 | Class method decorator for fetching all items. Add parameter `all=False` for decored method.
31 | If `all` is True, method runs as many times as it returns any results.
32 | Decorator receive parameters:
33 | * callback method `return_all`. It's called with the same parameters
34 | as decored method after all itmes are fetched.
35 | * `always_all` bool - return all instances in any case of argument `all`
36 | of decorated method
37 | Usage:
38 |
39 | @fetch_all(return_all=lambda self,instance,*a,**k: instance.items.all())
40 | def fetch_something(self, ..., *kwargs):
41 | ....
42 | """
43 | def wrapper(self, all=False, instances_all=None, **kwargs):
44 | response = {}
45 | instances = func(self, **kwargs)
46 | if len(instances) == 2 and isinstance(instances, tuple):
47 | instances, response = instances
48 |
49 | if always_all or all:
50 | if isinstance(instances, QuerySet):
51 | if instances_all is None:
52 | instances_all = instances.none()
53 | instances_count = instances.count()
54 | if instances_count:
55 | instances_all |= instances
56 | elif isinstance(instances, list):
57 | if instances_all is None:
58 | instances_all = []
59 | instances_count = len(instances)
60 | instances_all += instances
61 | else:
62 | raise ValueError("Wrong type of response from func %s. It should be QuerySet or list, not a %s" % (func, type(instances)))
63 |
64 | next_url = response['pagination'].get('next_url', None)
65 | if next_url:
66 | return wrapper(self, all=True, next_url=next_url, instances_all=instances_all, **kwargs)
67 |
68 | if return_all:
69 | kwargs['instances'] = instances_all
70 | return return_all(self, **kwargs)
71 | else:
72 | return instances_all
73 |
74 | else:
75 | return instances
76 |
77 | return wraps(func)(wrapper)
78 |
--------------------------------------------------------------------------------
/instagram_api/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from __future__ import unicode_literals
3 |
4 | from django.db import models, migrations
5 | import m2m_history.fields
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | dependencies = [
11 | ]
12 |
13 | operations = [
14 | migrations.CreateModel(
15 | name='Comment',
16 | fields=[
17 | ('fetched', models.DateTimeField(null=True, verbose_name='Fetched', blank=True)),
18 | ('id', models.BigIntegerField(serialize=False, primary_key=True)),
19 | ('text', models.TextField()),
20 | ('created_time', models.DateTimeField()),
21 | ],
22 | options={
23 | 'abstract': False,
24 | },
25 | bases=(models.Model,),
26 | ),
27 | migrations.CreateModel(
28 | name='Media',
29 | fields=[
30 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
31 | ('fetched', models.DateTimeField(null=True, verbose_name='Fetched', blank=True)),
32 | ('remote_id', models.CharField(unique=True, max_length=100)),
33 | ('caption', models.TextField(blank=True)),
34 | ('link', models.URLField(max_length=300)),
35 | ('type', models.CharField(max_length=20)),
36 | ('image_low_resolution', models.URLField()),
37 | ('image_standard_resolution', models.URLField()),
38 | ('image_thumbnail', models.URLField()),
39 | ('video_low_bandwidth', models.URLField()),
40 | ('video_low_resolution', models.URLField()),
41 | ('video_standard_resolution', models.URLField()),
42 | ('created_time', models.DateTimeField()),
43 | ('comments_count', models.PositiveIntegerField(null=True)),
44 | ('likes_count', models.PositiveIntegerField(null=True)),
45 | ],
46 | options={
47 | 'abstract': False,
48 | },
49 | bases=(models.Model,),
50 | ),
51 | migrations.CreateModel(
52 | name='Tag',
53 | fields=[
54 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
55 | ('fetched', models.DateTimeField(null=True, verbose_name='Fetched', blank=True)),
56 | ('name', models.CharField(unique=True, max_length=29)),
57 | ('media_count', models.PositiveIntegerField(null=True)),
58 | ('media_feed', models.ManyToManyField(related_name='tags', to='instagram_api.Media')),
59 | ],
60 | options={
61 | 'abstract': False,
62 | },
63 | bases=(models.Model,),
64 | ),
65 | migrations.CreateModel(
66 | name='User',
67 | fields=[
68 | ('fetched', models.DateTimeField(null=True, verbose_name='Fetched', blank=True)),
69 | ('id', models.BigIntegerField(serialize=False, primary_key=True)),
70 | ('username', models.CharField(unique=True, max_length=50)),
71 | ('full_name', models.CharField(max_length=255)),
72 | ('bio', models.CharField(max_length=255, verbose_name=b'BIO')),
73 | ('profile_picture', models.URLField(max_length=300)),
74 | ('website', models.URLField(max_length=300)),
75 | ('followers_count', models.PositiveIntegerField(null=True)),
76 | ('media_count', models.PositiveIntegerField(null=True)),
77 | ('followers', m2m_history.fields.ManyToManyHistoryField(to='instagram_api.User')),
78 | ],
79 | options={
80 | 'abstract': False,
81 | },
82 | bases=(models.Model,),
83 | ),
84 | migrations.AddField(
85 | model_name='media',
86 | name='likes_users',
87 | field=m2m_history.fields.ManyToManyHistoryField(related_name='likes_media', to='instagram_api.User'),
88 | preserve_default=True,
89 | ),
90 | migrations.AddField(
91 | model_name='media',
92 | name='user',
93 | field=models.ForeignKey(related_name='media_feed', to='instagram_api.User'),
94 | preserve_default=True,
95 | ),
96 | migrations.AddField(
97 | model_name='comment',
98 | name='media',
99 | field=models.ForeignKey(related_name='comments', to='instagram_api.Media'),
100 | preserve_default=True,
101 | ),
102 | migrations.AddField(
103 | model_name='comment',
104 | name='owner',
105 | field=models.ForeignKey(related_name='media_comments', to='instagram_api.User'),
106 | preserve_default=True,
107 | ),
108 | migrations.AddField(
109 | model_name='comment',
110 | name='user',
111 | field=models.ForeignKey(related_name='comments', to='instagram_api.User'),
112 | preserve_default=True,
113 | ),
114 | ]
115 |
--------------------------------------------------------------------------------
/quicktest.py:
--------------------------------------------------------------------------------
1 | """
2 | QuickDjangoTest module for testing in Travis CI https://travis-ci.org
3 | Changes log:
4 | * 2014-10-24 updated for compatibility with Django 1.7
5 | * 2014-11-03 different databases support: sqlite3, mysql, postgres
6 | * 2014-12-31 pep8, python 3 compatibility
7 | * 2015-02-01 Django 1.9 compatibility
8 | * 2015-02-25 updated code style
9 | * 2015-02-26 updated get_database() for Django 1.8
10 | * 2015-02-27 clean up variables
11 | """
12 |
13 | import argparse
14 | import os
15 | import sys
16 |
17 | from django.conf import settings
18 |
19 |
20 | class QuickDjangoTest(object):
21 |
22 | """
23 | A quick way to run the Django test suite without a fully-configured project.
24 |
25 | Example usage:
26 |
27 | >>> QuickDjangoTest('app1', 'app2')
28 |
29 | Based on a script published by Lukasz Dziedzia at:
30 | http://stackoverflow.com/questions/3841725/how-to-launch-tests-for-django-reusable-app
31 | """
32 | DIRNAME = os.path.dirname(__file__)
33 | INSTALLED_APPS = (
34 | 'django.contrib.auth',
35 | 'django.contrib.contenttypes',
36 | 'django.contrib.sessions',
37 | 'django.contrib.admin',
38 | )
39 |
40 | def __init__(self, *args):
41 | self.apps = args
42 |
43 | # Call the appropriate one
44 | method = getattr(self, '_tests_%s' % self.version.replace('.', '_'), None)
45 | if method and callable(method):
46 | method()
47 | else:
48 | self._tests_old()
49 |
50 | @property
51 | def version(self):
52 | """
53 | Figure out which version of Django's test suite we have to play with.
54 | """
55 | from django import VERSION
56 | if VERSION[0] == 1 and VERSION[1] >= 8:
57 | return '1.8'
58 | elif VERSION[0] == 1 and VERSION[1] >= 7:
59 | return '1.7'
60 | elif VERSION[0] == 1 and VERSION[1] >= 2:
61 | return '1.2'
62 | else:
63 | return
64 |
65 | def get_database(self, version):
66 | test_db = os.environ.get('DB', 'sqlite')
67 | if test_db == 'mysql':
68 | database = {
69 | 'ENGINE': 'django.db.backends.mysql',
70 | 'NAME': 'django',
71 | 'USER': 'root',
72 | }
73 | elif test_db == 'postgres':
74 | database = {
75 | 'ENGINE': 'django.db.backends.postgresql_psycopg2',
76 | 'USER': 'postgres',
77 | 'NAME': 'django',
78 | }
79 | if version < 1.8:
80 | database['OPTIONS'] = {'autocommit': True}
81 | else:
82 | database = {
83 | 'ENGINE': 'django.db.backends.sqlite3',
84 | 'NAME': os.path.join(self.DIRNAME, 'database.db'),
85 | 'USER': '',
86 | 'PASSWORD': '',
87 | 'HOST': '',
88 | 'PORT': '',
89 | }
90 | return {'default': database}
91 |
92 | @property
93 | def custom_settings(self):
94 | """
95 | Return custom settings from settings_test.py file
96 | :return: dict
97 | """
98 | try:
99 | import settings_test
100 | test_settings = dict([(k, v) for k, v in settings_test.__dict__.items() if k[0] != '_'])
101 | except ImportError:
102 | test_settings = {'INSTALLED_APPS': []}
103 | return test_settings
104 |
105 | def _tests_old(self):
106 | """
107 | Fire up the Django test suite from before version 1.2
108 | """
109 | test_settings = self.custom_settings
110 | installed_apps = test_settings.pop('INSTALLED_APPS', ())
111 | settings.configure(
112 | DEBUG=True,
113 | DATABASE_ENGINE='sqlite3',
114 | DATABASE_NAME=os.path.join(self.DIRNAME, 'database.db'),
115 | INSTALLED_APPS=tuple(self.INSTALLED_APPS + installed_apps + self.apps),
116 | **test_settings
117 | )
118 | from django.test.simple import run_tests
119 | failures = run_tests(self.apps, verbosity=1)
120 | if failures:
121 | sys.exit(failures)
122 |
123 | def _tests_1_2(self):
124 | """
125 | Fire up the Django test suite developed for version 1.2 and up
126 | """
127 | test_settings = self.custom_settings
128 | installed_apps = test_settings.pop('INSTALLED_APPS', ())
129 | settings.configure(
130 | DEBUG=True,
131 | DATABASES=self.get_database(1.2),
132 | INSTALLED_APPS=tuple(self.INSTALLED_APPS + installed_apps + self.apps),
133 | **test_settings
134 | )
135 | from django.test.simple import DjangoTestSuiteRunner
136 | failures = DjangoTestSuiteRunner().run_tests(self.apps, verbosity=1)
137 | if failures:
138 | sys.exit(failures)
139 |
140 | def _tests_1_7(self):
141 | """
142 | Fire up the Django test suite developed for version 1.7 and up
143 | """
144 | test_settings = self.custom_settings
145 | installed_apps = test_settings.pop('INSTALLED_APPS', ())
146 | settings.configure(
147 | DEBUG=True,
148 | DATABASES=self.get_database(1.7),
149 | MIDDLEWARE_CLASSES=('django.middleware.common.CommonMiddleware',
150 | 'django.middleware.csrf.CsrfViewMiddleware'),
151 | INSTALLED_APPS=tuple(self.INSTALLED_APPS + installed_apps + self.apps),
152 | **test_settings
153 | )
154 | from django.test.simple import DjangoTestSuiteRunner
155 | import django
156 | django.setup()
157 | failures = DjangoTestSuiteRunner().run_tests(self.apps, verbosity=1)
158 | if failures:
159 | sys.exit(failures)
160 |
161 | def _tests_1_8(self):
162 | """
163 | Fire up the Django test suite developed for version 1.8 and up
164 | """
165 | test_settings = self.custom_settings
166 | installed_apps = test_settings.pop('INSTALLED_APPS', ())
167 | settings.configure(
168 | DEBUG=True,
169 | DATABASES=self.get_database(1.8),
170 | MIDDLEWARE_CLASSES=('django.middleware.common.CommonMiddleware',
171 | 'django.middleware.csrf.CsrfViewMiddleware'),
172 | INSTALLED_APPS=tuple(self.INSTALLED_APPS + installed_apps + self.apps),
173 | **test_settings
174 | )
175 | from django.test.runner import DiscoverRunner
176 | import django
177 | django.setup()
178 | failures = DiscoverRunner().run_tests(self.apps, verbosity=1)
179 | if failures:
180 | sys.exit(failures)
181 |
182 |
183 | if __name__ == '__main__':
184 | """
185 | What do when the user hits this file from the shell.
186 |
187 | Example usage:
188 |
189 | $ python quicktest.py app1 app2
190 |
191 | """
192 | parser = argparse.ArgumentParser(
193 | usage="[args]",
194 | description="Run Django tests on the provided applications."
195 | )
196 | parser.add_argument('apps', nargs='+', type=str)
197 | QuickDjangoTest(*parser.parse_args().apps)
198 |
--------------------------------------------------------------------------------
/instagram_api/tests.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from datetime import datetime
3 |
4 | from django.test import TestCase
5 | from django.conf import settings
6 | from django.utils import timezone
7 |
8 | from .factories import UserFactory, LocationFactory
9 | from .models import Media, User, Tag, Location
10 | from .api import InstagramError
11 |
12 |
13 | USER_ID = 237074561 # tnt_online
14 | USER_PRIVATE_ID = 176980649
15 | USER_ID_2 = 775667951 # about 200 media
16 | USER_ID_3 = 1741896487 # about 400 followers
17 | MEDIA_ID = '934625295371059186_205828054'
18 | MEDIA_ID_2 = '806703315661297054_190931988' # media without caption
19 | LOCATION_ID = 1
20 | TAG_NAME = "snowyday"
21 | TAG_SEARCH_NAME = "snowy"
22 | LOCATION_SEARCH_NAME = "Dog Patch Labs"
23 |
24 | TOKEN = '1687258424.0fdde74.9badafabca4e49df90da02798db6bf02'
25 | INSTAGRAM_USERNAME = 'atsepk'
26 | INSTAGRAM_PASSWORD = 'Jh6#dFwEHc'
27 |
28 |
29 | class InstagramApiTestCase(TestCase):
30 |
31 | _settings = None
32 |
33 | def setUp(self):
34 | context = getattr(settings, 'SOCIAL_API_CALL_CONTEXT', {})
35 | self._settings = dict(context)
36 | context.update({'instagram': {'token': TOKEN}})
37 |
38 | def tearDown(self):
39 | setattr(settings, 'SOCIAL_API_CALL_CONTEXT', self._settings)
40 |
41 |
42 | class UserTest(InstagramApiTestCase):
43 |
44 | def setUp(self):
45 | super(UserTest, self).setUp()
46 | self.time = timezone.now()
47 |
48 | def test_fetch_user_by_name(self):
49 |
50 | u = User.remote.get_by_slug('tnt_online')
51 |
52 | self.assertEqual(int(u.id), USER_ID)
53 | self.assertEqual(u.username, 'tnt_online')
54 | self.assertEqual(u.full_name, u'Телеканал ТНТ')
55 | self.assertGreater(len(u.profile_picture), 0)
56 | self.assertGreater(len(u.website), 0)
57 |
58 | def test_search_users(self):
59 |
60 | users = User.remote.search('tnt_online')
61 |
62 | self.assertGreater(len(users), 0)
63 | for user in users:
64 | self.assertIsInstance(user, User)
65 |
66 | def test_fetch_user(self):
67 | u = User.remote.fetch(USER_ID)
68 |
69 | self.assertEqual(int(u.id), USER_ID)
70 | self.assertEqual(u.username, 'tnt_online')
71 | self.assertEqual(u.full_name, u'Телеканал ТНТ')
72 | self.assertGreater(len(u.profile_picture), 0)
73 | self.assertGreater(len(u.website), 0)
74 |
75 | self.assertGreater(u.followers_count, 0)
76 | self.assertGreater(u.follows_count, 0)
77 | self.assertGreater(u.media_count, 0)
78 |
79 | self.assertGreater(u.fetched, self.time)
80 |
81 | u.followers_count = None
82 | u.save()
83 | self.assertIsNone(u.followers_count)
84 |
85 | u.refresh()
86 | self.assertGreater(u.followers_count, 0)
87 |
88 | u = User.objects.get(id=u.id)
89 | self.assertGreater(u.followers_count, 0)
90 |
91 | def test_fetch_user_follows_graphql(self):
92 | u = User.remote.fetch(USER_ID_3)
93 | self.assertEqual(u.is_private, False)
94 |
95 | settings_temp = dict(OAUTH_TOKENS_INSTAGRAM_USERNAME=INSTAGRAM_USERNAME,
96 | OAUTH_TOKENS_INSTAGRAM_PASSWORD=INSTAGRAM_PASSWORD)
97 |
98 | with self.settings(**settings_temp):
99 | users = u.fetch_follows(source='graphql')
100 |
101 | self.assertGreaterEqual(u.follows_count, 996)
102 | self.assertEqual(u.follows_count, users.count())
103 | self.assertEqual(u.follows_count, User.objects.count() - 1)
104 |
105 | def test_fetch_user_followers_graphql(self):
106 | u = User.remote.fetch(USER_ID_3)
107 | self.assertEqual(u.is_private, False)
108 |
109 | settings_temp = dict(OAUTH_TOKENS_INSTAGRAM_USERNAME=INSTAGRAM_USERNAME,
110 | OAUTH_TOKENS_INSTAGRAM_PASSWORD=INSTAGRAM_PASSWORD)
111 |
112 | with self.settings(**settings_temp):
113 | users = u.fetch_followers(source='graphql')
114 |
115 | self.assertGreaterEqual(u.follows_count, 754)
116 | self.assertEqual(u.followers_count, users.count())
117 | self.assertEqual(u.followers_count, User.objects.count() - 1)
118 |
119 | def test_fetch_user_follows(self):
120 | u = User.remote.fetch(USER_ID_3)
121 | self.assertEqual(u.is_private, False)
122 | users = u.fetch_follows()
123 |
124 | self.assertGreaterEqual(u.follows_count, 970)
125 | self.assertEqual(u.follows_count, users.count())
126 |
127 | def test_fetch_user_followers(self):
128 | u = User.remote.fetch(USER_ID_3)
129 | self.assertEqual(u.is_private, False)
130 | users = u.fetch_followers()
131 |
132 | self.assertGreaterEqual(u.followers_count, 750)
133 | self.assertEqual(u.followers_count, users.count())
134 |
135 | # check counts for any first public follower
136 | for f in users:
137 | self.assertIsNone(f.followers_count)
138 | self.assertIsNone(f.follows_count)
139 | self.assertIsNone(f.media_count)
140 |
141 | f = User.remote.fetch(f.id)
142 | if f.is_private is False:
143 | self.assertIsNotNone(f.followers_count)
144 | self.assertIsNotNone(f.follows_count)
145 | self.assertIsNotNone(f.media_count)
146 | break
147 |
148 | # fetch followers once again and check counts
149 | u.fetch_followers()
150 | f = User.objects.get(id=f.id)
151 | self.assertIsNotNone(f.followers_count)
152 | self.assertIsNotNone(f.follows_count)
153 | self.assertIsNotNone(f.media_count)
154 |
155 | def test_fetch_users_with_full_name_bad_overlength(self):
156 | user = User.remote.get_by_slug('stasplot')
157 | self.assertEqual(user.full_name, u'Stas from Ishim Ишим Тюмень Tymen region Тюмень') # noqa
158 | user = User.remote.fetch(47274770)
159 | self.assertEqual(user.full_name, u'Stas from Ishim Ишим Тюмень Ty')
160 |
161 | user = User.remote.get_by_slug('keratin_krasnodar1')
162 | self.assertEqual(user.full_name, u'Кератин, Ботокс в Краснодаре \ud83c\udf80') # noqa
163 | user = User.remote.fetch(2057367004)
164 | self.assertEqual(user.full_name, u'Кератин, Ботокс в Краснодаре ')
165 |
166 | user = User.remote.get_by_slug('beautypageantsfans')
167 | self.assertEqual(user.full_name, u'I Am A Girl \xbfAnd What?\ud83d\udc81\ud83c\udffb\u2728\ud83d\udc51\ud83d\udc8b') # noqa
168 | user = User.remote.fetch(1164190771)
169 | self.assertEqual(user.full_name, u'I Am A Girl \xbfAnd What?\ud83d\udc81\ud83c\udffb\u2728\ud83d\udc51')
170 |
171 | def test_fetch_duplicate_user(self):
172 |
173 | u = UserFactory(id=0, username='tnt_online')
174 |
175 | self.assertEqual(User.objects.count(), 1)
176 | self.assertNotEqual(int(u.id), USER_ID)
177 | self.assertEqual(u.username, 'tnt_online')
178 |
179 | u = User.remote.fetch(USER_ID)
180 |
181 | self.assertEqual(User.objects.count(), 1)
182 | self.assertEqual(int(u.id), USER_ID)
183 | self.assertEqual(u.username, 'tnt_online')
184 |
185 | def test_fetch_duble_duplicate_user(self):
186 |
187 | u1 = UserFactory(username='tnt_online', id=8910216)
188 | u2 = UserFactory(username='bmwru', id=237074561)
189 |
190 | self.assertEqual(User.objects.count(), 2)
191 | self.assertEqual(int(u1.id), 8910216)
192 | self.assertEqual(int(u2.id), 237074561)
193 | self.assertEqual(u1.username, 'tnt_online')
194 | self.assertEqual(u2.username, 'bmwru')
195 |
196 | u1 = User.remote.fetch(8910216)
197 | u2 = User.remote.fetch(237074561)
198 |
199 | self.assertEqual(User.objects.count(), 2)
200 | self.assertEqual(int(u1.id), 8910216)
201 | self.assertEqual(int(u2.id), 237074561)
202 | self.assertEqual(u1.username, 'bmwru')
203 | self.assertEqual(u2.username, 'tnt_online')
204 |
205 | def test_fetch_real_duplicates_user(self):
206 |
207 | UserFactory(id=2116301016)
208 | User.remote.fetch(2116301016)
209 |
210 | with self.assertRaises(InstagramError):
211 | User.remote.get(1206219929)
212 |
213 | try:
214 | User.remote.get(1206219929)
215 | except InstagramError as e:
216 | self.assertEqual(e.code, 400)
217 |
218 | def test_fetch_private_user(self):
219 |
220 | with self.assertRaises(InstagramError):
221 | User.remote.fetch(USER_PRIVATE_ID)
222 |
223 | try:
224 | User.remote.fetch(USER_PRIVATE_ID)
225 | except InstagramError as e:
226 | self.assertEqual(e.code, 400)
227 |
228 | userf = UserFactory(id=USER_PRIVATE_ID)
229 | user = User.remote.fetch(USER_PRIVATE_ID)
230 |
231 | self.assertEqual(userf, user)
232 | self.assertFalse(userf.is_private)
233 | self.assertTrue(user.is_private)
234 |
235 | userf.refresh()
236 | self.assertTrue(userf.is_private)
237 |
238 | def test_unexisted_user(self):
239 | with self.assertRaises(InstagramError):
240 | User.remote.get(0)
241 |
242 | try:
243 | User.remote.get(0)
244 | except InstagramError as e:
245 | self.assertEqual(e.code, 400)
246 |
247 |
248 | class MediaTest(InstagramApiTestCase):
249 |
250 | def setUp(self):
251 | super(MediaTest, self).setUp()
252 | self.time = timezone.now()
253 |
254 | def test_fetch_media(self):
255 | m = Media.remote.fetch(MEDIA_ID)
256 |
257 | self.assertEqual(m.remote_id, MEDIA_ID)
258 | self.assertGreater(len(m.caption), 0)
259 | self.assertGreater(len(m.link), 0)
260 |
261 | self.assertGreater(m.comments_count, 0)
262 | self.assertGreater(m.likes_count, 0)
263 |
264 | self.assertGreater(m.fetched, self.time)
265 | self.assertIsInstance(m.created_time, datetime)
266 |
267 | # specifying timezone and then making it naive again
268 | self.assertEqual(m.created_time, timezone.make_aware(m.created_time, timezone.get_current_timezone()).replace(tzinfo=None))
269 |
270 | self.assertEqual(m.type, 'video')
271 | self.assertEqual(m.filter, 'Normal')
272 |
273 | self.assertGreater(len(m.image_low_resolution), 0)
274 | self.assertGreater(len(m.image_standard_resolution), 0)
275 | self.assertGreater(len(m.image_thumbnail), 0)
276 | self.assertGreater(len(m.video_low_bandwidth), 0)
277 | self.assertGreater(len(m.video_low_resolution), 0)
278 | self.assertGreater(len(m.video_standard_resolution), 0)
279 |
280 | self.assertGreater(m.comments.count(), 0)
281 | self.assertGreater(m.tags.count(), 0)
282 | # self.assertGreater(m.likes_users.count(), 0)
283 |
284 | # media without caption test
285 | m = Media.remote.fetch(MEDIA_ID_2)
286 | self.assertEqual(len(m.caption), 0)
287 |
288 | self.assertEqual(m.type, 'image')
289 |
290 | self.assertGreater(len(m.image_low_resolution), 0)
291 | self.assertGreater(len(m.image_standard_resolution), 0)
292 | self.assertGreater(len(m.image_thumbnail), 0)
293 |
294 | self.assertGreater(m.comments.count(), 0)
295 | # self.assertGreater(m.likes_users.count(), 0)
296 |
297 | def test_fetch_user_media_count(self):
298 | u = UserFactory(id=USER_ID)
299 |
300 | media = u.fetch_media(count=100)
301 | m = media[0]
302 |
303 | self.assertEqual(media.count(), 100)
304 | self.assertEqual(m.user, u)
305 |
306 | self.assertGreater(len(m.caption), 0)
307 | self.assertGreater(len(m.link), 0)
308 |
309 | self.assertGreater(m.comments_count, 0)
310 | self.assertGreater(m.likes_count, 0)
311 |
312 | self.assertGreater(m.fetched, self.time)
313 | self.assertIsInstance(m.created_time, datetime)
314 |
315 | def test_fetch_user_media(self):
316 | u = User.remote.fetch(USER_ID_2)
317 | media = u.fetch_media()
318 |
319 | self.assertGreater(media.count(), 210)
320 | self.assertEqual(media.count(), u.media_count)
321 | self.assertEqual(media.count(), u.media_feed.count())
322 |
323 | after = media.order_by('-created_time')[50].created_time
324 | Media.objects.all().delete()
325 |
326 | self.assertEqual(u.media_feed.count(), 0)
327 |
328 | media = u.fetch_media(after=after)
329 |
330 | self.assertEqual(media.count(), 53) # not 50 for some strange reason
331 | self.assertEqual(media.count(), u.media_feed.count())
332 |
333 | def test_fetch_media_with_location(self):
334 |
335 | media = Media.remote.fetch('1105137931436928268_1692711770')
336 |
337 | self.assertIsInstance(media.location, Location)
338 | self.assertEqual(media.location.name, 'Prague, Czech Republic')
339 |
340 | def test_fetch_comments(self):
341 | m = Media.remote.fetch(MEDIA_ID)
342 | comments = m.fetch_comments()
343 |
344 | self.assertGreater(m.comments_count, 0)
345 | # TODO: 84 != 80 strange bug of API, may be limit of comments to fetch
346 | # self.assertEqual(m.comments_count, len(comments))
347 |
348 | c = comments[0]
349 | self.assertEqual(c.media, m)
350 | self.assertGreater(len(c.text), 0)
351 | self.assertGreater(c.fetched, self.time)
352 | self.assertIsInstance(c.created_time, datetime)
353 |
354 | def test_fetch_likes(self):
355 | m = Media.remote.fetch(MEDIA_ID)
356 | likes = m.fetch_likes()
357 |
358 | self.assertGreater(m.likes_count, 0)
359 | # TODO: 2515 != 117 how to get all likes?
360 | # self.assertEqual(m.likes_count, likes.count())
361 | self.assertIsInstance(likes[0], User)
362 |
363 |
364 | class TagTest(InstagramApiTestCase):
365 | def test_fetch_tag(self):
366 | t = Tag.remote.fetch(TAG_NAME)
367 |
368 | self.assertEqual(t.name, TAG_NAME)
369 | self.assertGreater(t.media_count, 0)
370 |
371 | def test_search_tags(self):
372 |
373 | tags = Tag.remote.search(TAG_SEARCH_NAME)
374 |
375 | self.assertGreater(len(tags), 0)
376 | for tag in tags:
377 | self.assertIsInstance(tag, Tag)
378 |
379 | def test_fetch_tag_media(self):
380 | t = Tag.remote.fetch("merrittislandnwr")
381 | media = t.fetch_media()
382 |
383 | self.assertGreater(media.count(), 0)
384 | self.assertEqual(media.count(), t.media_feed.count())
385 |
386 |
387 | class LocationTest(InstagramApiTestCase):
388 | def test_fetch_location(self):
389 | location = Location.remote.fetch(LOCATION_ID)
390 |
391 | self.assertEqual(location.id, LOCATION_ID)
392 | self.assertEqual(location.name, "Dog Patch Labs")
393 | self.assertEqual(location.latitude, 37.782492553)
394 | self.assertEqual(location.longitude, -122.387785235)
395 | self.assertEqual(location.media_count, None)
396 |
397 | def test_search_locations(self):
398 | locations = Location.remote.search(lat=37.782492553, lng=-122.387785235)
399 |
400 | self.assertGreater(len(locations), 0)
401 | for location in locations:
402 | self.assertIsInstance(location, Location)
403 |
404 | def test_fetch_location_media(self):
405 | location = LocationFactory(id=LOCATION_ID)
406 | media = location.fetch_media()
407 |
408 | self.assertGreater(media.count(), 0)
409 | self.assertEqual(media.count(), location.media_feed.count())
410 | self.assertEqual(media.count(), location.media_count)
411 |
412 |
413 | # class InstagramApiTest(UserTest, MediaTest):
414 | # def call(api, *a, **kw):
415 | # raise InstagramAPIError(503, "Rate limited", "Your client is making too many request per second")
416 | #
417 | # @mock.patch('instagram.client.InstagramAPI.user', side_effect=call)
418 | # @mock.patch('instagram_api.api.InstagramApi.repeat_call',
419 | # side_effect=lambda *a, **kw: models.User.object_from_dictionary({'id': '205828054'}))
420 | # def test_client_rate_limit(self, call, repeat_call):
421 | # self.assertGreaterEqual(len(CLIENT_IDS), 2)
422 | # User.remote.fetch(USER_ID_2)
423 | # self.assertEqual(call.called, True)
424 | # self.assertEqual(repeat_call.called, True)
425 |
--------------------------------------------------------------------------------
/instagram_api/models.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from datetime import datetime
3 | import calendar
4 | import logging
5 | import re
6 | import time
7 | import sys
8 | import six
9 |
10 | from django.db import models
11 | from django.db.models.fields import FieldDoesNotExist
12 | from django.db.utils import IntegrityError
13 | from django.utils import timezone
14 | from instagram.helper import timestamp_to_datetime
15 | from instagram.models import ApiModel
16 | from m2m_history.fields import ManyToManyHistoryField
17 | from social_api.utils import override_api_context
18 |
19 | from . import fields
20 | from .api import api_call, InstagramError
21 | from .decorators import atomic
22 | from .graphql import GraphQL
23 |
24 | try:
25 | from django.db.models.related import RelatedObject as ForeignObjectRel
26 | except ImportError:
27 | # django 1.8 +
28 | from django.db.models.fields.related import ForeignObjectRel
29 |
30 | __all__ = ['User', 'Media', 'Comment', 'InstagramContentError', 'InstagramModel', 'InstagramManager', 'UserManager'
31 | 'Tag', 'TagManager']
32 |
33 | log = logging.getLogger('instagram_api')
34 |
35 |
36 | class InstagramContentError(Exception):
37 | pass
38 |
39 |
40 | class InstagramManager(models.Manager):
41 | """
42 | Instagram Manager for RESTful CRUD operations
43 | """
44 | def __init__(self, methods=None, remote_pk=None, *args, **kwargs):
45 | if methods and len(methods.items()) < 1:
46 | raise ValueError('Argument methods must contains at least 1 specified method')
47 |
48 | self.methods = methods or {}
49 | self.remote_pk = remote_pk or ('id',)
50 | if not isinstance(self.remote_pk, tuple):
51 | self.remote_pk = (self.remote_pk,)
52 |
53 | super(InstagramManager, self).__init__(*args, **kwargs)
54 |
55 | def bulk_create_from_instances(self, instances):
56 | ids = [instance.id for instance in instances]
57 | ids_exists = self.model.objects.filter(id__in=ids).values_list('id', flat=True)
58 | instances = [instance for instance in instances if instance.id not in ids_exists]
59 | self.model.objects.bulk_create(instances)
60 |
61 | def get_or_create_from_instance(self, instance):
62 |
63 | remote_pk_dict = {}
64 | for field_name in self.remote_pk:
65 | remote_pk_dict[field_name] = getattr(instance, field_name)
66 |
67 | try:
68 | old_instance = self.model.objects.get(**remote_pk_dict)
69 | instance._substitute(old_instance)
70 | instance.save()
71 | except self.model.DoesNotExist:
72 | instance.save()
73 | log.debug('Fetch and create new object %s with remote pk %s' % (self.model, remote_pk_dict))
74 |
75 | return instance
76 |
77 | def api_call(self, method, *args, **kwargs):
78 | if method in self.methods:
79 | method = self.methods[method]
80 | return api_call(method, *args, **kwargs)
81 |
82 | def fetch(self, *args, **kwargs):
83 | """
84 | Retrieve and save object to local DB
85 | """
86 | result = self.get(*args, **kwargs)
87 | if isinstance(result, list):
88 | instances = self.model.objects.none()
89 | for instance in result:
90 | instance = self.get_or_create_from_instance(instance)
91 | instances |= instance.__class__.objects.filter(pk=instance.pk)
92 | return instances
93 | else:
94 | return self.get_or_create_from_instance(result)
95 |
96 | def get(self, *args, **kwargs):
97 | """
98 | Retrieve objects from remote server
99 | """
100 | response = self.api_call('get', *args, **kwargs)
101 |
102 | extra_fields = kwargs.pop('extra_fields', {})
103 | extra_fields['fetched'] = timezone.now()
104 | return self.parse_response(response, extra_fields)
105 |
106 | def parse_response(self, response, extra_fields=None):
107 | if isinstance(response, (list, tuple)):
108 | return self.parse_response_list(response, extra_fields)
109 | elif isinstance(response, ApiModel):
110 | return self.parse_response_object(response, extra_fields)
111 | else:
112 | raise InstagramContentError('Instagram response should be list or ApiModel, not %s' % response)
113 |
114 | def parse_response_object(self, resource, extra_fields=None):
115 |
116 | instance = self.model()
117 | # important to do it before calling parse method
118 | if extra_fields:
119 | instance.__dict__.update(extra_fields)
120 | instance._response = resource.__dict__ if isinstance(resource, ApiModel) else resource
121 | instance.parse()
122 |
123 | return instance
124 |
125 | def parse_response_list(self, response_list, extra_fields=None):
126 |
127 | instances = []
128 | for response in response_list:
129 |
130 | if not isinstance(response, ApiModel):
131 | log.error("Resource %s is not dictionary" % response)
132 | continue
133 |
134 | instance = self.parse_response_object(response, extra_fields)
135 | instances += [instance]
136 |
137 | return instances
138 |
139 |
140 | class InstagramModel(models.Model):
141 |
142 | class Meta:
143 | abstract = True
144 |
145 | def save(self, *args, **kwargs):
146 | # cut all CharFields to max allowed length
147 | cut = False
148 | for field in self._meta.fields:
149 | if isinstance(field, (models.CharField, models.TextField)):
150 | value = getattr(self, field.name)
151 | if isinstance(field, models.CharField) and value:
152 | if len(value) > field.max_length:
153 | value = value[:field.max_length]
154 | cut = True
155 | if isinstance(value, six.string_types):
156 | # check strings for bad symbols in string encoding
157 | # there is problems to save users with bad encoded strings
158 | while True:
159 | try:
160 | value.encode('utf-16').decode('utf-16')
161 | break
162 | except UnicodeDecodeError:
163 | if cut and len(value) > 2:
164 | value = value[:-1]
165 | else:
166 | value = ''
167 | break
168 | setattr(self, field.name, value)
169 |
170 | try:
171 | super(InstagramModel, self).save(*args, **kwargs)
172 | except Exception as e:
173 | six.reraise(type(e), '%s while saving %s' % (str(e), self.__dict__), sys.exc_info()[2])
174 |
175 |
176 | class InstagramBaseModel(InstagramModel):
177 | _refresh_pk = 'id'
178 |
179 | fetched = models.DateTimeField(u'Fetched', null=True, blank=True)
180 | objects = models.Manager()
181 |
182 | class Meta:
183 | abstract = True
184 |
185 | def __init__(self, *args, **kwargs):
186 | super(InstagramBaseModel, self).__init__(*args, **kwargs)
187 |
188 | # different lists for saving related objects
189 | self._relations_post_save = {'fk': {}, 'm2m': {}}
190 | self._relations_pre_save = []
191 | self._tweepy_model = None
192 | self._response = {}
193 |
194 | def _substitute(self, old_instance):
195 | """
196 | Substitute new user with old one while updating in method Manager.get_or_create_from_instance()
197 | Can be overrided in child models
198 | """
199 | self.pk = old_instance.pk
200 |
201 | def save(self, *args, **kwargs):
202 | """
203 | Save all related instances before or after current instance
204 | """
205 | for field, instance in self._relations_pre_save:
206 | instance = instance.__class__.remote.get_or_create_from_instance(instance)
207 | setattr(self, field, instance)
208 | self._relations_pre_save = []
209 |
210 | try:
211 | super(InstagramBaseModel, self).save(*args, **kwargs)
212 | except Exception as e:
213 | six.reraise(type(e), '%s while saving %s' % (str(e), self.__dict__), sys.exc_info()[2])
214 |
215 | def parse(self):
216 | """
217 | Parse API response and define fields with values
218 | """
219 | for key, value in self._response.items():
220 | if key == '_api':
221 | continue
222 |
223 | try:
224 | field, model, direct, m2m = self._meta.get_field_by_name(key)
225 | except FieldDoesNotExist:
226 | log.debug('Field with name "%s" doesn\'t exist in the model %s' % (key, type(self)))
227 | continue
228 |
229 | if value:
230 | if isinstance(field, ForeignObjectRel):
231 | self._relations_post_save['fk'][key] = [field.model.remote.parse_response_object(item)
232 | for item in value]
233 | elif isinstance(field, models.ManyToManyField):
234 | self._relations_post_save['m2m'][key] = [field.rel.to.remote.parse_response_object(item)
235 | for item in value]
236 | else:
237 | if isinstance(field, models.BooleanField):
238 | value = bool(value)
239 |
240 | elif isinstance(field, (models.OneToOneField, models.ForeignKey)) and value:
241 | rel_instance = field.rel.to.remote.parse_response_object(value)
242 | value = rel_instance
243 | if isinstance(field, models.ForeignKey):
244 | self._relations_pre_save += [(key, rel_instance)]
245 |
246 | elif isinstance(field, (fields.CommaSeparatedCharField,
247 | models.CommaSeparatedIntegerField)) and isinstance(value, list):
248 | value = ','.join([unicode(v) for v in value])
249 |
250 | elif isinstance(field, (models.CharField, models.TextField)) and value:
251 | if isinstance(value, (str, unicode)):
252 | value = value.strip()
253 |
254 | elif isinstance(field, models.IntegerField) and value:
255 | value = int(value)
256 |
257 | setattr(self, key, value)
258 |
259 | def get_url(self):
260 | return 'https://instagram.com/%s' % self.slug
261 |
262 | def refresh(self):
263 | """
264 | Refresh current model with remote data
265 | """
266 | instance = self.__class__.remote.fetch(getattr(self, self._refresh_pk))
267 | self.__dict__.update(instance.__dict__)
268 |
269 |
270 | class InstagramSearchManager(InstagramManager):
271 | def search(self, q=None, **kwargs):
272 | if q:
273 | kwargs['q'] = q
274 | instances = self.api_call('search', **kwargs)
275 |
276 | if isinstance(instances[0], list) and (len(instances) > 1) and \
277 | (isinstance(instances[1], basestring) or instances[1] is None):
278 | instances, _next = instances
279 | while _next:
280 | instances_new, _next = self.api_call('search', with_next_url=_next)
281 | [instances.append(i) for i in instances_new]
282 |
283 | extra_fields = kwargs.pop('extra_fields', {})
284 | extra_fields['fetched'] = timezone.now()
285 |
286 | return self.parse_response_list(instances, extra_fields)
287 |
288 |
289 | class UserManager(InstagramSearchManager):
290 |
291 | def get(self, *args, **kwargs):
292 | if 'extra_fields' not in kwargs:
293 | kwargs['extra_fields'] = {}
294 | kwargs['extra_fields']['is_private'] = False
295 | try:
296 | return super(UserManager, self).get(*args, **kwargs)
297 | except InstagramError as e:
298 | if e.code == 400:
299 | if e.error_type == 'APINotAllowedError':
300 | # {'error_message': 'you cannot view this resource',
301 | # 'error_type': 'APINotAllowedError',
302 | # 'status_code': 400}
303 | try:
304 | instance = self.model.objects.get(pk=args[0])
305 | instance.is_private = True
306 | instance.save()
307 | return instance
308 | except self.model.DoesNotExist:
309 | raise e
310 | elif e.error_type == 'APINotFoundError':
311 | # {'error_message': 'this user does not exist',
312 | # 'error_type': 'APINotFoundError',
313 | # 'status_code': 400}
314 | try:
315 | instance = self.model.objects.get(pk=args[0])
316 | instance.delete()
317 | raise
318 | except self.model.DoesNotExist:
319 | raise e
320 | else:
321 | raise
322 |
323 | def fetch_by_slug(self, *args, **kwargs):
324 | result = self.get_by_slug(*args, **kwargs)
325 | return self.get_or_create_from_instance(result)
326 |
327 | def get_by_url(self, url):
328 | """
329 | Return object by url
330 | :param url:
331 | """
332 | m = re.findall(r'(?:https?://)?(?:www\.)?instagram\.com/([^/]+)/?', url)
333 | if not len(m):
334 | raise ValueError("Url should be started with https://instagram.com/")
335 |
336 | return self.get_by_slug(m[0])
337 |
338 | def get_by_slug(self, slug):
339 | """
340 | Return existed User by slug or new intance with empty pk
341 | :param slug:
342 | """
343 | users = self.search(slug)
344 | for user in users:
345 | if user.username == slug:
346 | return self.get(user.id)
347 | raise ValueError("No users found for the name %s" % slug)
348 |
349 | def fetch_followers(self, user, **kwargs):
350 | return self.create_related_users('followed_by', user, **kwargs)
351 |
352 | def fetch_follows(self, user, **kwargs):
353 | return self.create_related_users('follows', user, **kwargs)
354 |
355 | def create_related_users(self, method, user, **kwargs):
356 | if kwargs.pop('source') == 'graphql':
357 | ids = self.create_related_users_graphql(method, user)
358 | else:
359 | ids = self.create_related_users_api(method, user)
360 |
361 | method = method.replace('followed_by', 'followers')
362 | m2m_relation = getattr(user, method)
363 | initial = m2m_relation.versions.count() == 0
364 | setattr(user, method, ids) # user.followers = ids
365 |
366 | if initial:
367 | m2m_relation.get_queryset_through().update(time_from=None)
368 | m2m_relation.versions.update(added_count=0)
369 |
370 | return m2m_relation.all()
371 |
372 | def create_related_users_api(self, method, user):
373 | ids = []
374 | extra_fiels = {'fetched': timezone.now()}
375 |
376 | _next = True
377 | while _next:
378 | kwargs = {} if _next is True else {'with_next_url': _next}
379 | users = []
380 | instances, _next = self.api_call(method, user.pk, **kwargs)
381 | for instance in instances:
382 | instance = self.parse_response_object(instance, extra_fiels)
383 | users += [instance]
384 | ids += [instance.id]
385 | self.bulk_create_from_instances(users)
386 | return ids
387 |
388 | def create_related_users_graphql(self, method, user):
389 | graphql = GraphQL()
390 | ids = []
391 | extra_fiels = {'fetched': timezone.now()}
392 | for resources in graphql.related_users(method, user):
393 | users = []
394 | for instance in resources:
395 | instance['profile_picture'] = instance['profile_pic_url']
396 | instance = self.parse_response_object(instance, extra_fiels)
397 | users += [instance]
398 | ids += [instance.id]
399 | self.bulk_create_from_instances(users)
400 | return ids
401 |
402 | def fetch_media_likes(self, media):
403 | # TODO: get all likes
404 | # https://instagram.com/developer/endpoints/likes/#get_media_likes
405 | # no pagination to get all likes
406 | # http://stackoverflow.com/questions/20478485/get-a-list-of-users-who-have-liked-a-media-not-working-anymore
407 |
408 | extra_fields = {'fetched': timezone.now()}
409 |
410 | # users
411 | response = self.api_call('likes', media.remote_id)
412 | result = self.parse_response(response, extra_fields)
413 |
414 | instances = []
415 | for instance in result:
416 | instance = self.get_or_create_from_instance(instance)
417 | instances.append(instance)
418 |
419 | media.likes_users = instances + list(media.likes_users.all())
420 |
421 | return media.likes_users.all()
422 |
423 |
424 | class User(InstagramBaseModel):
425 |
426 | _followers_ids = []
427 | _follows_ids = []
428 |
429 | id = models.BigIntegerField(primary_key=True)
430 | username = models.CharField(max_length=30, db_index=True)
431 | full_name = models.CharField(max_length=80) # max_length=30 in interface
432 | bio = models.CharField(max_length=150)
433 |
434 | profile_picture = models.URLField(max_length=112)
435 | website = models.URLField(max_length=150) # found max_length=106
436 |
437 | followers_count = models.PositiveIntegerField(null=True, db_index=True)
438 | follows_count = models.PositiveIntegerField(null=True, db_index=True)
439 | media_count = models.PositiveIntegerField(null=True, db_index=True)
440 |
441 | followers = ManyToManyHistoryField('User', versions=True, related_name='follows')
442 |
443 | is_private = models.NullBooleanField('Account is private', db_index=True)
444 |
445 | objects = models.Manager()
446 | remote = UserManager(methods={
447 | 'get': 'user',
448 | 'search': 'user_search',
449 | 'follows': 'user_follows',
450 | 'followed_by': 'user_followed_by',
451 | 'likes': 'media_likes',
452 | })
453 |
454 | @property
455 | def slug(self):
456 | return self.username
457 |
458 | def __unicode__(self):
459 | return self.full_name or self.username
460 |
461 | @property
462 | def instagram_link(self):
463 | return u'https://instagram.com/%s/' % self.username
464 |
465 | def _substitute(self, old_instance):
466 | super(User, self)._substitute(old_instance)
467 | for field_name in ['followers_count', 'follows_count', 'media_count', 'is_private']:
468 | if getattr(self, field_name) is None and getattr(old_instance, field_name) is not None:
469 | setattr(self, field_name, getattr(old_instance, field_name))
470 |
471 | def save(self, *args, **kwargs):
472 |
473 | try:
474 | with atomic():
475 | super(InstagramBaseModel, self).save(*args, **kwargs)
476 | except IntegrityError as e:
477 | if 'username' in e.message:
478 | # duplicate key value violates unique constraint "instagram_api_user_username_key"
479 | # DETAIL: Key (username)=(...) already exists.
480 | user_local = User.objects.get(username=self.username)
481 | try:
482 | # check for recursive loop
483 | # get remote user
484 | user_remote = User.remote.get(user_local.pk)
485 | try:
486 | user_local2 = User.objects.get(username=user_remote.username)
487 | # if users excahnge usernames or user is dead (400 error)
488 | if user_local2.pk == self.pk or user_remote.is_private:
489 | user_local.username = 'temp%s' % time.time()
490 | user_local.save()
491 | except User.DoesNotExist:
492 | pass
493 | # fetch right user
494 | User.remote.fetch(user_local.pk)
495 | except InstagramError as e:
496 | if e.code == 400:
497 | user_local.delete()
498 | else:
499 | raise
500 | super(InstagramBaseModel, self).save(*args, **kwargs)
501 | else:
502 | raise
503 |
504 | def parse(self):
505 | if isinstance(self._response, dict) and 'counts' in self._response:
506 | count = self._response['counts']
507 | self._response['followers_count'] = count.get('followed_by', 0)
508 | self._response['follows_count'] = count.get('follows', 0)
509 | self._response['media_count'] = count.get('media', 0)
510 |
511 | super(User, self).parse()
512 |
513 | def fetch_follows(self, **kwargs):
514 | return User.remote.fetch_follows(user=self, **kwargs)
515 |
516 | def fetch_followers(self, **kwargs):
517 | return User.remote.fetch_followers(user=self, **kwargs)
518 |
519 | def fetch_media(self, **kwargs):
520 | return Media.remote.fetch_user_media(user=self, **kwargs)
521 |
522 | def refresh(self):
523 | # do refresh via client_id, because is_private is dependent on access_token and relation with current user
524 | with override_api_context('instagram', use_client_id=True):
525 | super(User, self).refresh()
526 |
527 |
528 | class MediaManager(InstagramManager):
529 |
530 | def fetch_user_media(self, user, count=None, min_id=None, max_id=None,
531 | after=None, before=None):
532 |
533 | extra_fields = {'fetched': timezone.now(), 'user_id': user.pk}
534 | kwargs = {'user_id': user.pk}
535 |
536 | if count:
537 | kwargs['count'] = count
538 | if min_id:
539 | kwargs['min_id'] = min_id
540 | if max_id:
541 | kwargs['max_id'] = max_id
542 | if after:
543 | kwargs['min_timestamp'] = time.mktime(after.timetuple())
544 | if before:
545 | kwargs['max_timestamp'] = time.mktime(before.timetuple())
546 |
547 | instances, _next = self.api_call('user_recent_media', **kwargs)
548 | while _next:
549 | instances_new, _next = self.api_call('user_recent_media', with_next_url=_next)
550 | instances_new = sorted(instances_new, reverse=True, key=lambda j: j.created_time)
551 | for i in instances_new:
552 | instances.append(i)
553 | # strange, but API arguments doesn't work
554 | if count and len(instances) >= count or after and i.created_time.replace(tzinfo=timezone.utc) <= after:
555 | _next = False
556 | break
557 |
558 | for instance in instances:
559 | instance = self.parse_response_object(instance, extra_fields)
560 | self.get_or_create_from_instance(instance)
561 |
562 | return user.media_feed.all()
563 |
564 | def fetch_tag_media(self, tag, count=None, max_tag_id=None):
565 |
566 | extra_fields = {'fetched': timezone.now()}
567 |
568 | kwargs = {'tag_name': tag.name, 'count': count, 'max_tag_id': max_tag_id}
569 | instances, _next = self.api_call('tag_recent_media', **kwargs)
570 | while _next:
571 | instances_new, _next = self.api_call('tag_recent_media', with_next_url=_next, tag_name=tag.name)
572 | [instances.append(i) for i in instances_new]
573 |
574 | for instance in instances:
575 | extra_fields['user_id'] = instance.user.id
576 | instance = self.parse_response_object(instance, extra_fields)
577 | instance = self.get_or_create_from_instance(instance)
578 | instance.tags.add(tag)
579 |
580 | return tag.media_feed.all()
581 |
582 | def fetch_location_media(self, location, count=None, max_id=None):
583 |
584 | extra_fields = {'fetched': timezone.now()}
585 |
586 | kwargs = {'location_id': location.pk, 'count': count, 'max_id': max_id}
587 | instances, _next = self.api_call('location_recent_media', **kwargs)
588 | while _next:
589 | instances_new, _next = self.api_call('location_recent_media', with_next_url=_next, location_id=location.pk)
590 | [instances.append(i) for i in instances_new]
591 |
592 | for instance in instances:
593 | extra_fields['user_id'] = instance.user.id
594 | extra_fields['location_id'] = location.pk
595 | instance = self.parse_response_object(instance, extra_fields)
596 | self.get_or_create_from_instance(instance)
597 |
598 | if count is None:
599 | location.media_count = location.media_feed.count()
600 | location.save()
601 |
602 | return location.media_feed.all()
603 |
604 |
605 | class Media(InstagramBaseModel):
606 | remote_id = models.CharField(max_length=30, unique=True)
607 | caption = models.TextField(blank=True)
608 | link = models.URLField(max_length=68)
609 |
610 | type = models.CharField(max_length=5)
611 | filter = models.CharField(max_length=40) # TODO: tune max_length of this field
612 |
613 | image_low_resolution = models.URLField(max_length=200)
614 | image_standard_resolution = models.URLField(max_length=200)
615 | image_thumbnail = models.URLField(max_length=200)
616 |
617 | video_low_bandwidth = models.URLField(max_length=130)
618 | video_low_resolution = models.URLField(max_length=130)
619 | video_standard_resolution = models.URLField(max_length=130)
620 |
621 | created_time = models.DateTimeField()
622 |
623 | comments_count = models.PositiveIntegerField(null=True, default=0)
624 | likes_count = models.PositiveIntegerField(null=True, default=0)
625 |
626 | user = models.ForeignKey('User', related_name="media_feed")
627 | location = models.ForeignKey('Location', null=True, related_name="media_feed")
628 | likes_users = ManyToManyHistoryField('User', related_name="likes_media")
629 | tags = models.ManyToManyField('Tag', related_name='media_feed')
630 |
631 | remote = MediaManager(remote_pk=('remote_id',), methods={
632 | 'get': 'media',
633 | 'user_recent_media': 'user_recent_media',
634 | 'tag_recent_media': 'tag_recent_media',
635 | 'location_recent_media': 'location_recent_media',
636 | })
637 |
638 | def get_url(self):
639 | return self.link
640 |
641 | def __unicode__(self):
642 | return self.caption
643 |
644 | def parse(self):
645 | self._response['remote_id'] = self._response.pop('id')
646 |
647 | for prefix in ['video', 'image']:
648 | key = '%ss' % prefix
649 | if key in self._response:
650 | for k, v in self._response[key].items():
651 | media = self._response[key][k]
652 | if isinstance(media, ApiModel):
653 | media = media.__dict__
654 | self._response['%s_%s' % (prefix, k)] = media['url']
655 |
656 | if not isinstance(self._response['created_time'], datetime):
657 | self._response['created_time'] = timestamp_to_datetime(self._response['created_time'])
658 |
659 | self._response['created_time'] = datetime.fromtimestamp(calendar.timegm(self._response['created_time'].timetuple()))
660 |
661 | if 'comment_count' in self._response:
662 | self._response['comments_count'] = self._response.pop('comment_count')
663 | elif 'comments' in self._response:
664 | self._response['comments_count'] = self._response.pop('comments')['count']
665 |
666 | if 'like_count' in self._response:
667 | self._response['likes_count'] = self._response.pop('like_count')
668 | elif 'likes' in self._response:
669 | self._response['likes_count'] = self._response.pop('likes')['count']
670 |
671 | if isinstance(self._response['caption'], ApiModel):
672 | self._response['caption'] = self._response['caption'].text
673 | elif isinstance(self._response['caption'], dict):
674 | self._response['caption'] = self._response['caption']['text']
675 |
676 | # if 'likes' in self._response:
677 | # self._response['likes_users'] = self._response.pop('likes')
678 |
679 | super(Media, self).parse()
680 |
681 | def fetch_comments(self):
682 | return Comment.remote.fetch_media_comments(self)
683 |
684 | def fetch_likes(self):
685 | return User.remote.fetch_media_likes(self)
686 |
687 | def save(self, *args, **kwargs):
688 | if self.caption is None:
689 | self.caption = ''
690 |
691 | super(Media, self).save(*args, **kwargs)
692 |
693 | for field, relations in self._relations_post_save['fk'].items():
694 | extra_fields = {'media_id': self.pk, 'owner_id': self.user_id} if field == 'comments' else {}
695 | for instance in relations:
696 | instance.__dict__.update(extra_fields)
697 | instance.__class__.remote.get_or_create_from_instance(instance)
698 |
699 | for field, relations in self._relations_post_save['m2m'].items():
700 | for instance in relations:
701 | instance = instance.__class__.remote.get_or_create_from_instance(instance)
702 | getattr(self, field).add(instance)
703 |
704 |
705 | class CommentManager(InstagramManager):
706 | def fetch_media_comments(self, media):
707 | response = self.api_call('comments', media.remote_id)
708 |
709 | extra_fields = {'fetched': timezone.now(), 'media_id': media.pk, 'owner_id': media.user_id}
710 | result = self.parse_response(response, extra_fields)
711 |
712 | instances = self.model.objects.none()
713 | for instance in result:
714 | instance = self.get_or_create_from_instance(instance)
715 | instances |= instance.__class__.objects.filter(pk=instance.pk)
716 |
717 | return instances
718 |
719 |
720 | class Comment(InstagramBaseModel):
721 | owner = models.ForeignKey(User, related_name='media_comments')
722 | user = models.ForeignKey(User, related_name='comments')
723 | media = models.ForeignKey(Media, related_name="comments")
724 |
725 | id = models.BigIntegerField(primary_key=True)
726 | text = models.TextField()
727 | created_time = models.DateTimeField()
728 |
729 | remote = CommentManager(methods={
730 | 'comments': 'media_comments',
731 | })
732 |
733 | def get_url(self):
734 | return self.media.link
735 |
736 | def parse(self):
737 | self._response['created_time'] = self._response.pop('created_at')
738 | super(Comment, self).parse()
739 |
740 |
741 | class TagManager(InstagramSearchManager):
742 | pass
743 |
744 |
745 | class Tag(InstagramBaseModel):
746 | _refresh_pk = 'name'
747 | name = models.CharField(max_length=50, unique=True)
748 | media_count = models.PositiveIntegerField(null=True)
749 |
750 | remote = TagManager(remote_pk=('name',), methods={
751 | 'get': 'tag',
752 | 'search': 'tag_search'
753 | })
754 |
755 | def __unicode__(self):
756 | return '#%s' % self.name
757 |
758 | def fetch_media(self, **kwargs):
759 | return Media.remote.fetch_tag_media(tag=self, **kwargs)
760 |
761 |
762 | class LocationManager(InstagramSearchManager):
763 | pass
764 |
765 |
766 | class Location(InstagramBaseModel):
767 | id = models.BigIntegerField(primary_key=True)
768 | name = models.CharField(max_length=100)
769 | latitude = models.FloatField(null=True)
770 | longitude = models.FloatField(null=True)
771 | media_count = models.PositiveIntegerField(null=True)
772 |
773 | remote = LocationManager(methods={
774 | 'get': 'location',
775 | 'search': 'location_search'
776 | })
777 |
778 | def __unicode__(self):
779 | return self.name
780 |
781 | def fetch_media(self, **kwargs):
782 | return Media.remote.fetch_location_media(location=self, **kwargs)
783 |
784 | def parse(self):
785 | super(Location, self).parse()
786 | if self._response['point']:
787 | self.latitude = self._response['point'].latitude
788 | self.longitude = self._response['point'].longitude
789 |
--------------------------------------------------------------------------------