pax_global_header 0000666 0000000 0000000 00000000064 12644042615 0014516 g ustar 00root root 0000000 0000000 52 comment=4be5b22c9ef086f8c647d3a95303df38495888c8
diaspy-0.5.1/ 0000775 0000000 0000000 00000000000 12644042615 0013012 5 ustar 00root root 0000000 0000000 diaspy-0.5.1/.gitignore 0000664 0000000 0000000 00000000674 12644042615 0015011 0 ustar 00root root 0000000 0000000 *~
*.swp
docs/build/*
*/__pycache__/*
__pycache__/*
.env
check-*.py
# Testing
testconf.py
TEST_COUNT
sketch.py
# Private stuff
private.*
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
diaspy-0.5.1/Changelog.markdown 0000664 0000000 0000000 00000023440 12644042615 0016450 0 ustar 00root root 0000000 0000000 ## Changelog for `diaspy`, unofficial DIASPORA\* interface for Python
This changelog file follows few rules:
* __rem__: indicates removed features,
* __new__: indicates new features,
* __upd__: indicates updated features,
* __dep__: indicates deprecated features,
Deprecation means that in the next version feature will be removed.
Also, after every version there should be a brief note describing possible
problems with migrating to it from older versions and usage of new features.
Users can always read the manual and dcumentation to make themselves more knowledgeable and
are encouraged to do so. They only need to remember that documentation is usually more
up-to-date than manual and if conflicts appear they should follow the order:
*docstrings* -> *docs/* -> *manual/*
----
#### Known issues
* __bug__: `diaspy` has problems/can't connect to pods using SNI (this is an issue with requests/urllib3/python),
----
#### Version `0.4.3`:
* __new__: `people.User().fetchprofile()` will issue a warning when user cannot be found on current pod,
* __new__: `settings.Profile` is now loaded during initialization (can be switched off),
* __fix__: fixed a bug in `__repr__()` method in `people.User()` object,
----
#### Version `0.4.2` (2013-12-19):
This version has some small incompatibilities with `0.4.1` so read Changelog carefully.
* __new__: `diaspy.people.User._fetchstream()` method,
* __new__: `diaspy.people.Me()` object representing current user,
* __new__: `**kwargs` added to `diaspy.streams.Generic.json()` methdo to give developers control over the creation of JSON,
* __new__: `.getHCard()` method added to `diaspy.people.User()`,
* __upd__: `diaspy.connection.Connection.login()` modifies connection object in-place **and** returns it (this allows more fluent API),
* __upd__: `diaspy.connection.Connection.login()` no longer returns status code (if login was unsuccessful it'll raise an exception),
* __upd__: `diaspy.connection.Connection._login()` no longer returns status code (if login was unsuccessful it'll raise an exception),
* __upd__: better error message in `diaspy.models.Post().__init__()`,
* __upd__: `data` variable in `diaspy.models.Post()` renamed to `_data` to indicate that it's considered private,
* __upd__: after deleting a post `Activity` stream is purged instead of being refilled (this preserves state of stream which is not reset to last 15 posts),
* __upd__: `filterByIDs()` method in `Aspects` stream renamed to `filter()`,
* __rem__: `diaspy.connection.Connection.getUserInfo()` moved to `diaspy.connection.Connection.getUserData()`,
* __rem__: `fetch` parameter removed from `diaspy.connection.Connection.getUserData()`,
* __dep__: `max_time` parameter in `diaspy.streams.*.more()` method is deprecated,
* __fix__: this release should fix the bug which prevented diaspy from working with some pods (e.g. diasp.eu and joindiaspora.com),
----
#### Version `0.4.1` (2013-09-12):
Login and authentication procedure backend received major changes in this version.
There are no longer `username` and `password` variables in `Connection` object.
Instead, credentials are stored (together with the token) in single variable `_login_data`.
This is preserved until you call `login()` at which point credentials are erased and
only token is left -- it can be obtained by calling `repr(Connection)`.
Also, this release is compatible with DIASPORA\* 0.2.0.0 but should still support
pods running on older versions.
And the test suite was updated. Yay!
* __new__: `diaspy.errors.SettingsError`.
* __upd__: `diaspy.settings.Account.setEmail()` can now raise `SettingsError` when request fails,
* __upd__: `diaspy.settings.Account.getEmail()` will now return empty string instead of raising an exception if cannot fetch mail,
* __upd__: improved language fetching in `diaspy.settings.Account.getLanguages()`.
* __rem__: `diaspy/client.py` is removed,
**`0.4.1-rc.3` (2013-09-08):**
* __new__: `diaspy.settings.Profile.load()` method for loading profile information,
* __new__: `diaspy.settings.Profile.update()` method for updating profile information,
* __new__: `diaspy.settings.Profile.setName()` method,
* __new__: `diaspy.settings.Profile.setBio()` method,
* __new__: `diaspy.settings.Profile.setLocation()` method,
* __new__: `diaspy.settings.Profile.setTags()` method,
* __new__: `diaspy.settings.Profile.setGender()` method,
* __new__: `diaspy.settings.Profile.setBirthDate()` method,
* __new__: `diaspy.settings.Profile.setSearchable()` method,
* __new__: `diaspy.settings.Profile.setNSFW()` method,
**`0.4.1-rc.2` (2013-09-06):**
* __new__: `diaspy.search.Search.tags()` method for getting tag suggestions,
* __new__: `diaspy.settings.Profile.getName()` method,
* __new__: `diaspy.settings.Profile.getBio()` method,
* __new__: `diaspy.settings.Profile.getLocation()` method,
* __new__: `diaspy.settings.Profile.getTags()` method,
* __new__: `diaspy.settings.Profile.getGender()` method,
* __new__: `diaspy.settings.Profile.getBirthDate()` method,
* __new__: `diaspy.settings.Profile.isSearchable()` method,
* __new__: `diaspy.settings.Profile.isNSFW()` method,
* __new__: `provider_display_name` parameter in `diaspy.streams.Stream.post()` (thanks @svbergerem),
* __upd__: `remeber_me` parameter in `diaspy.connection.Connection.login()`,
* __upd__: you must supply `username` and `password` parameters on init of `diaspy.connection.Connection`,
* __upd__: you must update your testconf.py (new fields are required for settings tests),
* __upd__: `diaspy.settings.Settings` renamed to `diaspy.settings.Account`,
* __rem__: `username` and `password` parameters removed from `diaspy.connection.Connection.login()`
must be supplied on init,
**`0.4.1-rc.1` (2013-09-02):**
* __new__: `__getitem__()` in `diaspy.models.Post`,
* __new__: `__dict__()` in `diaspy.models.Post`,
* __new__: `guid` argument in `diaspy.models.Post.__init__()`,
* __new__: `json()` method in `diaspy.streams.Generic` adds the possibility to export streams to JSON,
* __new__: `full()` method in `diaspy.streams.Generic` will try to fetch full stream (containing all posts),
* __new__: `setEmail()` method in `diaspy.settings.Settings`,
* __new__: `setLanguage()` method in `diaspy.settings.Settings`,
* __new__: `downloadPhotos()` method in `diaspy.settings.Settings`,
* __new__: `backtime` argument in `more()` method in `diaspy.streams.Generic`,
* __new__: `DiaspyError` will be raised when connection is created with empty password and/or username,
* __new__: `getSessionToken()` method in `diaspy.connection.Connection` returns string from `_diaspora_session` cookie,
* __new__: `direct` parameter in `diaspy.connection.Connection().get()` allowing to disable pod expansion,
* __upd__: if `Post()` is created with fetched comments, data will also be fetched as a dependency,
* __upd__: `id` argument type is now `int` (`diaspy.models.Post.__init__()`),
* __upd__: `Search().lookup_user()` renamed to `Search().lookupUser()`,
* __upd__: `diaspy.messages` renamed to `diaspy.conversations` (but will be accessible under both names for this and next release),
* __upd__: `LoginError` moved to `diaspy.errors`,
* __upd__: `TokenError` moved to `diaspy.errors`,
* __upd__: `diaspy.connection.Connection.podswitch()` gained two new positional arguments: `username` and `password`,
* __upd__: `aspect_id` renamed to `id` in `diaspy.streams.Aspects().remove()`,
* __fix__: fixed some bugs in regular expressions used by `diaspy` internals (html tag removal, so you get nicer notifications),
* __fix__: fixed authentication issues,
----
#### Version `0.4.0` (2013-08-20):
This release is **not backwards compatible with `0.3.x` line**! You'll have to check your code for corrections.
Also, this release if first to officially released fork version.
* __dep__: `diaspy.client` is officially deprecated (will be removed in `0.4.1`),
* __upd__: `diaspy.conversations` renamed to `diaspy.messages`,
* __udp__: `diaspy.conversations.Conversation` moved to `diaspy.models`,
* __new__: `diaspy.messages.Mailbox()` object representing diaspora\* mailbox,
----
#### Version `0.3.2` (2013-08-20):
* __upd__: `diaspy.connection.getUserData()` raises `DiaspyError` when it cannot find user data,
* __rem__: `diaspy.client.Client` must be explicitly imported,
----
#### Version `0.3.1` (2013-07-12):
* __upd__: `diaspy.people.sephandle()` raises `InvalidHandleError` instead of `UserError`
* __upd__: `models.Post()._fetch()` renamed to `_fetchdata()` (because of new `_fetchcomments()` method)
* __new__: `models.Comment()` object: wrapper for comments, not to be created manually
* __new__: `comments` parameter in `models.Post`: defines whether to fetch post's commets
* __new__: `connection.Connection` has new parameter in `__init__()`: it's `schema`
* __new__: `author()` method in `models.Post()`
The new parameter in `connection.Connection` is useful when operating with handles.
As handle does not contain schema (`http`, `https`, etc.) `_setlogin()` would raise an
unhandled exception -- `requests.exceptions.MissingSchema`.
Now, however, `Connection` will catch the exception, add missing schema and try once more.
This parameter is provided to give programmers ability to manipulate it.
Also, now you can pass just `pod.example.com` as `pod` parameter. Less typing!
When it comes to posts, we are now able to fetch comments.
----
#### Version `0.3.0` (2013-07-07):
First edition of Changelog for `diaspy`.
Developers should update their code as version `0.3.0` may not be fully
backwards compatible depending on how the code is written.
If you always pass named arguments and do not rely on their order you can, at least in
theory, not worry about this update.
Version `0.3.0` introduces few new features, fixes several bugs and brings a bit of
redesign and refactoring od `diaspy`'s code.
diaspy-0.5.1/LICENSE 0000664 0000000 0000000 00000002073 12644042615 0014021 0 ustar 00root root 0000000 0000000 The MIT License (MIT)
Copyright (c) 2013, Moritz Kiefer
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diaspy-0.5.1/Makefile 0000664 0000000 0000000 00000000542 12644042615 0014453 0 ustar 00root root 0000000 0000000 .PHONY: style-check test
style-check:
flake8 --max-complexity 6 ./diaspy/
test:
python3 -m unittest --verbose --catch --failfast tests.py
test-python2:
python2 -m unittest --verbose --catch --failfast tests
clean:
rm -v ./{diaspy/,}*.pyc
rm -rv ./{diaspy/,}__pycache__/
install:
python setup.py install
rm -rvf dist/
rm -rvf diaspy.egg-info
diaspy-0.5.1/README.md 0000664 0000000 0000000 00000003576 12644042615 0014304 0 ustar 00root root 0000000 0000000 ## Unofficial Python interface for Diaspora\* social network
`diaspy` is a set of modules which form an Python interface to the API of
Disapora\* social network.
Test suite will cause problems when run with 2.x so testing should be done
using python3 interpreter.
Object oriented design of `diaspy` makes it easily reusable by other
developers who want to use only part of the interface and create derivative
works from it.
Developrs who don't like the design of `diaspy` and want to create something better
can use only `diaspy.connection.Connection()` object as it is capable of
doing everything. Other modules are just layers that provide easier access to
parts of the Diaspora\* API.
----
### Dependencies
List of software `diaspy` requires to run.
Versions used by maintainer are the ones available in stock Arch x86\_64 repositories.
**`python`**
Version: 3.3.3
[Website](https://www.python.org/)
**`python-requests`**
Version: 2.1.0
[Website](http://docs.python-requests.org/en/latest/)
----
#### Quick intro
#### 1. Posting text to your stream
You only need two objects to do this: `Stream()` and `Connection()`.
>>> import diaspy
>>> c = diaspy.connection.Connection(pod='https://pod.example.com',
... username='foo',
... password='bar')
>>> c.login()
>>> stream = diaspy.streams.Stream(c)
>>> stream.post('Your first post')
----
#### 2. Reference implementation
There is no official reference implementation of D\* client using `diaspy`.
The `diaspy.client` module is no longer maintained and will be removed in the future.
However, there is a small script written that uses `diaspy` as its backend.
Look for `diacli` in marekjm's repositories on GitHub.
----
To get more information about how the code works read
documentation (`./doc/` directory) and manual (`./manual/` directory).
diaspy-0.5.1/diaspy/ 0000775 0000000 0000000 00000000000 12644042615 0014303 5 ustar 00root root 0000000 0000000 diaspy-0.5.1/diaspy/__init__.py 0000664 0000000 0000000 00000000523 12644042615 0016414 0 ustar 00root root 0000000 0000000 # flake8: noqa
import diaspy.connection as connection
import diaspy.models as models
import diaspy.streams as streams
import diaspy.conversations as messages
import diaspy.conversations as conversations
import diaspy.people as people
import diaspy.notifications as notifications
import diaspy.settings as settings
__version__ = '0.5.1'
diaspy-0.5.1/diaspy/connection.py 0000664 0000000 0000000 00000020566 12644042615 0017025 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This module abstracts connection to pod.
"""
import json
import re
import requests
import warnings
from diaspy import errors
DEBUG = True
class Connection():
"""Object representing connection with the pod.
"""
_token_regex = re.compile(r'name="csrf-token"\s+content="(.*?)"')
_userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
# this is for older version of D*
_token_regex_2 = re.compile(r'content="(.*?)"\s+name="csrf-token')
_userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
_verify_SSL = True
def __init__(self, pod, username, password, schema='https'):
"""
:param pod: The complete url of the diaspora pod to use.
:type pod: str
:param username: The username used to log in.
:type username: str
:param password: The password used to log in.
:type password: str
"""
self.pod = pod
self._session = requests.Session()
self._login_data = {'user[remember_me]': 1, 'utf8': '✓'}
self._userdata = {}
self._token = ''
self._diaspora_session = ''
self._cookies = self._fetchcookies()
try:
#self._setlogin(username, password)
self._login_data = {'user[username]': username,
'user[password]': password,
'authenticity_token': self._fetchtoken()}
success = True
except requests.exceptions.MissingSchema:
self.pod = '{0}://{1}'.format(schema, self.pod)
warnings.warn('schema was missing')
success = False
finally:
pass
try:
if not success:
self._login_data = {'user[username]': username,
'user[password]': password,
'authenticity_token': self._fetchtoken()}
except Exception as e:
raise errors.LoginError('cannot create login data (caused by: {0})'.format(e))
def _fetchcookies(self):
request = self.get('stream')
return request.cookies
def __repr__(self):
"""Returns token string.
It will be easier to change backend if programs will just use:
repr(connection)
instead of calling a specified method.
"""
return self._fetchtoken()
def get(self, string, headers={}, params={}, direct=False, **kwargs):
"""This method gets data from session.
Performs additional checks if needed.
Example:
To obtain 'foo' from pod one should call `get('foo')`.
:param string: URL to get without the pod's URL and slash eg. 'stream'.
:type string: str
:param direct: if passed as True it will not be expanded
:type direct: bool
"""
if not direct: url = '{0}/{1}'.format(self.pod, string)
else: url = string
return self._session.get(url, params=params, headers=headers, verify=self._verify_SSL, **kwargs)
def post(self, string, data, headers={}, params={}, **kwargs):
"""This method posts data to session.
Performs additional checks if needed.
Example:
To post to 'foo' one should call `post('foo', data={})`.
:param string: URL to post without the pod's URL and slash eg. 'status_messages'.
:type string: str
:param data: Data to post.
:param headers: Headers (optional).
:type headers: dict
:param params: Parameters (optional).
:type params: dict
"""
string = '{0}/{1}'.format(self.pod, string)
request = self._session.post(string, data, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
return request
def put(self, string, data=None, headers={}, params={}, **kwargs):
"""This method PUTs to session.
"""
string = '{0}/{1}'.format(self.pod, string)
if data is not None: request = self._session.put(string, data, headers=headers, params=params, **kwargs)
else: request = self._session.put(string, headers=headers, params=params, verify=self._verify_SSL, **kwargs)
return request
def delete(self, string, data, headers={}, **kwargs):
"""This method lets you send delete request to session.
Performs additional checks if needed.
:param string: URL to use.
:type string: str
:param data: Data to use.
:param headers: Headers to use (optional).
:type headers: dict
"""
string = '{0}/{1}'.format(self.pod, string)
request = self._session.delete(string, data=data, headers=headers, **kwargs)
return request
def _setlogin(self, username, password):
"""This function is used to set data for login.
.. note::
It should be called before _login() function.
"""
self._login_data = {'user[username]': username,
'user[password]': password,
'authenticity_token': self._fetchtoken()}
def _login(self):
"""Handles actual login request.
Raises LoginError if login failed.
"""
request = self.post('users/sign_in',
data=self._login_data,
allow_redirects=False)
if request.status_code != 302:
raise errors.LoginError('{0}: login failed'.format(request.status_code))
def login(self, remember_me=1):
"""This function is used to log in to a pod.
Will raise LoginError if password or username was not specified.
"""
if not self._login_data['user[username]'] or not self._login_data['user[password]']:
raise errors.LoginError('username and/or password is not specified')
self._login_data['user[remember_me]'] = remember_me
status = self._login()
self._login_data = {}
return self
def logout(self):
"""Logs out from a pod.
When logged out you can't do anything.
"""
self.get('users/sign_out')
self.token = ''
def podswitch(self, pod, username, password):
"""Switches pod from current to another one.
"""
self.pod = pod
self._setlogin(username, password)
self._login()
def _fetchtoken(self):
"""This method tries to get token string needed for authentication on D*.
:returns: token string
"""
request = self.get('stream')
token = self._token_regex.search(request.text)
if token is None: token = self._token_regex_2.search(request.text)
if token is not None: token = token.group(1)
else: raise errors.TokenError('could not find valid CSRF token')
self._token = token
return token
def get_token(self, fetch=True):
"""This function returns a token needed for authentication in most cases.
**Notice:** using repr() is recommended method for getting token.
Each time it is run a _fetchtoken() is called and refreshed token is stored.
It is more safe to use than _fetchtoken().
By setting new you can request new token or decide to get stored one.
If no token is stored new one will be fetched anyway.
:returns: string -- token used to authenticate
"""
try:
if fetch or not self._token: self._fetchtoken()
except requests.exceptions.ConnectionError as e:
warnings.warn('{0} was cought: reusing old token'.format(e))
finally:
if not self._token: raise errors.TokenError('cannot obtain token and no previous token found for reuse')
return self._token
def getSessionToken(self):
"""Returns session token string (_diaspora_session).
"""
return self._diaspora_session
def getUserData(self):
"""Returns user data.
"""
request = self.get('bookmarklet')
userdata = self._userinfo_regex.search(request.text)
if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
if userdata is None: raise errors.DiaspyError('cannot find user data')
userdata = userdata.group(1)
return json.loads(userdata)
def set_verify_SSL(self, verify):
"""Sets whether there should be an error if a SSL-Certificate could not be verified.
"""
self._verify_SSL = verify
diaspy-0.5.1/diaspy/conversations.py 0000664 0000000 0000000 00000001570 12644042615 0017555 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
from diaspy import errors, models
class Mailbox():
"""Object implementing diaspora* mailbox.
"""
def __init__(self, connection, fetch=True):
self._connection = connection
self._mailbox = []
if fetch: self._fetch()
def __len__(self):
return len(self._mailbox)
def __iter__(self):
return iter(self._mailbox)
def __getitem__(self, n):
return self._mailbox[n]
def _fetch(self):
"""This method will fetch messages from user's mailbox.
"""
request = self._connection.get('conversations.json')
if request.status_code != 200:
raise errors.DiaspyError('wrong status code: {0}'.format(request.status_code))
mailbox = request.json()
self._mailbox = [models.Conversation(self._connection, c['conversation']['id']) for c in mailbox]
diaspy-0.5.1/diaspy/errors.py 0000664 0000000 0000000 00000006430 12644042615 0016174 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""This module contains custom exceptions that are raised by diaspy.
These are not described by DIASPORA* protocol as exceptions that should be
raised by API implementations but are specific to this particular implementation.
If your program should catch all exceptions raised by diaspy and
does not need to handle them specifically you can use following code:
# this line imports all errors
from diaspy.errors import *
try:
# your code...
except DiaspyError as e:
# your error handling code...
finally:
# closing code...
"""
import warnings
class DiaspyError(Exception):
"""Base exception for all errors
raised by diaspy.
"""
pass
class LoginError(DiaspyError):
"""Exception raised when something
bad happens while performing actions
related to logging in.
"""
pass
class TokenError(DiaspyError):
pass
class UserError(DiaspyError):
"""Exception raised when something related to users goes wrong.
"""
pass
class InvalidHandleError(DiaspyError):
"""Raised when invalid handle is found.
"""
pass
class SearchError(DiaspyError):
"""Exception raised when something related to search goes wrong.
"""
pass
class ConversationError(DiaspyError):
"""Exception raised when something related to conversations goes wrong.
"""
pass
class AspectError(DiaspyError):
"""Exception raised when something related to aspects goes wrong.
"""
pass
class PostError(DiaspyError):
"""Exception raised when something related to posts goes wrong.
"""
pass
class StreamError(DiaspyError):
"""Exception raised when something related to streams goes wrong.
"""
pass
class SettingsError(DiaspyError):
"""Exception raised when something related to settings goes wrong.
"""
pass
def react(r, message='', accepted=[200, 201, 202, 203, 204, 205, 206], exception=DiaspyError):
"""This method tries to decide how to react
to a response code passed to it. If it's an
error code it will raise an exception (it will
call `throw()` method.
If response code is not accepted AND cannot
be matched to any exception, generic exception
(DiaspyError) is raised (provided that `exception`
param was left untouched).
By default `accepted` param contains all HTTP
success codes.
User can force type of exception to raise by passing
`exception` param.
:param r: response code
:type r: int
:param message: message for the exception
:type message: str
:param accepted: list of accepted error codes
:type accepted: list
:param exception: preferred exception to raise
:type exception: valid exception type (default: DiaspyError)
"""
warnings.warn(DeprecationWarning)
if r in accepted: e = None
else: e = DiaspyError
if e is not None: e = exception
throw(e, message=message)
def throw(e, message=''):
"""This function throws an error with given message.
If None is passed as `e` throw() will not raise
anything.
:param e: exception to throw
:type e: any valid exception type or None
:param message: message for exception
:type message: str
"""
warnings.warn(DeprecationWarning)
if e is None: pass
else: raise e(message)
diaspy-0.5.1/diaspy/models.py 0000664 0000000 0000000 00000042070 12644042615 0016143 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""This module is only imported in other diaspy modules and
MUST NOT import anything.
"""
import json
import re
from diaspy import errors
class Aspect():
"""This class represents an aspect.
Class can be initialized by passing either an id and/or name as
parameters.
If both are missing, an exception will be raised.
"""
def __init__(self, connection, id=None, name=None):
self._connection = connection
self.id, self.name = id, name
if id and not name:
self.name = self._findname()
elif name and not id:
self.id = self._findid()
elif not id and not name:
raise Exception('Aspect must be initialized with either an id or name')
def _findname(self):
"""Finds name for aspect.
"""
name = None
aspects = self._connection.getUserData()['aspects']
for a in aspects:
if a['id'] == self.id:
name = a['name']
break
return name
def _findid(self):
"""Finds id for aspect.
"""
id = None
aspects = self._connection.getUserData()['aspects']
for a in aspects:
if a['name'] == self.name:
id = a['id']
break
return id
def _getajax(self):
"""Returns HTML returned when editing aspects via web UI.
"""
start_regexp = re.compile('
')
ajax = self._connection.get('aspects/{0}/edit'.format(self.id)).text
begin = ajax.find(start_regexp.search(ajax).group(0))
end = ajax.find('
')
return ajax[begin:end]
def _extractusernames(self, ajax):
"""Extracts usernames and GUIDs from ajax returned by Diaspora*.
Returns list of two-tuples: (guid, diaspora_name).
"""
userline_regexp = re.compile('[\w()*@. -]+')
return [(line[17:33], re.escape(line[35:-4])) for line in userline_regexp.findall(ajax)]
def _extractpersonids(self, ajax, usernames):
"""Extracts `person_id`s and usernames from ajax and list of usernames.
Returns list of two-tuples: (username, id)
"""
personid_regexp = 'alt=["\']{0}["\'] class=["\']avatar["\'] data-person_id=["\'][0-9]+["\']'
personids = [re.compile(personid_regexp.format(name)).search(ajax).group(0) for guid, name in usernames]
for n, line in enumerate(personids):
i, id = -2, ''
while line[i].isdigit():
id = line[i] + id
i -= 1
personids[n] = (usernames[n][1], id)
return personids
def _defineusers(self, ajax, personids):
"""Gets users contained in this aspect by getting users who have `delete` method.
"""
method_regexp = 'data-method="delete" data-person_id="{0}"'
users = []
for name, id in personids:
if re.compile(method_regexp.format(id)).search(ajax): users.append(name)
return users
def _getguids(self, users_in_aspect, usernames):
"""Defines users contained in this aspect.
"""
guids = []
for guid, name in usernames:
if name in users_in_aspect: guids.append(guid)
return guids
def getUsers(self):
"""Returns list of GUIDs of users who are listed in this aspect.
"""
ajax = self._getajax()
usernames = self._extractusernames(ajax)
personids = self._extractpersonids(ajax, usernames)
users_in_aspect = self._defineusers(ajax, personids)
return self._getguids(users_in_aspect, usernames)
def addUser(self, user_id):
"""Add user to current aspect.
:param user_id: user to add to aspect
:type user_id: int
:returns: JSON from request
"""
data = {'authenticity_token': repr(self._connection),
'aspect_id': self.id,
'person_id': user_id}
request = self._connection.post('aspect_memberships.json', data=data)
if request.status_code == 400:
raise errors.AspectError('duplicate record, user already exists in aspect: {0}'.format(request.status_code))
elif request.status_code == 404:
raise errors.AspectError('user not found from this pod: {0}'.format(request.status_code))
elif request.status_code != 200:
raise errors.AspectError('wrong status code: {0}'.format(request.status_code))
return request.json()
def removeUser(self, user_id):
"""Remove user from current aspect.
:param user_id: user to remove from aspect
:type user: int
"""
data = {'authenticity_token': repr(self._connection),
'aspect_id': self.id,
'person_id': user_id}
request = self._connection.delete('aspect_memberships/{0}.json'.format(self.id), data=data)
if request.status_code != 200:
raise errors.AspectError('cannot remove user from aspect: {0}'.format(request.status_code))
return request.json()
class Notification():
"""This class represents single notification.
"""
_who_regexp = re.compile(r'/people/[0-9a-f]+" class=\'hovercardable')
_when_regexp = re.compile(r'[0-9]{4,4}(-[0-9]{2,2}){2,2} [0-9]{2,2}(:[0-9]{2,2}){2,2} UTC')
_aboutid_regexp = re.compile(r'/posts/[0-9a-f]+')
_htmltag_regexp = re.compile('?[a-z]+( *[a-z_-]+=["\'].*?["\'])* */?>')
def __init__(self, connection, data):
self._connection = connection
self.type = list(data.keys())[0]
self._data = data[self.type]
self.id = self._data['id']
self.unread = self._data['unread']
def __getitem__(self, key):
"""Returns a key from notification data.
"""
return self._data[key]
def __str__(self):
"""Returns notification note.
"""
string = re.sub(self._htmltag_regexp, '', self._data['note_html'])
string = string.strip().split('\n')[0]
while ' ' in string: string = string.replace(' ', ' ')
return string
def __repr__(self):
"""Returns notification note with more details.
"""
return '{0}: {1}'.format(self.when(), str(self))
def about(self):
"""Returns id of post about which the notification is informing OR:
If the id is None it means that it's about user so .who() is called.
"""
about = self._aboutid_regexp.search(self._data['note_html'])
if about is None: about = self.who()
else: about = int(about.group(0)[7:])
return about
def who(self):
"""Returns list of guids of the users who caused you to get the notification.
"""
return [who[8:24] for who in self._who_regexp.findall(self._data['note_html'])]
def when(self):
"""Returns UTC time as found in note_html.
"""
return self._when_regexp.search(self._data['note_html']).group(0)
def mark(self, unread=False):
"""Marks notification to read/unread.
Marks notification to read if `unread` is False.
Marks notification to unread if `unread` is True.
:param unread: which state set for notification
:type unread: bool
"""
headers = {'x-csrf-token': repr(self._connection)}
params = {'set_unread': json.dumps(unread)}
self._connection.put('notifications/{0}'.format(self['id']), params=params, headers=headers)
self._data['unread'] = unread
class Conversation():
"""This class represents a conversation.
.. note::
Remember that you need to have access to the conversation.
"""
def __init__(self, connection, id, fetch=True):
"""
:param conv_id: id of the post and not the guid!
:type conv_id: str
:param connection: connection object used to authenticate
:type connection: connection.Connection
"""
self._connection = connection
self.id = id
self._data = {}
if fetch: self._fetch()
def _fetch(self):
"""Fetches JSON data representing conversation.
"""
request = self._connection.get('conversations/{}.json'.format(self.id))
if request.status_code == 200:
self._data = request.json()['conversation']
else:
raise errors.ConversationError('cannot download conversation data: {0}'.format(request.status_code))
def answer(self, text):
"""Answer that conversation
:param text: text to answer.
:type text: str
"""
data = {'message[text]': text,
'utf8': '✓',
'authenticity_token': repr(self._connection)}
request = self._connection.post('conversations/{}/messages'.format(self.id),
data=data,
headers={'accept': 'application/json'})
if request.status_code != 200:
raise errors.ConversationError('{0}: Answer could not be posted.'
.format(request.status_code))
return request.json()
def delete(self):
"""Delete this conversation.
Has to be implemented.
"""
data = {'authenticity_token': repr(self._connection)}
request = self._connection.delete('conversations/{0}/visibility/'
.format(self.id),
data=data,
headers={'accept': 'application/json'})
if request.status_code != 404:
raise errors.ConversationError('{0}: Conversation could not be deleted.'
.format(request.status_code))
def get_subject(self):
"""Returns the subject of this conversation
"""
return self._data['subject']
class Comment():
"""Represents comment on post.
Does not require Connection() object. Note that you should not manually
create `Comment()` objects -- they are designed to be created automatically
by `Post()` objects.
"""
def __init__(self, data):
self._data = data
def __str__(self):
"""Returns comment's text.
"""
return self._data['text']
def __repr__(self):
"""Returns comments text and author.
Format: AUTHOR (AUTHOR'S GUID): COMMENT
"""
return '{0} ({1}): {2}'.format(self.author(), self.author('guid'), str(self))
def when(self):
"""Returns time when the comment had been created.
"""
return self._data['created_at']
def author(self, key='name'):
"""Returns author of the comment.
"""
return self._data['author'][key]
class Post():
"""This class represents a post.
.. note::
Remember that you need to have access to the post.
"""
def __init__(self, connection, id=0, guid='', fetch=True, comments=True):
"""
:param id: id of the post (GUID is recommended)
:type id: int
:param guid: GUID of the post
:type guid: str
:param connection: connection object used to authenticate
:type connection: connection.Connection
:param fetch: defines whether to fetch post's data or not
:type fetch: bool
:param comments: defines whether to fetch post's comments or not (if True also data will be fetched)
:type comments: bool
"""
if not (guid or id): raise TypeError('neither guid nor id was provided')
self._connection = connection
self.id = id
self.guid = guid
self._data = {}
self.comments = []
if fetch: self._fetchdata()
if comments:
if not self._data: self._fetchdata()
self._fetchcomments()
def __repr__(self):
"""Returns string containing more information then str().
"""
return '{0} ({1}): {2}'.format(self._data['author']['name'], self._data['author']['guid'], self._data['text'])
def __str__(self):
"""Returns text of a post.
"""
return self._data['text']
def __getitem__(self, key):
return self._data[key]
def __dict__(self):
"""Returns dictionary of posts data.
"""
return self._data
def _fetchdata(self):
"""This function retrieves data of the post.
:returns: guid of post whose data was fetched
"""
if self.id: id = self.id
if self.guid: id = self.guid
request = self._connection.get('posts/{0}.json'.format(id))
if request.status_code != 200:
raise errors.PostError('{0}: could not fetch data for post: {1}'.format(request.status_code, id))
else:
self._data = request.json()
return self['guid']
def _fetchcomments(self):
"""Retreives comments for this post.
Retrieving comments via GUID will result in 404 error.
DIASPORA* does not supply comments through /posts/:guid/ endpoint.
"""
id = self._data['id']
if self['interactions']['comments_count']:
request = self._connection.get('posts/{0}/comments.json'.format(id))
if request.status_code != 200:
raise errors.PostError('{0}: could not fetch comments for post: {1}'.format(request.status_code, id))
else:
self.comments = [Comment(c) for c in request.json()]
def update(self):
"""Updates post data.
"""
self._fetchdata()
self._fetchcomments()
def like(self):
"""This function likes a post.
It abstracts the 'Like' functionality.
:returns: dict -- json formatted like object.
"""
data = {'authenticity_token': repr(self._connection)}
request = self._connection.post('posts/{0}/likes'.format(self.id),
data=data,
headers={'accept': 'application/json'})
if request.status_code != 201:
raise errors.PostError('{0}: Post could not be liked.'
.format(request.status_code))
return request.json()
def reshare(self):
"""This function reshares a post
"""
data = {'root_guid': self._data['guid'],
'authenticity_token': repr(self._connection)}
request = self._connection.post('reshares',
data=data,
headers={'accept': 'application/json'})
if request.status_code != 201:
raise Exception('{0}: Post could not be reshared'.format(request.status_code))
return request.json()
def comment(self, text):
"""This function comments on a post
:param text: text to comment.
:type text: str
"""
data = {'text': text,
'authenticity_token': repr(self._connection)}
request = self._connection.post('posts/{0}/comments'.format(self.id),
data=data,
headers={'accept': 'application/json'})
if request.status_code != 201:
raise Exception('{0}: Comment could not be posted.'
.format(request.status_code))
return request.json()
def delete(self):
""" This function deletes this post
"""
data = {'authenticity_token': repr(self._connection)}
request = self._connection.delete('posts/{0}'.format(self.id),
data=data,
headers={'accept': 'application/json'})
if request.status_code != 204:
raise errors.PostError('{0}: Post could not be deleted'.format(request.status_code))
def delete_comment(self, comment_id):
"""This function removes a comment from a post
:param comment_id: id of the comment to remove.
:type comment_id: str
"""
data = {'authenticity_token': repr(self._connection)}
request = self._connection.delete('posts/{0}/comments/{1}'
.format(self.id, comment_id),
data=data,
headers={'accept': 'application/json'})
if request.status_code != 204:
raise errors.PostError('{0}: Comment could not be deleted'
.format(request.status_code))
def delete_like(self):
"""This function removes a like from a post
"""
data = {'authenticity_token': repr(self._connection)}
url = 'posts/{0}/likes/{1}'.format(self.id, self._data['interactions']['likes'][0]['id'])
request = self._connection.delete(url, data=data)
if request.status_code != 204:
raise errors.PostError('{0}: Like could not be removed.'
.format(request.status_code))
def author(self, key='name'):
"""Returns author of the post.
:param key: all keys available in data['author']
"""
return self._data['author'][key]
diaspy-0.5.1/diaspy/notifications.py 0000664 0000000 0000000 00000003023 12644042615 0017524 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import time
from diaspy.models import Notification
"""This module abstracts notifications.
"""
class Notifications():
"""This class represents notifications of a user.
"""
def __init__(self, connection):
self._connection = connection
self._notifications = self.get()
def __iter__(self):
return iter(self._notifications)
def __getitem__(self, n):
return self._notifications[n]
def last(self):
"""Returns list of most recent notifications.
"""
params = {'per_page': 5, '_': int(round(time.time(), 3)*1000)}
headers = {'x-csrf-token': repr(self._connection)}
request = self._connection.get('notifications.json', headers=headers, params=params)
if request.status_code != 200:
raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
return [Notification(self._connection, n) for n in request.json()]
def get(self, per_page=5, page=1):
"""Returns list of notifications.
"""
params = {'per_page': per_page, 'page': page}
headers = {'x-csrf-token': repr(self._connection)}
request = self._connection.get('notifications.json', headers=headers, params=params)
if request.status_code != 200:
raise Exception('status code: {0}: cannot retreive notifications'.format(request.status_code))
notifications = request.json()
return [Notification(self._connection, n) for n in notifications]
diaspy-0.5.1/diaspy/people.py 0000664 0000000 0000000 00000016123 12644042615 0016144 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
import json
import re
import warnings
from diaspy.streams import Outer
from diaspy.models import Aspect
from diaspy import errors
from diaspy import search
def sephandle(handle):
"""Separate Diaspora* handle into pod pod and user.
:returns: two-tuple (pod, user)
"""
if re.match('^[a-zA-Z]+[a-zA-Z0-9_-]*@[a-z0-9.]+\.[a-z]+$', handle) is None:
raise errors.InvalidHandleError('{0}'.format(handle))
handle = handle.split('@')
pod, user = handle[1], handle[0]
return (pod, user)
class User():
"""This class abstracts a D* user.
This object goes around the limitations of current D* API and will
extract user data using black magic.
However, no chickens are harmed when you use it.
The parameter fetch should be either 'posts', 'data' or 'none'. By
default it is 'posts' which means in addition to user data, stream
will be fetched. If user has not posted yet diaspy will not be able
to extract the information from his/her posts. Since there is no official
way to do it we rely on user posts. If this will be the case user
will be notified with appropriate exception message.
If fetch is 'data', only user data will be fetched. If the user is
not found, no exception will be returned.
When creating new User() one can pass either guid, handle and/or id as
optional parameters. GUID takes precedence over handle when fetching
user stream. When fetching user data, handle is required.
"""
def __init__(self, connection, guid='', handle='', fetch='posts', id=0):
self._connection = connection
self.stream = []
self.data = {
'guid': guid,
'handle': handle,
'id': id,
}
self._fetch(fetch)
def __getitem__(self, key):
return self.data[key]
def __str__(self):
return self['guid']
def __repr__(self):
return '{0} ({1})'.format(self['handle'], self['guid'])
def _fetchstream(self):
self.stream = Outer(self._connection, location='people/{0}.json'.format(self['guid']))
def _fetch(self, fetch):
"""Fetch user posts or data.
"""
if fetch == 'posts':
if self['handle'] and not self['guid']: self.fetchhandle()
else: self.fetchguid()
elif fetch == 'data' and self['handle']:
self.fetchprofile()
def _finalize_data(self, data):
"""Adjustments are needed to have similar results returned
by search feature and fetchguid()/fetchhandle().
"""
names = [('id', 'id'),
('guid', 'guid'),
('name', 'name'),
('avatar', 'avatar'),
('handle', 'diaspora_id'),
]
final = {}
for f, d in names:
final[f] = data[d]
return final
def _postproc(self, request):
"""Makes necessary modifications to user data and
sets up a stream.
:param request: request object
:type request: request
"""
if request.status_code != 200: raise Exception('wrong error code: {0}'.format(request.status_code))
request = request.json()
if not len(request): raise errors.UserError('cannot extract user data: no posts to analyze')
self.data = self._finalize_data(request[0]['author'])
def fetchhandle(self, protocol='https'):
"""Fetch user data and posts using Diaspora handle.
"""
pod, user = sephandle(self['handle'])
request = self._connection.get('{0}://{1}/u/{2}.json'.format(protocol, pod, user), direct=True)
self._postproc(request)
self._fetchstream()
def fetchguid(self):
"""Fetch user data and posts using guid.
"""
if self['guid']:
request = self._connection.get('people/{0}.json'.format(self['guid']))
self._postproc(request)
self._fetchstream()
else:
raise errors.UserError('GUID not set')
def fetchprofile(self):
"""Fetches user data.
"""
data = search.Search(self._connection).user(self['handle'])
if not data:
raise errors.UserError('user with handle "{0}" has not been found on pod "{1}"'.format(self['handle'], self._connection.pod))
else:
self.data = data[0]
def getHCard(self):
"""Returns XML string containing user HCard.
"""
request = self._connection.get('hcard/users/{0}'.format(self['guid']))
if request.status_code != 200: raise errors.UserError('could not fetch hcard for user: {0}'.format(self['guid']))
return request.text
class Me():
"""Object represetnting current user.
"""
_userinfo_regex = re.compile(r'window.current_user_attributes = ({.*})')
_userinfo_regex_2 = re.compile(r'gon.user=({.*});gon.preloads')
def __init__(self, connection):
self._connection = connection
def getInfo(self):
"""This function returns the current user's attributes.
:returns: dict
"""
request = self._connection.get('bookmarklet')
userdata = self._userinfo_regex.search(request.text)
if userdata is None: userdata = self._userinfo_regex_2.search(request.text)
if userdata is None: raise errors.DiaspyError('cannot find user data')
userdata = userdata.group(1)
return json.loads(userdata)
class Contacts():
"""This class represents user's list of contacts.
"""
def __init__(self, connection):
self._connection = connection
def add(self, user_id, aspect_ids):
"""Add user to aspects of given ids.
:param user_id: user guid
:type user_id: str
:param aspect_ids: list of aspect ids
:type aspect_ids: list
"""
for aid in aspect_ids: Aspect(self._connection, aid).addUser(user_id)
def remove(self, user_id, aspect_ids):
"""Remove user from aspects of given ids.
:param user_id: user guid
:type user_id: str
:param aspect_ids: list of aspect ids
:type aspect_ids: list
"""
for aid in aspect_ids: Aspect(self._connection, aid).removeUser(user_id)
def get(self, set=''):
"""Returns list of user contacts.
Contact is a User() who is in one or more of user's
aspects.
By default, it will return list of users who are in
user's aspects.
If `set` is `all` it will also include users who only share
with logged user and are not in his/hers aspects.
If `set` is `only_sharing` it will return users who are only
sharing with logged user and ARE NOT in his/hers aspects.
:param set: if passed could be 'all' or 'only_sharing'
:type set: str
"""
params = {}
if set: params['set'] = set
request = self._connection.get('contacts.json', params=params)
if request.status_code != 200:
raise Exception('status code {0}: cannot get contacts'.format(request.status_code))
return [User(self._connection, guid=user['guid'], handle=user['handle'], fetch=None) for user in request.json()]
diaspy-0.5.1/diaspy/search.py 0000664 0000000 0000000 00000003302 12644042615 0016120 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""This module holds functionality related to searching.
"""
from diaspy import errors
class Search():
"""This object is used for searching for content on Diaspora*.
"""
def __init__(self, connection):
self._connection = connection
def lookupUser(self, handle):
"""This function will launch a webfinger lookup from the pod for the
handle requested. Response code is returned and if the lookup was successful,
user should soon be searchable via pod used for connection.
:param string: Handle to search for.
"""
request = self._connection.get('people', headers={'accept': 'text/html'}, params={'q': handle})
return request.status_code
def user(self, query):
"""Searches for a user.
Will return list of dictionaries containing
data of found users.
"""
request = self._connection.get('people.json', params={'q': query, 'utf-8': '%u2713'})
if request.status_code != 200:
raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
return request.json()
def tags(self, query, limit=10):
"""Retrieve tag suggestions.
:param query: query used to search
:type query: str
:param limit: maxmal number of suggestions returned
:type limit: int
"""
params = {'q': query, 'limit': limit}
request = self._connection.get('tags', params=params, headers={'x-csrf-token': repr(self._connection)})
if request.status_code != 200:
raise errors.SearchError('wrong status code: {0}'.format(request.status_code))
return [i['name'] for i in request.json()]
diaspy-0.5.1/diaspy/settings.py 0000664 0000000 0000000 00000027153 12644042615 0016525 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
"""This module provides access to user's settings on Diaspora*.
"""
import json
import os
import re
import urllib
import warnings
from diaspy import errors, streams
class Account():
"""Provides interface to account settings.
"""
email_regexp = re.compile('(.*?)')
def __init__(self, connection):
self._connection = connection
def downloadxml(self):
"""Returns downloaded XML.
"""
request = self._connection.get('user/export')
return request.text
def downloadPhotos(self, size='large', path='.', mark_nsfw=True, _critical=False, _stream=None):
"""Downloads photos into the current working directory.
Sizes are: large, medium, small.
Filename is: {post_guid}_{photo_guid}.{extension}
Normally, this method will catch urllib-generated errors and
just issue warnings about photos that couldn't be downloaded.
However, with _critical param set to True errors will become
critical - the will be reraised in finally block.
:param size: size of the photos to download - large, medium or small
:type size: str
:param path: path to download (defaults to current working directory
:type path: str
:param mark_nsfw: will append '-nsfw' to images from posts marked as nsfw,
:type mark_nsfw: bool
:param _stream: diaspy.streams.Generic-like object (only for testing)
:param _critical: if True urllib errors will be reraised after generating a warning (may be removed)
:returns: integer, number of photos downloaded
"""
photos = 0
if _stream is None:
stream = streams.Activity(self._connection)
stream.full()
else:
stream = _stream
for i, post in enumerate(stream):
if post['nsfw'] is not False: nsfw = '-nsfw'
else: nsfw = ''
if post['photos']:
for n, photo in enumerate(post['photos']):
# photo format -- .jpg, .png etc.
ext = photo['sizes'][size].split('.')[-1]
name = '{0}_{1}{2}.{3}'.format(post['guid'], photo['guid'], nsfw, ext)
filename = os.path.join(path, name)
try:
urllib.request.urlretrieve(url=photo['sizes'][size], filename=filename)
except (urllib.error.HTTPError, urllib.error.URLError) as e:
warnings.warn('downloading image {0} from post {1}: {2}'.format(photo['guid'], post['guid'], e))
finally:
if _critical: raise
photos += 1
return photos
def setEmail(self, email):
"""Changes user's email.
"""
data = {'_method': 'put', 'utf8': '✓', 'user[email]': email, 'authenticity_token': repr(self._connection)}
request = self._connection.post('user', data=data, allow_redirects=False)
if request.status_code != 302:
raise errors.SettingsError('setting email failed: {0}'.format(request.status_code))
def getEmail(self):
"""Returns currently used email.
"""
data = self._connection.get('user/edit')
email = self.email_regexp.search(data.text)
if email is None: email = ''
else: email = email.group(1)
return email
def setLanguage(self, lang):
"""Changes user's email.
:param lang: language identifier from getLanguages()
"""
data = {'_method': 'put', 'utf8': '✓', 'user[language]': lang, 'authenticity_token': repr(self._connection)}
request = self._connection.post('user', data=data, allow_redirects=False)
if request.status_code != 302:
raise errors.SettingsError('setting language failed: {0}'.format(request.status_code))
def getLanguages(self):
"""Returns a list of tuples containing ('Language name', 'identifier').
One of the Black Magic(tm) methods.
"""
request = self._connection.get('user/edit')
return self.language_option_regexp.findall(request.text)
class Privacy():
"""Provides interface to provacy settings.
"""
def __init__(self, connection):
self._connection = connection
class Profile():
"""Provides interface to profile settigns.
WARNING:
Because of the way update requests for profile are created every field must be sent.
The `load()` method is used to load all information into the dictionary.
Setters can then be used to adjust the data.
Finally, `update()` can be called to send data back to pod.
"""
firstname_regexp = re.compile('id="profile_first_name" name="profile\[first_name\]" type="text" value="(.*?)" />')
lastname_regexp = re.compile('id="profile_last_name" name="profile\[last_name\]" type="text" value="(.*?)" />')
bio_regexp = re.compile('')
location_regexp = re.compile('id="profile_location" name="profile\[location\]" placeholder="Fill me out" type="text" value="(.*?)" />')
gender_regexp = re.compile('id="profile_gender" name="profile\[gender\]" placeholder="Fill me out" type="text" value="(.*?)" />')
birth_year_regexp = re.compile('selected="selected" value="([0-9]{4,4})">[0-9]{4,4}')
birth_month_regexp = re.compile('selected="selected" value="([0-9]{1,2})">(.*?)')
birth_day_regexp = re.compile('selected="selected" value="([0-9]{1,2})">[0-9]{1,2}')
is_searchable_regexp = re.compile('checked="checked" id="profile_searchable" name="profile\[searchable\]" type="checkbox" value="(.*?)" />')
is_nsfw_regexp = re.compile('checked="checked" id="profile_nsfw" name="profile\[nsfw\]" type="checkbox" value="(.*?)" />')
def __init__(self, connection, no_load=False):
self._connection = connection
self.data = {'utf-8': '✓',
'_method': 'put',
'profile[first_name]': '',
'profile[last_name]': '',
'profile[tag_string]': '',
'tags': '',
'file': '',
'profile[bio]': '',
'profile[location]': '',
'profile[gender]': '',
'profile[date][year]': '',
'profile[date][month]': '',
'profile[date][day]': '',
}
self._html = self._fetchhtml()
self._loaded = False
if not no_load: self.load()
def _fetchhtml(self):
"""Fetches html that will be used to extract data.
"""
return self._connection.get('profile/edit').text
def getName(self):
"""Returns two-tuple: (first, last) name.
"""
first = self.firstname_regexp.search(self._html).group(1)
last = self.lastname_regexp.search(self._html).group(1)
return (first, last)
def getTags(self):
"""Returns tags user had selected when describing him/her-self.
"""
guid = self._connection.getUserData()['guid']
html = self._connection.get('people/{0}'.format(guid)).text
description_regexp = re.compile('#.*?')
return [tag.lower() for tag in re.findall(description_regexp, html)]
def getBio(self):
"""Returns user bio.
"""
bio = self.bio_regexp.search(self._html).group(1)
return bio
def getLocation(self):
"""Returns location string.
"""
location = self.location_regexp.search(self._html).group(1)
return location
def getGender(self):
"""Returns location string.
"""
gender = self.gender_regexp.search(self._html).group(1)
return gender
def getBirthDate(self, named_month=False):
"""Returns three-tuple: (year, month, day).
:param named_month: if True, return name of the month instead of integer
:type named_month: bool
"""
year = self.birth_year_regexp.search(self._html)
if year is None: year = -1
else: year = int(year.group(1))
month = self.birth_month_regexp.search(self._html)
if month is None:
if named_month: month = ''
else: month = -1
else:
if named_month:
month = month.group(2)
else:
month = int(month.group(1))
day = self.birth_day_regexp.search(self._html)
if day is None: day = -1
else: day = int(day.group(1))
return (year, month, day)
def isSearchable(self):
"""Returns True if profile is searchable.
"""
searchable = self.is_searchable_regexp.search(self._html)
# this is because value="true" in every case so we just
# check if the field is "checked"
if searchable is None: searchable = False # if it isn't - the regexp just won't match
else: searchable = True
return searchable
def isNSFW(self):
"""Returns True if profile is marked as NSFW.
"""
nsfw = self.is_nsfw_regexp.search(self._html)
if nsfw is None: nsfw = False
else: nsfw = True
return nsfw
def setName(self, first, last):
"""Set first and last name.
"""
self.data['profile[first_name]'] = first
self.data['profile[last_name]'] = last
def setTags(self, tags):
"""Sets tags that describe the user.
"""
self.data['tags'] = ', '.join(['#{}'.format(tag) for tag in tags])
def setBio(self, bio):
"""Set bio of a user.
"""
self.data['profile[bio]'] = bio
def setLocation(self, location):
"""Set location of a user.
"""
self.data['profile[location]'] = location
def setGender(self, gender):
"""Set gender of a user.
"""
self.data['profile[gender]'] = gender
def setBirthDate(self, year, month, day):
"""Set birth date of a user.
"""
self.data['profile[date][year]'] = year
self.data['profile[date][month]'] = month
self.data['profile[date][day]'] = day
def setSearchable(self, searchable):
"""Set user's searchable status.
"""
self.data['profile[searchable]'] = json.dumps(searchable)
def setNSFW(self, nsfw):
"""Set user NSFW status.
"""
self.data['profile[nsfw]'] = json.dumps(nsfw)
def load(self):
"""Loads profile data into self.data dictionary.
**Notice:** Not all keys are loaded yet.
"""
self.setName(*self.getName())
self.setBio(self.getBio())
self.setLocation(self.getLocation())
self.setGender(self.getGender())
self.setBirthDate(*self.getBirthDate(named_month=False))
self.setSearchable(self.isSearchable())
self.setNSFW(self.isNSFW())
self.setTags(self.getTags())
self._loaded = True
def update(self):
"""Updates profile information.
"""
if not self._loaded: raise errors.DiaspyError('profile was not loaded')
self.data['authenticity_token'] = repr(self._connection)
print(self.data)
request = self._connection.post('profile', data=self.data, allow_redirects=False)
return request.status_code
class Services():
"""Provides interface to services settings.
"""
def __init__(self, connection):
self._connection = connection
diaspy-0.5.1/diaspy/streams.py 0000664 0000000 0000000 00000041727 12644042615 0016346 0 ustar 00root root 0000000 0000000 """Docstrings for this module are taken from:
https://gist.github.com/MrZYX/01c93096c30dc44caf71
Documentation for D* JSON API taken from:
http://pad.spored.de/ro/r.qWmvhSZg7rk4OQam
"""
import json
import time
from diaspy.models import Post, Aspect
from diaspy import errors
class Generic():
"""Object representing generic stream.
"""
_location = 'stream.json'
def __init__(self, connection, location='', fetch=True):
"""
:param connection: Connection() object
:type connection: diaspy.connection.Connection
:param location: location of json (optional)
:type location: str
:param fetch: will call .fill() if true
:type fetch: bool
"""
self._connection = connection
if location: self._location = location
self._stream = []
# since epoch
self.max_time = int(time.mktime(time.gmtime()))
if fetch: self.fill()
def __contains__(self, post):
"""Returns True if stream contains given post.
"""
return post in self._stream
def __iter__(self):
"""Provides iterable interface for stream.
"""
return iter(self._stream)
def __getitem__(self, n):
"""Returns n-th item in Stream.
"""
return self._stream[n]
def __len__(self):
"""Returns length of the Stream.
"""
return len(self._stream)
def _obtain(self, max_time=0, suppress=True):
"""Obtains stream from pod.
suppress:bool - suppress post-fetching errors (e.g. 404)
"""
params = {}
if max_time:
params['max_time'] = max_time
params['_'] = int(time.time() * 1000)
request = self._connection.get(self._location, params=params)
if request.status_code != 200:
raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
posts = []
for post in request.json():
try:
posts.append(Post(self._connection, guid=post['guid']))
except errors.PostError:
if not suppress:
raise
return posts
def _expand(self, new_stream):
"""Appends older posts to stream.
"""
ids = [post.id for post in self._stream]
stream = self._stream
for post in new_stream:
if post.id not in ids:
stream.append(post)
ids.append(post.id)
self._stream = stream
def _update(self, new_stream):
"""Updates stream with new posts.
"""
ids = [post.id for post in self._stream]
stream = self._stream
for i in range(len(new_stream)):
if new_stream[-i].id not in ids:
stream = [new_stream[-i]] + stream
ids.append(new_stream[-i].id)
self._stream = stream
def clear(self):
"""Set stream to empty.
"""
self._stream = []
def purge(self):
"""Removes all unexistent posts from stream.
"""
stream = []
for post in self._stream:
deleted = False
try:
# error will tell us that the post has been deleted
post.update()
except Exception:
deleted = True
finally:
if not deleted: stream.append(post)
self._stream = stream
def update(self):
"""Updates stream with new posts.
"""
self._update(self._obtain())
def fill(self):
"""Fills the stream with posts.
**Notice:** this will create entirely new list of posts.
If you want to preseve posts already present in stream use update().
"""
self._stream = self._obtain()
def more(self, max_time=0, backtime=84600):
"""Tries to download more (older posts) posts from Stream.
:param backtime: how many seconds substract each time (defaults to one day)
:type backtime: int
:param max_time: seconds since epoch (optional, diaspy'll figure everything on its own)
:type max_time: int
"""
if not max_time: max_time = self.max_time - backtime
self.max_time = max_time
new_stream = self._obtain(max_time=max_time)
self._expand(new_stream)
def full(self, backtime=84600, retry=42, callback=None):
"""Fetches full stream - containing all posts.
WARNING: this is a **VERY** long running function.
Use callback parameter to access information about the stream during its
run.
Default backtime is one day. But sometimes user might not have any activity for longer
period (in the beginning of my D* activity I was posting once a month or so).
The role of retry is to hadle such situations by trying to go further back in time.
If a post is found the counter is restored.
Default retry is 42. If you don't know why go to the nearest library (or to the nearest
Piratebay mirror) and grab a copy of "A Hitchhiker's Guide to the Galaxy" and read the
book to find out. This will also increase your level of geekiness and you'll have a
great time reading the book.
:param backtime: how many seconds to substract each time
:type backtime: int
:param retry: how many times the functin should look deeper than your last post
:type retry: int
:param callback: callable taking diaspy.streams.Generic as an argument
:returns: integer, lenght of the stream
"""
oldstream = self.copy()
self.more()
while len(oldstream) < len(self):
oldstream = self.copy()
if callback is not None: callback(self)
self.more(backtime=backtime)
if len(oldstream) < len(self): continue
# but if no posts were found start retrying...
print('retrying... {0}'.format(retry))
n = retry
while n > 0:
print('\t', n, self.max_time)
# try to get even more posts...
self.more(backtime=backtime)
print('\t', len(oldstream), len(self))
# check if it was a success...
if len(oldstream) < len(self):
# and if so restore normal order of execution by
# going one loop higher
break
oldstream = self.copy()
# if it was not a success substract one backtime, keep calm and
# try going further back in time...
n -= 1
# check the comment below
# no commented code should be present in good software
#if len(oldstream) == len(self): break
return len(self)
def copy(self):
"""Returns copy (list of posts) of current stream.
"""
return [p for p in self._stream]
def json(self, comments=False, **kwargs):
"""Returns JSON encoded string containing stream's data.
:param comments: to include comments or not to include 'em, that is the question this param holds answer to
:type comments: bool
"""
stream = [post for post in self._stream]
if comments:
for i, post in enumerate(stream):
post._fetchcomments()
comments = [c.data for c in post.comments]
post['interactions']['comments'] = comments
stream[i] = post
stream = [post._data for post in stream]
return json.dumps(stream, **kwargs)
class Outer(Generic):
"""Object used by diaspy.models.User to represent
stream of other user.
"""
def _obtain(self, max_time=0):
"""Obtains stream from pod.
"""
params = {}
if max_time: params['max_time'] = max_time
request = self._connection.get(self._location, params=params)
if request.status_code != 200:
raise errors.StreamError('wrong status code: {0}'.format(request.status_code))
return [Post(self._connection, post['id']) for post in request.json()]
class Stream(Generic):
"""The main stream containing the combined posts of the
followed users and tags and the community spotlights posts
if the user enabled those.
"""
location = 'stream.json'
def post(self, text='', aspect_ids='public', photos=None, photo='', provider_display_name=''):
"""This function sends a post to an aspect.
If both `photo` and `photos` are specified `photos` takes precedence.
:param text: Text to post.
:type text: str
:param aspect_ids: Aspect ids to send post to.
:type aspect_ids: str
:param photo: filename of photo to post
:type photo: str
:param photos: id of photo to post (obtained from _photoupload())
:type photos: int
:param provider_display_name: name of provider displayed under the post
:type provider_display_name: str
:returns: diaspy.models.Post -- the Post which has been created
"""
data = {}
data['aspect_ids'] = aspect_ids
data['status_message'] = {'text': text, 'provider_display_name': provider_display_name}
if photo: data['photos'] = self._photoupload(photo)
if photos: data['photos'] = photos
request = self._connection.post('status_messages',
data=json.dumps(data),
headers={'content-type': 'application/json',
'accept': 'application/json',
'x-csrf-token': repr(self._connection)})
if request.status_code != 201:
raise Exception('{0}: Post could not be posted.'.format(request.status_code))
post = Post(self._connection, request.json()['id'])
return post
def _photoupload(self, filename, aspects=[]):
"""Uploads picture to the pod.
:param filename: path to picture file
:type filename: str
:param aspect_ids: list of ids of aspects to which you want to upload this photo
:type aspect_ids: list of integers
:returns: id of the photo being uploaded
"""
data = open(filename, 'rb')
image = data.read()
data.close()
params = {}
params['photo[pending]'] = 'true'
params['set_profile_image'] = ''
params['qqfile'] = filename
if not aspects: aspects = self._connection.getUserData()['aspects']
for i, aspect in enumerate(aspects):
params['photo[aspect_ids][{0}]'.format(i)] = aspect['id']
headers = {'content-type': 'application/octet-stream',
'x-csrf-token': repr(self._connection),
'x-file-name': filename}
request = self._connection.post('photos', data=image, params=params, headers=headers)
if request.status_code != 200:
raise errors.StreamError('photo cannot be uploaded: {0}'.format(request.status_code))
return request.json()['data']['photo']['id']
class Activity(Stream):
"""Stream representing user's activity.
"""
_location = 'activity.json'
def _delid(self, id):
"""Deletes post with given id.
"""
post = None
for p in self._stream:
if p['id'] == id:
post = p
break
if post is not None: post.delete()
def delete(self, post):
"""Deletes post from users activity.
`post` can be either post id or Post()
object which will be identified and deleted.
After deleting post the stream will be purged.
:param post: post identifier
:type post: str, diaspy.models.Post
"""
if type(post) == str: self._delid(post)
elif type(post) == Post: post.delete()
else: raise TypeError('this method accepts str or Post types: {0} given')
self.purge()
class Aspects(Generic):
"""This stream contains the posts filtered by
the specified aspect IDs. You can choose the aspect IDs with
the parameter `aspect_ids` which value should be
a comma seperated list of aspect IDs.
If the parameter is ommitted all aspects are assumed.
An example call would be `aspects.json?aspect_ids=23,5,42`
"""
_location = 'aspects.json'
def getAspectID(self, aspect_name):
"""Returns id of an aspect of given name.
Returns -1 if aspect is not found.
:param aspect_name: aspect name (must be spelled exactly as when created)
:type aspect_name: str
:returns: int
"""
id = -1
aspects = self._connection.getUserData()['aspects']
for aspect in aspects:
if aspect['name'] == aspect_name: id = aspect['id']
return id
def filter(self, ids):
"""Filters posts by given aspect ids.
:parameter ids: list of apsect ids
:type ids: list of integers
"""
self._location = 'aspects.json' + '?{0}'.format(','.join(ids))
self.fill()
def add(self, aspect_name, visible=0):
"""This function adds a new aspect.
Status code 422 is accepted because it is returned by D* when
you try to add aspect already present on your aspect list.
:param aspect_name: name of aspect to create
:param visible: whether the contacts in this aspect are visible to each other or not
:returns: Aspect() object of just created aspect
"""
data = {'authenticity_token': repr(self._connection),
'aspect[name]': aspect_name,
'aspect[contacts_visible]': visible}
request = self._connection.post('aspects', data=data)
if request.status_code not in [200, 422]:
raise Exception('wrong status code: {0}'.format(request.status_code))
id = self.getAspectID(aspect_name)
return Aspect(self._connection, id)
def remove(self, id=-1, name=''):
"""This method removes an aspect.
You can give it either id or name of the aspect.
When both are specified, id takes precedence over name.
Status code 500 is accepted because although the D* will
go nuts it will remove the aspect anyway.
:param aspect_id: id fo aspect to remove
:type aspect_id: int
:param name: name of aspect to remove
:type name: str
"""
if id == -1 and name: id = self.getAspectID(name)
data = {'_method': 'delete',
'authenticity_token': repr(self._connection)}
request = self._connection.post('aspects/{0}'.format(id), data=data)
if request.status_code not in [200, 302, 500]:
raise Exception('wrong status code: {0}: cannot remove aspect'.format(request.status_code))
class Commented(Generic):
"""This stream contains all posts
the user has made a comment on.
"""
_location = 'commented.json'
class Liked(Generic):
"""This stream contains all posts the user liked.
"""
_location = 'liked.json'
class Mentions(Generic):
"""This stream contains all posts
the user is mentioned in.
"""
_location = 'mentions.json'
class FollowedTags(Generic):
"""This stream contains all posts
containing tags the user is following.
"""
_location = 'followed_tags.json'
def get(self):
"""Returns list of followed tags.
"""
return []
def remove(self, tag_id):
"""Stop following a tag.
:param tag_id: tag id
:type tag_id: int
"""
data = {'authenticity_token': self._connection.get_token()}
request = self._connection.delete('tag_followings/{0}'.format(tag_id), data=data)
if request.status_code != 404:
raise Exception('wrong status code: {0}'.format(request.status_code))
def add(self, tag_name):
"""Follow new tag.
Error code 403 is accepted because pods respod with it when request
is sent to follow a tag that a user already follows.
:param tag_name: tag name
:type tag_name: str
:returns: int (response code)
"""
data = {'name': tag_name,
'authenticity_token': repr(self._connection),
}
headers = {'content-type': 'application/json',
'x-csrf-token': repr(self._connection),
'accept': 'application/json'
}
request = self._connection.post('tag_followings', data=json.dumps(data), headers=headers)
if request.status_code not in [201, 403]:
raise Exception('wrong error code: {0}'.format(request.status_code))
return request.status_code
class Tag(Generic):
"""This stream contains all posts containing a tag.
"""
def __init__(self, connection, tag, fetch=True):
"""
:param connection: Connection() object
:type connection: diaspy.connection.Connection
:param tag: tag name
:type tag: str
"""
self._connection = connection
self._location = 'tags/{0}.json'.format(tag)
if fetch: self.fill()
diaspy-0.5.1/docs/ 0000775 0000000 0000000 00000000000 12644042615 0013742 5 ustar 00root root 0000000 0000000 diaspy-0.5.1/docs/Makefile 0000664 0000000 0000000 00000012706 12644042615 0015410 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rvf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/diaspy.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/diaspy.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/diaspy"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/diaspy"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
diaspy-0.5.1/docs/source/ 0000775 0000000 0000000 00000000000 12644042615 0015242 5 ustar 00root root 0000000 0000000 diaspy-0.5.1/docs/source/conf.py 0000664 0000000 0000000 00000017745 12644042615 0016557 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# diaspy documentation build configuration file, created by
# sphinx-quickstart on Tue Jan 15 15:35:41 2013.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
sys.path.insert(0, os.path.abspath(os.path.join(os.pardir, os.pardir)))
# -- General configuration -----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'diaspy'
copyright = '2013, Moritz Kiefer, Marek Marecki and others'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '0.4.2'
# The full version, including alpha/beta/rc tags.
release = '0.4.2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'diaspydoc'
# -- Options for LaTeX output --------------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'diaspy.tex', 'diaspy Documentation',
'Moritz Kiefer', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output --------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'diaspy', 'diaspy Documentation',
['Moritz Kiefer and Marek Marecki'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output ------------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'diaspy', 'diaspy Documentation',
'Moritz Kiefer and Marek Marecki', 'diaspy', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}
autoclass_content = 'both'
diaspy-0.5.1/docs/source/connection.rst 0000664 0000000 0000000 00000000200 12644042615 0020123 0 ustar 00root root 0000000 0000000 connection Module
=================
.. automodule:: diaspy.connection
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/conversations.rst 0000664 0000000 0000000 00000000211 12644042615 0020663 0 ustar 00root root 0000000 0000000 conversations Module
====================
.. automodule:: diaspy.conversations
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/errors.rst 0000664 0000000 0000000 00000000164 12644042615 0017311 0 ustar 00root root 0000000 0000000 errors Module
=============
.. automodule:: diaspy.errors
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/index.rst 0000664 0000000 0000000 00000000661 12644042615 0017106 0 ustar 00root root 0000000 0000000 .. diaspy documentation master file, created by
sphinx-quickstart on Tue Jan 15 15:35:41 2013.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to diaspy's documentation!
==================================
Contents:
.. toctree::
:maxdepth: 2
modules
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
diaspy-0.5.1/docs/source/models.rst 0000664 0000000 0000000 00000000164 12644042615 0017260 0 ustar 00root root 0000000 0000000 models Module
=============
.. automodule:: diaspy.models
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/modules.rst 0000664 0000000 0000000 00000000234 12644042615 0017443 0 ustar 00root root 0000000 0000000 diaspy
======
.. toctree::
:maxdepth: 4
connection
models
streams
notifications
people
settings
search
conversations
errors
diaspy-0.5.1/docs/source/notifications.rst 0000664 0000000 0000000 00000000211 12644042615 0020637 0 ustar 00root root 0000000 0000000 notifications Module
====================
.. automodule:: diaspy.notifications
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/people.rst 0000664 0000000 0000000 00000000164 12644042615 0017261 0 ustar 00root root 0000000 0000000 people Module
=============
.. automodule:: diaspy.people
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/search.rst 0000664 0000000 0000000 00000000165 12644042615 0017243 0 ustar 00root root 0000000 0000000 search Module
==============
.. automodule:: diaspy.search
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/settings.rst 0000664 0000000 0000000 00000000172 12644042615 0017634 0 ustar 00root root 0000000 0000000 settings Module
===============
.. automodule:: diaspy.settings
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/docs/source/streams.rst 0000664 0000000 0000000 00000000167 12644042615 0017456 0 ustar 00root root 0000000 0000000 streams Module
==============
.. automodule:: diaspy.streams
:members:
:undoc-members:
:show-inheritance:
diaspy-0.5.1/logger.py 0000664 0000000 0000000 00000000664 12644042615 0014651 0 ustar 00root root 0000000 0000000 #!/usr/bin/env python3
"""Module used for obtaining (pod, username, password) tuple.
Used by me when I want to debug in public places and not want to show
my password.
"""
import getpass
def getdata():
"""Asks for data.
"""
pod = input('Pod: ')
username = input('Username at \'{0}\': '.format(pod))
password = getpass.getpass('Password for {0}@{1}: '.format(username, pod))
return (pod, username, password)
diaspy-0.5.1/manual/ 0000775 0000000 0000000 00000000000 12644042615 0014267 5 ustar 00root root 0000000 0000000 diaspy-0.5.1/manual/connection.markdown 0000664 0000000 0000000 00000007111 12644042615 0020172 0 ustar 00root root 0000000 0000000 #### `Connection()` object
This is the object that is used by `diaspy`'s internals.
It is pushed around and used by various methods and other objects:
* `Post()` and `Conversation()` objects require it to authenticate and
do their work,
* streams use it for getting their contents,
* etc.
`Connection()` is the most low-level part of `diaspy` and provides everything
what is needed to talk to a pod.
However, using only `Connection()` would be hard and cumberstone so there are
other modules to aid you and you are strongly encouraged to use them.
----
##### Login procedure
`Client()` object available in `diaspy` will login you automatically - provided
you gave it valid pod, username and password.
On the other hand, `Connection()` is more stupid and it will not log you in unless
you explicitly order it to do so.
Logging in with `Connection()` is done via `login()` method.
**Example:**
connection = diaspy.connection.Connection(pod='https://pod.example.com')
connection.login('user', 'password')
OR
connection = diaspy.connection.Connection(pod='https://pod.example.com',
username='user',
password='password')
connection.login()
In the example above two ways of logging in were shown.
In the first one only *pod* is passed to the object and
*username* and *password* were passed to `login()` method.
In the second one everything is passed directly to the object being
created and `login()` is called without any arguments.
Both ways are valid and will result in exactly the same connection.
But consider the following example:
connection = diaspy.connection.Connection(pod='https://pod.example.com',
username='user',
password='password')
connection.login(username='loser', password='passphrase')
This code will result in connection with username `loser` and
password `passphrase` because data passed to `login()` overrides data
passed directly to object.
**Remember:** if you pass something to `login()` it will *override* credentials set when the
connection was created.
----
##### Authentication
Authentication in Diaspora\* is done with *token*. It is a string
which is passed to pod with several requests to prove you are who you are
pretending to be.
`Connection` provides you with a method to fetch this token and perform
various actions on your account.
This method is called `_fetchtoken()`.
It will try to download a token for you.
Once a token is fetched there is no use for doing it again.
You can save time by using `get_token()` method.
It will check whether the token had been already fetched and reuse it.
This is especially useful on slow or unstable connections.
`get_token()` has an optional `fetch` argument (it is of `bool` type, by default `False`)
which will tell it to fetch new token if you find suitable.
However, recommended way of dealing with token is to use `repr()` function on
`Connection` object. This will allow of your programs to be future-proof because if
for any reason we will change the way in which authorization is handled `get_token()`
method may be gone -- `repr()` will stay.
Here is how you should create your auth flow:
connection = diaspy.connection.Connection(...)
connection.login()
token = repr(connection)
----
##### Note for developers
If you want to write your own interface or client for D\*
`Connection()` is the only object you need.
----
###### Manual for `diaspy`, written by Marek Marecki
diaspy-0.5.1/manual/models.markdown 0000664 0000000 0000000 00000000466 12644042615 0017324 0 ustar 00root root 0000000 0000000 #### `diaspy` models
Design of models in `diaspy` follow few simple rules.
##### Initialization
First argument is always `Connection` object stored in `self._connection`.
Parameters specific to each model go after it.
If model requires some king of id (guid, conversation id, post id) it is simply `id`.
diaspy-0.5.1/manual/notifications.markdown 0000664 0000000 0000000 00000002214 12644042615 0020703 0 ustar 00root root 0000000 0000000 #### `Notifications()` object
In order to get list of notifications use `diaspy.notifications.Notifications()` object.
It support iteration and indexing.
When creating new instance of `Notifications` only `Connection` object is needed.
#### Methods
##### `last()`
This method will return you last five notifications.
##### `get()`
This is slightly more advanced then `last()`. It allows you to specify how many
notifications per page you want to get and which page you want to recieve.
----
#### `Notification()` model
Single notification (it should be obvious that it requires object of its own) is located in
`diaspy.models.Notification()`. It has several methods you can use.
##### 1. `who()`
This method will return list of guids of the users who caused you to get this notification.
##### 2. `when()`
This method will return UTC time when you get the notification.
##### 3. `mark()`
To mark notification as `read` or `unread`. It has one parameter - `unread` which is boolean.
Also, you can use `str()` and `repr()` on the `Notification()` and you will get nice
string.
----
###### Manual for `diaspy`, written by Marek Marecki
diaspy-0.5.1/manual/people.markdown 0000664 0000000 0000000 00000004755 12644042615 0017332 0 ustar 00root root 0000000 0000000 #### `User()` object
This object is used to represent a D\* user.
----
##### `User()` object -- getting user data
You have to know either GUID or *handle* of a user.
Assume that *1234567890abcdef* and *otheruser@pod.example.com* point to
the same user.
>>> c = diaspy.connection.Connection('https://pod.example.com', 'foo', 'bar')
>>>
>>> user_guid = diaspy.people.User(c, guid='1234567890abcdef')
>>> user_handle = diaspy.people.User(c, handle='otheruser@pod.example.com')
Now, you have two `User` objects containing the data of one user.
The object is subscriptable so you can do like this:
>>> user_guid['handle']
'otheruser@pod.example.com'
>>>
>>> user_handle['guid']
'1234567890abcdef'
`User` object contains following items in its `data` dict:
* `id`, `str`, id of the user;
* `guid`, `str`, guid of the user;
* `handle`, `str`, D\* id (or handle) of the user;
* `name`, `str`, name of the user;
* `avatar`, `dict`, links to avatars of the user;
> **Historical note:** the above values were changed in version `0.3.0`.
> `diaspora_id` became `handle` and `image_urls` became `avatar` to have more
> consistent results.
> This is because we can get only user data and this returns dict containing
> `handle` and `avatar` and not `diaspora_id` and `image_urls`.
> Users who migrated from version `0.2.x` and before to version `0.3.0` had to
> update their software.
Also `User` object contains a stream for this user.
* `stream`, `diaspy.streams.Outer`, stream of the user (provides all methods of generic stream);
====
#### `Contacts()` object
This is object abstracting list of user's contacts.
It may be slightly confusing to use and reading just docs could be not enough.
The only method of this object is `get()` and its only parameter is `set` which
is optional (defaults to empty string).
If called without specifying `set` `get()` will return list of users (`User` objects)
who are in your aspects.
Optional `set` parameter can be either `all` or `only_sharing`.
If passed as `only_sharing` it will return only users who are not in your aspects but who share
with you - which means that you are in their aspects.
If passed as `all` it will return list of *all* your contacts - those who are in your aspects and
those who are not.
To sum up: people *who you share with* are *in* your aspects. People *who share with you* have you in
their aspects. These two states can be mixed.
----
###### Manual for `diaspy`, written by Marek Marecki
diaspy-0.5.1/manual/posting.markdown 0000664 0000000 0000000 00000003067 12644042615 0017524 0 ustar 00root root 0000000 0000000 #### `Post()` object and posting
`Post` object is used to represent a post on D\*.
----
##### Posting
Posting is done through a `Stream` object method `post()`.
It supports posting just text, images or text and images.
`Stream().post()` returns `Post` object referring to the post
which have just been created.
##### Text
If you want to post just text you should call `post()` method with
`text` argument.
stream.post(text='Your post.')
It will return `Post` you have just created.
##### Posting images
Posting images, from back-end point of view, is a two-step process.
First, you have to *upload* an image to the desired pod.
This is done by `_photoupload()` method.
It will return *id* of uploaded image.
Then you have to actually post your image and this is done by appending
`photos` field containg the id of uploaded image to the data being
sent by request. This is handled by `post()` method.
`post()` has two very similar arguments used for posting photos.
They are `photos` - which takes id and `photo` - which takes filename.
You can post images using either of them. Even passing them side by side
is accepted but remember that `photos` will overwrite data set by `photo`.
Example #1: using `photo`
stream.post(photo='./kitten-image.png')
Example #2: using `photos`
id = stream._photoupload(filename='./kitten-image.png')
stream.post(photos=id)
The effect will be the same.
To either call you can append `text` argument which will be posted alongside
the image.
----
###### Manual for `diaspy`, written by Marek Marecki
diaspy-0.5.1/manual/search.markdown 0000664 0000000 0000000 00000000642 12644042615 0017302 0 ustar 00root root 0000000 0000000 #### `Search()` object
Searching is useful only if it comes to searching for users.
Tags can be searched for just by creating `Tag` object using
tag-name as an arument.
----
##### `lookup_user()`
This method is used for telling a pod "Hey, I want this user searchable via you!"
##### `user()`
This method will return you a list of dictionaries containg data of users whose
handle conatins query you have used.
diaspy-0.5.1/manual/streams.markdown 0000664 0000000 0000000 00000003461 12644042615 0017515 0 ustar 00root root 0000000 0000000 #### `Stream()` object
This object is used to represent user's stream on D\*.
It is returned by `Client()`'s method `get_stream()` and
is basically a list of posts.
----
##### Getting stream
To get basic stream you have to have working `Connection()` as
this is required by `Stream()`'s constructor.
c = diaspy.connection.Connection(pod='https://pod.example.com',
username='foo',
password='bar')
c.login()
stream = diaspy.models.Stream(c)
Now you have a stream filled with posts (if any can be found on user's stream).
----
##### `fill()`, `update()` and `more()`
When you want to refresh stream call it's `fill()` method. It will overwrite old stream
contents.
On the contrary, `update()` will get a new stream but will not overwrite old stream saved
in the object memory. It will append every new post to the old stream.
`more()` complements `update()` it will fetch you older posts instead of newer ones.
----
##### Length of and iterating over a stream
Stream's length can be checked by calling `len()` on it.
len(stream)
10
When you want to iterate over a stream (e.g. when you want to print first *n* posts on
the stream) you can do it in two ways.
First, using `len()` and `range()` functions.
for i in range(len(stream)):
# do stuff...
Second, iterating directly over the stream contents:
for post in stream:
# do stuff...
----
##### Posting data to stream
This is described in [`posting`](./posting.markdown) document in this manual.
----
##### Clearing stream
##### `clear()`
This will remove all posts from visible stream.
##### `purge()`
This will scan stream for nonexistent posts (eg. deleted) and remove them.
----
###### Manual for `diaspy`, written by Marek Marecki
diaspy-0.5.1/requirements.txt 0000664 0000000 0000000 00000000027 12644042615 0016275 0 ustar 00root root 0000000 0000000 diaspy
requests==1.1.0
diaspy-0.5.1/setup.py 0000664 0000000 0000000 00000001640 12644042615 0014525 0 ustar 00root root 0000000 0000000 from setuptools import setup, find_packages
setup(
name='diaspy-api',
version='0.5.1',
author='Marek Marecki',
author_email='marekjm@ozro.pw',
url='https://github.com/marekjm/diaspy',
license='GNU GPL v3',
description='Unofficial Python API for Diaspora*',
keywords='diaspy diaspora social network api',
classifiers = [
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Software Development :: Libraries',
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Utilities',
],
packages=find_packages(),
install_requires=['requests']
)
diaspy-0.5.1/test-image.png 0000664 0000000 0000000 00000024762 12644042615 0015572 0 ustar 00root root 0000000 0000000 PNG
IHDR g- bKGD C pHYs tIMELr iTXtComment Created with GIMPd.e IDATx]kl?}y? @ P@(j4$>*Z)jDjJUV6I+i&W
! yk 5^ 5gI
~)? M~ '(7@#Q&=e[