├── steemconnect ├── __init__.py ├── defaults.py ├── mixins.py ├── utils.py ├── client.py └── operations.py ├── setup.py ├── README.md ├── examples └── login_app │ └── login_app.py ├── docs ├── hot-signing.rst ├── example-flask-app.rst ├── index.rst ├── gettingstarted.rst ├── usingtheaccesstoken.rst ├── Makefile └── conf.py ├── LICENSE ├── .gitignore └── tests.py /steemconnect/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /steemconnect/defaults.py: -------------------------------------------------------------------------------- 1 | OAUTH_BASE_URL = "https://steemconnect.com/oauth2/" 2 | SC2_API_BASE_URL = "https://steemconnect.com/api/" -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='steemconnect', 5 | version='0.0.6', 6 | packages=['steemconnect'], 7 | url='https://github.com/emre/steemconnect', 8 | license='MIT', 9 | author='Emre Yilmaz', 10 | author_email='mail@emreyilmaz.me', 11 | description='Python client library of SteemConnect', 12 | install_requires=["requests", "responses"] 13 | ) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | steemconnect-python-client is a simple yet powerful library to interact with the [Steemconnect](https://steemconnect.com). There was no *production ready* library for Python (Or I couldn't find it.) so I have decided to write my own. 2 | 3 | #### Installation 4 | 5 | ``` 6 | $ (sudo) pip install steemconnect 7 | ``` 8 | 9 | #### Documentation 10 | 11 | https://steemconnect-python-client.readthedocs.io/en/latest/ 12 | -------------------------------------------------------------------------------- /steemconnect/mixins.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class CustomJsonMixin: 5 | 6 | def get_base_op_structure(self, required_auths, required_posting_auths, 7 | custom_operation_id, structure): 8 | return [ 9 | "custom_json", { 10 | "required_auths": required_auths, 11 | "required_posting_auths": required_posting_auths, 12 | "id": custom_operation_id, 13 | "json": json.dumps(structure), 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /examples/login_app/login_app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from steemconnect.client import Client 3 | 4 | app = Flask(__name__) 5 | 6 | 7 | client_id = "your.app" 8 | client_secret = "your_secret" 9 | 10 | c = Client(client_id=client_id, client_secret=client_secret) 11 | 12 | 13 | @app.route('/') 14 | def index(): 15 | login_url = c.get_login_url( 16 | "http://localhost:5000/welcome", 17 | "login", 18 | ) 19 | return "Login with SteemConnect" % login_url 20 | 21 | 22 | @app.route('/welcome') 23 | def welcome(): 24 | c.access_token = request.args.get("access_token") 25 | return "Welcome %s!" % c.me()["name"] -------------------------------------------------------------------------------- /steemconnect/utils.py: -------------------------------------------------------------------------------- 1 | def requires_access_token(func): 2 | def _decorated(self, *args, **kwargs): 3 | if not self.access_token: 4 | raise ValueError("you need to set your access_token" 5 | " property for this method.") 6 | 7 | return func(self, *args, **kwargs) 8 | return _decorated 9 | 10 | 11 | def requires_client_id_and_secret(func): 12 | def _decorated(self, *args, **kwargs): 13 | if not self.client_id or not self.client_secret: 14 | raise ValueError("you need to set your client_id and client_secret" 15 | " properties for this method.") 16 | 17 | return func(self, *args, **kwargs) 18 | return _decorated 19 | -------------------------------------------------------------------------------- /docs/hot-signing.rst: -------------------------------------------------------------------------------- 1 | 2 | Hot signing 3 | ================================= 4 | 5 | client's hot_sign() method creates a SteemConnect specific URL which you can redirect users and expect them 6 | to broadcast operations are not supported in the api. (transfer, create_delegation, etc.) 7 | 8 | .. function:: hot_sign(self, operation, params, redirect_uri=None): 9 | 10 | 11 | :param operation: String. Operation name. Ex: transfer. 12 | :param params: Dict. Operation data. 13 | :param redirect_uri: String. Optional. If you pass that, SteemConnect will redirect 14 | the user to that URL after the operation succeeds. 15 | 16 | Example: A transfer to emrebeyler with 1 SBD 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | .. code-block:: python 20 | 21 | url = self.c.hot_sign( 22 | "transfer", 23 | { 24 | "to": "emrebeyler", 25 | "amount": "1 SBD", 26 | "memo": "Donation", 27 | }, 28 | redirect_uri="http://localhost" 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Emre Yılmaz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/example-flask-app.rst: -------------------------------------------------------------------------------- 1 | 2 | Example flask application 3 | ================================= 4 | 5 | This simple flask application redirects the user to steemconnect for the authorization. Once 6 | the user authorizes your app, it calls /me endpoint and gives a warm welcome message with the "name" property of the user. 7 | 8 | .. figure:: https://steemitimages.com/0x0/https://gateway.ipfs.io/ipfs/QmaixSnnT5df3vN9vVAr7KKULL1aBoYRRrBSQ6Mj9qKuhd 9 | 10 | 11 | .. code-block:: python 12 | 13 | from flask import Flask, request 14 | from steemconnect.client import Client 15 | 16 | app = Flask(__name__) 17 | 18 | 19 | client_id = "your.app" 20 | client_secret = "your_secret" 21 | 22 | c = Client(client_id=client_id, client_secret=client_secret) 23 | 24 | 25 | @app.route('/') 26 | def index(): 27 | login_url = c.get_login_url( 28 | "http://localhost:5000/welcome", 29 | "login", 30 | ) 31 | return "Login with SteemConnect" % login_url 32 | 33 | 34 | @app.route('/welcome') 35 | def welcome(): 36 | c.access_token = request.args.get("access_token") 37 | return "Welcome %s!" % c.me()["name"] 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | 3 | steemconnect-python-client 4 | ====================================================== 5 | 6 | steemconnect-python-client is a simple **yet powerful** library to interact with the Steemconnect. Steemconnect 7 | is a central-single sign on solution for steem based applications. It's considered a secure layer for certain 8 | actions and backed by the Steemit inc. 9 | 10 | Steemconnect implements Oauth2 for the authorization logic. 11 | 12 | What can you do with this client? 13 | ---------- 14 | 15 | - Implementing Authorization/Authentication flow through OAuth 16 | - Broadcasting supported operations to the STEEM blockchain with the user of your app. 17 | 18 | Installation 19 | ------------- 20 | 21 | steemconnect-python-client requires python3.6 and above. Even though it's easy to make it compatible 22 | with lower versions, it's doesn't have support by design to keep the library simple. 23 | 24 | You can install the library by typing to your console: 25 | 26 | .. code-block:: bash 27 | 28 | $ (sudo) pip install steemconnect 29 | 30 | After that, you can continue with :doc:`/gettingstarted`. 31 | 32 | Documentation Pages 33 | ----------- 34 | 35 | .. toctree:: 36 | :maxdepth: 5 37 | 38 | gettingstarted 39 | usingtheaccesstoken 40 | broadcast 41 | hot-signing 42 | example-flask-app 43 | 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | flow_tests.py 104 | op_tests.py -------------------------------------------------------------------------------- /docs/gettingstarted.rst: -------------------------------------------------------------------------------- 1 | 2 | Getting Started 3 | ================================= 4 | 5 | Steemconnect supports two different oauth flows for authorization. 6 | 7 | Implicit grant flow 8 | ~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | - You create an authorization link with your app's client id and permission scopes. 11 | - User visits the page (steemconnect) and authorizes the application with the given permission scopes. 12 | - steemconnect returns user to the your app with a access token in the query string. 13 | 14 | From that point, you can use this access token to broadcast operations on user's behalf. 15 | 16 | Code authorization flow 17 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | 19 | access tokens has a short TTL on Oauth standards. Every time a user has their token expired, you have two choices: 20 | 21 | - Re-log the user and get a fresh token 22 | - Add "offline" scope to the required scopes and get a refresh token to refresh the access tokens. 23 | 24 | The second approach is required on some cases for the dApps and you will need to use Code authorization flow this. 25 | When you add the "offline" scope to the required scopes, you will get a code instead of access token. 26 | 27 | With this code, you can get new access tokens till forever. *(As long as the user don't revoke access of your app.)* 28 | 29 | .. note :: 30 | All of these flows are covered at steemconnect-python-client, however have a look to `Steemconnect wiki `_. 31 | to learn more about it. 32 | 33 | 34 | Creating your app on Steemconnect 35 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | 37 | 38 | .. figure:: https://camo.githubusercontent.com/73676c4237e95b24365ef8dfea78119e19237108/68747470733a2f2f7331342e706f7374696d672e63632f6b65716437727268742f53637265656e5f53686f745f323031382d30342d31395f61745f372e35342e35385f504d2e706e67 39 | :width: 600 40 | 41 | You need to `register your app `_ into steemconnect before working with them. This will provide client_id and client_secret information which you will need to interact with the API. 42 | 43 | 44 | Redirecting user 45 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | .. code-block:: python 48 | 49 | from steemconnect.client import Client 50 | 51 | c = Client( 52 | client_id="app_name", 53 | client_secret="client_secret", 54 | ) 55 | 56 | 57 | At this point, we need to redirect the user to steemconnect for they to log in. That requires creating a URL. 58 | 59 | .. code-block:: python 60 | 61 | auth_url = c.get_login_url( 62 | "http://callback.to.your.app", 63 | "login,vote", 64 | ) 65 | 66 | - The first parameter is the callback URL when the user authorizes your app on SteemConnect. 67 | - Second parameter is the scopes you need. Available scopes are listed at `Steemconnect wiki `_. 68 | 69 | .. important :: 70 | If you need to use the Code authorization flow, you need to pass **get_refresh_token=True** to this function. Also, "offline" scope is mandatory. 71 | 72 | Once the user authorizes your app, steemconnect will redirect the user to your app with an access token or code depending the flow you choose. 73 | If you get a **code** in the query string, you can use this code to create access tokens for the specified user. 74 | 75 | .. code-block :: python 76 | 77 | c.get_access_token( 78 | code, 79 | ) 80 | 81 | Example output 82 | 83 | .. code-block :: javascript 84 | 85 | { 86 | 'access_token': 'access_token_string', 87 | 'expires_in': 604800, 88 | 'username': 'emrebeyler', 89 | 'refresh_token': 'refresh_token_string' 90 | } 91 | 92 | If you use the Implicit grant flow, then you may skip this step. 93 | 94 | Continue with :doc:`/usingtheaccesstoken` to learn what can you do with the access tokens. 95 | 96 | -------------------------------------------------------------------------------- /docs/usingtheaccesstoken.rst: -------------------------------------------------------------------------------- 1 | 2 | Using access tokens 3 | ================================= 4 | 5 | Once you get the access token, you can create a new Client instance with just access_token. 6 | 7 | .. code-block:: python 8 | 9 | c = Client( 10 | access_token="", 11 | ) 12 | 13 | 14 | Getting authorized user's information 15 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 16 | 17 | This api call gives information about the authorized user. 18 | 19 | .. code-block:: python 20 | 21 | print(c.me()) 22 | 23 | 24 | Updating user profile (metadata) 25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | 28 | .. code-block:: python 29 | 30 | metadata = { 31 | "profile": { 32 | "name": "Emre", 33 | "location": "Istanbul, Turkey", 34 | "about": "Developer, STEEM witness.", 35 | "profile_image": "http://foo.bar/image.png" 36 | } 37 | } 38 | 39 | resp = c.update_user_metadata(metadata) 40 | 41 | 42 | Broadcasting operations 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | 45 | It's possible to 46 | 47 | - vote a post 48 | - create a post/comment 49 | - follow/unfollow/ignore/resteem 50 | - claim reward balance 51 | - delete comment 52 | - create custom jsons 53 | 54 | via steemconnect's broadcast apis. 55 | 56 | 57 | .. note:: 58 | All operations live inside the **steemconnect.operations** module. You need to import the corresponding classes before using them. 59 | 60 | 61 | Voting for a post 62 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | 64 | .. code-block:: python 65 | 66 | vote = Vote("account", "author", "permlink", percent) 67 | c.broadcast([vote.to_operation_structure()]) 68 | 69 | Creating a comment/post 70 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 71 | 72 | .. code-block:: python 73 | 74 | comment = Comment( 75 | "author", 76 | "permlink", 77 | "body", 78 | title="test title", 79 | json_metadata={"app":"foo/0.0.1"}, 80 | ) 81 | c.broadcast([comment.to_operation_structure()]) 82 | 83 | Creating a comment/post with CommentOptions 84 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 85 | 86 | .. code-block:: python 87 | 88 | comment = Comment( 89 | "author", 90 | "permlink", 91 | "body", 92 | title="test title", 93 | json_metadata={"app":"foo/0.0.1"}, 94 | ) 95 | 96 | comment_options = CommentOptions( 97 | parent_comment=comment, 98 | allow_curation_rewards=False, 99 | ) 100 | 101 | c.broadcast([ 102 | comment.to_operation_structure(), 103 | comment_options.to_operation_structure() 104 | ]) 105 | 106 | 107 | 108 | Follow an account 109 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 110 | 111 | .. code-block:: python 112 | 113 | follow = Follow("follower", "following") 114 | c.broadcast([follow.to_operation_structure()]) 115 | 116 | 117 | Unfollow an account 118 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 119 | 120 | .. code-block:: python 121 | 122 | unfollow = Unfollow("follower", "following") 123 | c.broadcast([unfollow.to_operation_structure()]) 124 | 125 | Mute an account 126 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 127 | 128 | .. code-block:: python 129 | 130 | ignore = Mute("follower", "following") 131 | c.broadcast([ignore.to_operation_structure()]) 132 | 133 | 134 | Resteem a post 135 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 136 | 137 | .. code-block:: python 138 | 139 | resteem = Resteem("account", "author", "permlink") 140 | c.broadcast([resteem.to_operation_structure()]) 141 | 142 | 143 | Claim reward balance 144 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 145 | 146 | .. code-block:: python 147 | 148 | claim_reward_balance = ClaimRewardBalance('account', '0.000 STEEM', '1.500 SBD', '1132.996000 VESTS') 149 | c.broadcast([claim_reward_balance.to_operation_structure()]) 150 | 151 | Delete comment 152 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 153 | 154 | .. code-block:: python 155 | 156 | delete_comment = DeleteComment( 157 | "author", "permlink" 158 | ) 159 | c.broadcast([delete_comment.to_operation_structure()]) 160 | 161 | Create custom jsons 162 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 163 | 164 | .. code-block:: python 165 | 166 | custom_json = CustomJson( 167 | required_auth, 168 | required_posting_auths, 169 | id 170 | json_structure, 171 | ) 172 | c.broadcast([custom_json.to_operation_structure()]) 173 | 174 | -------------------------------------------------------------------------------- /steemconnect/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib.parse 3 | 4 | import requests 5 | 6 | from .defaults import (OAUTH_BASE_URL, SC2_API_BASE_URL) 7 | from .utils import (requires_access_token, requires_client_id_and_secret) 8 | 9 | 10 | class Client: 11 | 12 | def __init__(self, client_id=None, client_secret=None, access_token=None, 13 | oauth_base_url=None, sc2_api_base_url=None): 14 | self.client_id = client_id 15 | self.client_secret = client_secret 16 | self.access_token = access_token 17 | self.oauth_base_url = oauth_base_url or OAUTH_BASE_URL 18 | self.sc2_api_base_url = sc2_api_base_url or SC2_API_BASE_URL 19 | 20 | @property 21 | def headers(self): 22 | return {'Authorization': self.access_token} 23 | 24 | def get_login_url(self, redirect_uri, scope, 25 | get_refresh_token=False): 26 | params = { 27 | "client_id": self.client_id, 28 | "redirect_uri": redirect_uri, 29 | "scope": scope, 30 | } 31 | if get_refresh_token: 32 | params.update({ 33 | "response_type": "code", 34 | }) 35 | 36 | return urllib.parse.urljoin( 37 | self.oauth_base_url, 38 | "authorize?" + urllib.parse.urlencode(params)) 39 | 40 | @requires_client_id_and_secret 41 | def get_access_token(self, code): 42 | post_data = { 43 | "grant_type": "authorization_code", 44 | "code": code, 45 | "client_id": self.client_id, 46 | "client_secret": self.client_secret, 47 | } 48 | 49 | r = requests.post( 50 | urllib.parse.urljoin(self.sc2_api_base_url, "oauth2/token/"), 51 | data=post_data 52 | ) 53 | 54 | return r.json() 55 | 56 | @requires_access_token 57 | def me(self): 58 | url = urllib.parse.urljoin(self.sc2_api_base_url, "me/") 59 | r = requests.post(url, headers=self.headers) 60 | return r.json() 61 | 62 | @requires_access_token 63 | def broadcast(self, operations): 64 | url = urllib.parse.urljoin(self.sc2_api_base_url, "broadcast/") 65 | data = { 66 | "operations": operations, 67 | } 68 | headers = self.headers.copy() 69 | headers.update({ 70 | "Content-Type": "application/json; charset=utf-8", 71 | }) 72 | 73 | r = requests.post(url, headers=headers, data=json.dumps(data)) 74 | try: 75 | return r.json() 76 | except ValueError: 77 | return r.content 78 | 79 | @requires_client_id_and_secret 80 | def refresh_access_token(self, refresh_token, scope): 81 | post_data = { 82 | "refresh_token": refresh_token, 83 | "client_id": self.client_id, 84 | "client_secret": self.client_secret, 85 | "scope": scope, 86 | } 87 | 88 | r = requests.post( 89 | urllib.parse.urljoin(self.sc2_api_base_url, "oauth2/token/"), 90 | data=post_data 91 | ) 92 | 93 | return r.json() 94 | 95 | def revoke_token(self, access_token): 96 | post_data = { 97 | "access_token": access_token, 98 | } 99 | 100 | r = requests.post( 101 | urllib.parse.urljoin(self.sc2_api_base_url, "oauth2/token/revoke"), 102 | data=post_data 103 | ) 104 | 105 | return r.json() 106 | 107 | @requires_access_token 108 | def update_user_metadata(self, metadata): 109 | put_data = { 110 | "user_metadata": metadata, 111 | } 112 | r = requests.put( 113 | urllib.parse.urljoin(self.sc2_api_base_url, "me/"), 114 | data=put_data, headers=self.headers) 115 | 116 | return r.json() 117 | 118 | def hot_sign(self, operation, params, redirect_uri=None): 119 | 120 | if not isinstance(operation, str) or not isinstance(params, dict): 121 | raise ValueError("Invalid Request.") 122 | 123 | base_url = self.sc2_api_base_url.replace("/api", "") 124 | 125 | if redirect_uri: 126 | params.update({"redirect_uri": redirect_uri}) 127 | 128 | params = urllib.parse.urlencode(params) 129 | url = urllib.parse.urljoin(base_url, "sign/%s" % operation) 130 | url += "?" + params 131 | 132 | return url 133 | 134 | 135 | -------------------------------------------------------------------------------- /steemconnect/operations.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from .mixins import CustomJsonMixin 4 | 5 | 6 | class Vote: 7 | 8 | def __init__(self, voter, author, permlink, percent): 9 | self.voter = voter 10 | self.author = author 11 | self.permlink = permlink 12 | self.weight = percent * 100 13 | 14 | def to_operation_structure(self): 15 | return [ 16 | "vote", { 17 | "voter": self.voter, 18 | "author": self.author, 19 | "permlink": self.permlink, 20 | "weight": self.weight 21 | } 22 | ] 23 | 24 | 25 | class Comment: 26 | 27 | def __init__(self, author, permlink, body, title=None, 28 | parent_author=None, parent_permlink=None, json_metadata=None): 29 | self.author = author 30 | self.permlink = permlink 31 | self.body = body 32 | self.title = title 33 | self.parent_author = parent_author 34 | self.parent_permlink = parent_permlink 35 | self.json_metadata = json.dumps(json_metadata) if json_metadata else '' 36 | 37 | def to_operation_structure(self): 38 | return [ 39 | "comment", { 40 | "parent_author": self.parent_author or "", 41 | "parent_permlink": self.parent_permlink, 42 | "author": self.author, 43 | "permlink": self.permlink, 44 | "title": self.title or "", 45 | "body": self.body, 46 | "json_metadata": self.json_metadata or "", 47 | } 48 | ] 49 | 50 | 51 | class CommentOptions: 52 | 53 | def __init__(self, author=None, permlink=None, extensions=None, 54 | allow_curation_rewards=None, max_accepted_payout=None, 55 | percent_steem_dollars=0, allow_votes=True, 56 | parent_comment=None): 57 | if parent_comment: 58 | self.author = parent_comment.author 59 | self.permlink = parent_comment.permlink 60 | else: 61 | self.author = author 62 | self.permlink = permlink 63 | self.extensions = extensions or [] 64 | self.allow_curation_rewards = allow_curation_rewards or True 65 | self.max_accepted_payout = max_accepted_payout or "1000.000 SBD" 66 | self.percent_steem_dollars = percent_steem_dollars 67 | self.allow_votes = allow_votes or True 68 | 69 | def to_operation_structure(self): 70 | return [ 71 | "comment_options", { 72 | "author": self.author, 73 | "permlink": self.permlink or "", 74 | "allow_curation_rewards": self.allow_curation_rewards, 75 | "max_accepted_payout": self.max_accepted_payout, 76 | "percent_steem_dollars": self.percent_steem_dollars, 77 | "allow_votes": self.allow_votes, 78 | "extensions": self.extensions, 79 | } 80 | ] 81 | 82 | 83 | class DeleteComment: 84 | 85 | def __init__(self, author, permlink): 86 | self.author = author 87 | self.permlink = permlink 88 | 89 | def to_operation_structure(self): 90 | return [ 91 | "delete_comment", { 92 | "author": self.author, 93 | "permlink": self.permlink 94 | } 95 | ] 96 | 97 | 98 | class Follow(CustomJsonMixin): 99 | 100 | def __init__(self, follower, following): 101 | self.follower = follower 102 | self.following = following 103 | self.what = ["blog"] 104 | 105 | def to_operation_structure(self): 106 | return self.get_base_op_structure( 107 | [], 108 | [self.follower], 109 | "follow", 110 | ["follow", { 111 | "follower": self.follower, 112 | "following": self.following, 113 | "what": self.what, 114 | }] 115 | ) 116 | 117 | 118 | class Unfollow(Follow): 119 | 120 | def __init__(self, *args, **kwargs): 121 | super().__init__(*args, **kwargs) 122 | self.what = [] 123 | 124 | 125 | class Mute(Follow): 126 | def __init__(self, *args, **kwargs): 127 | super().__init__(*args, **kwargs) 128 | self.what = ["ignore"] 129 | 130 | 131 | class Resteem(CustomJsonMixin): 132 | 133 | def __init__(self, account, author, permlink): 134 | self.account = account 135 | self.author = author 136 | self.permlink = permlink 137 | 138 | def to_operation_structure(self): 139 | return self.get_base_op_structure( 140 | [], 141 | [self.account], 142 | "follow", 143 | ["reblog", { 144 | "account": self.account, 145 | "author": self.author, 146 | "permlink": self.permlink, 147 | }] 148 | ) 149 | 150 | 151 | class ClaimRewardBalance: 152 | 153 | def __init__(self, account, reward_steem, reward_sbd, reward_vests): 154 | self.account = account 155 | self.reward_steem = reward_steem 156 | self.reward_vests = reward_vests 157 | self.reward_sbd = reward_sbd 158 | 159 | def to_operation_structure(self): 160 | return [ 161 | "claim_reward_balance", { 162 | "account": self.account, 163 | "reward_steem": self.reward_steem, 164 | "reward_sbd": self.reward_sbd, 165 | "reward_vests": self.reward_vests, 166 | } 167 | ] 168 | 169 | 170 | class CustomJson(CustomJsonMixin): 171 | 172 | def __init__(self, required_auth, required_posting_auths, 173 | custom_operation_id, structure): 174 | self.required_auth = required_auth 175 | self.required_posting_auths = required_posting_auths 176 | self.custom_operation_id = custom_operation_id 177 | self.structure = structure 178 | 179 | def to_operation_structure(self): 180 | return self.get_base_op_structure( 181 | self.required_auth, 182 | self.required_posting_auths, 183 | self.custom_operation_id, 184 | self.structure 185 | ) 186 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/steemconnect-python-client.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/steemconnect-python-client.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/steemconnect-python-client" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/steemconnect-python-client" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # steemconnect-python-client documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Aug 25 21:55:41 2018. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'steemconnect-python-client' 47 | copyright = u'2018, emrebeyler' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.0.4' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.0.4' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all 73 | # documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | # If true, keep warnings as "system message" paragraphs in the built documents. 94 | #keep_warnings = False 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'sphinx_rtd_theme' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | html_theme_options = { 107 | 108 | } 109 | 110 | # Add any paths that contain custom themes here, relative to this directory. 111 | #html_theme_path = [] 112 | 113 | # The name for this set of Sphinx documents. If None, it defaults to 114 | # " v documentation". 115 | #html_title = None 116 | 117 | # A shorter title for the navigation bar. Default is the same as html_title. 118 | html_short_title = "steemconnect" 119 | 120 | # The name of an image file (relative to this directory) to place at the top 121 | # of the sidebar. 122 | #html_logo = None 123 | 124 | # The name of an image file (within the static path) to use as favicon of the 125 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 126 | # pixels large. 127 | #html_favicon = None 128 | 129 | # Add any paths that contain custom static files (such as style sheets) here, 130 | # relative to this directory. They are copied after the builtin static files, 131 | # so a file named "default.css" will overwrite the builtin "default.css". 132 | html_static_path = ['_static'] 133 | 134 | # Add any extra paths that contain custom files (such as robots.txt or 135 | # .htaccess) here, relative to this directory. These files are copied 136 | # directly to the root of the documentation. 137 | #html_extra_path = [] 138 | 139 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 140 | # using the given strftime format. 141 | #html_last_updated_fmt = '%b %d, %Y' 142 | 143 | # If true, SmartyPants will be used to convert quotes and dashes to 144 | # typographically correct entities. 145 | #html_use_smartypants = True 146 | 147 | # Custom sidebar templates, maps document names to template names. 148 | #html_sidebars = {} 149 | 150 | # Additional templates that should be rendered to pages, maps page names to 151 | # template names. 152 | #html_additional_pages = {} 153 | 154 | # If false, no module index is generated. 155 | #html_domain_indices = True 156 | 157 | # If false, no index is generated. 158 | #html_use_index = True 159 | 160 | # If true, the index is split into individual pages for each letter. 161 | #html_split_index = False 162 | 163 | # If true, links to the reST sources are added to the pages. 164 | #html_show_sourcelink = True 165 | 166 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 167 | #html_show_sphinx = True 168 | 169 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 170 | #html_show_copyright = True 171 | 172 | # If true, an OpenSearch description file will be output, and all pages will 173 | # contain a tag referring to it. The value of this option must be the 174 | # base URL from which the finished HTML is served. 175 | #html_use_opensearch = '' 176 | 177 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 178 | #html_file_suffix = None 179 | 180 | # Output file base name for HTML help builder. 181 | htmlhelp_basename = 'steemconnect-python-clientdoc' 182 | 183 | 184 | # -- Options for LaTeX output --------------------------------------------- 185 | 186 | latex_elements = { 187 | # The paper size ('letterpaper' or 'a4paper'). 188 | #'papersize': 'letterpaper', 189 | 190 | # The font size ('10pt', '11pt' or '12pt'). 191 | #'pointsize': '10pt', 192 | 193 | # Additional stuff for the LaTeX preamble. 194 | #'preamble': '', 195 | } 196 | 197 | # Grouping the document tree into LaTeX files. List of tuples 198 | # (source start file, target name, title, 199 | # author, documentclass [howto, manual, or own class]). 200 | latex_documents = [ 201 | ('index', 'steemconnect-python-client.tex', u'steemconnect-python-client Documentation', 202 | u'emrebeyler', 'manual'), 203 | ] 204 | 205 | # The name of an image file (relative to this directory) to place at the top of 206 | # the title page. 207 | #latex_logo = None 208 | 209 | # For "manual" documents, if this is true, then toplevel headings are parts, 210 | # not chapters. 211 | #latex_use_parts = False 212 | 213 | # If true, show page references after internal links. 214 | #latex_show_pagerefs = False 215 | 216 | # If true, show URL addresses after external links. 217 | #latex_show_urls = False 218 | 219 | # Documents to append as an appendix to all manuals. 220 | #latex_appendices = [] 221 | 222 | # If false, no module index is generated. 223 | #latex_domain_indices = True 224 | 225 | 226 | # -- Options for manual page output --------------------------------------- 227 | 228 | # One entry per manual page. List of tuples 229 | # (source start file, name, description, authors, manual section). 230 | man_pages = [ 231 | ('index', 'steemconnect-python-client', u'steemconnect-python-client Documentation', 232 | [u'emrebeyler'], 1) 233 | ] 234 | 235 | # If true, show URL addresses after external links. 236 | #man_show_urls = False 237 | 238 | 239 | # -- Options for Texinfo output ------------------------------------------- 240 | 241 | # Grouping the document tree into Texinfo files. List of tuples 242 | # (source start file, target name, title, author, 243 | # dir menu entry, description, category) 244 | texinfo_documents = [ 245 | ('index', 'steemconnect-python-client', u'steemconnect-python-client Documentation', 246 | u'emrebeyler', 'steemconnect-python-client', 'One line description of project.', 247 | 'Miscellaneous'), 248 | ] 249 | 250 | # Documents to append as an appendix to all manuals. 251 | #texinfo_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | #texinfo_domain_indices = True 255 | 256 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 257 | #texinfo_show_urls = 'footnote' 258 | 259 | # If true, do not generate a @detailmenu in the "Top" node's menu. 260 | #texinfo_no_detailmenu = False 261 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import responses 4 | 5 | from steemconnect.client import Client 6 | from steemconnect.operations import * 7 | 8 | 9 | class TestClient(unittest.TestCase): 10 | 11 | def test_get_login_url(self): 12 | c = Client( 13 | "client_id" 14 | "client_secret" 15 | ) 16 | 17 | self.assertEqual( 18 | c.get_login_url("http://localhost", "login"), 19 | "https://v2.steemconnect.com/oauth2/authorize?client_id" 20 | "=client_idclient_secret&redirect_uri=http%3A%2F%2Flocalh" 21 | "ost&scope=login") 22 | 23 | def test_get_login_url_override_defaults(self): 24 | c = Client( 25 | oauth_base_url="http://foo.bar/oauth2/", 26 | client_id="client_id", 27 | client_secret="client_secret" 28 | ) 29 | 30 | self.assertEqual( 31 | c.get_login_url("http://localhost", "login"), 32 | "http://foo.bar/oauth2/authorize?client_id=client_id&" 33 | "redirect_uri=http%3A%2F%2Flocalhost&scope=login") 34 | 35 | @responses.activate 36 | def test_get_access_token(self): 37 | 38 | def request_callback(request): 39 | self.assertEqual( 40 | request.url, 41 | "https://v2.steemconnect.com/api/oauth2/token/") 42 | 43 | self.assertEqual( 44 | request.body, 45 | 'grant_type=authorization_code&code=code&client_id=' 46 | 'client_id&client_secret=client_secret' 47 | ) 48 | return 200, {}, json.dumps({"access_token": "foo"}) 49 | 50 | c = Client( 51 | client_id="client_id", 52 | client_secret="client_secret" 53 | ) 54 | responses.add_callback( 55 | responses.POST, 56 | 'https://v2.steemconnect.com/api/oauth2/token/', 57 | callback=request_callback, 58 | ) 59 | 60 | c.get_access_token("code") 61 | 62 | @responses.activate 63 | def test_refresh_access_token(self): 64 | 65 | def request_callback(request): 66 | self.assertEqual( 67 | request.url, 68 | "https://v2.steemconnect.com/api/oauth2/token/") 69 | 70 | self.assertEqual( 71 | request.body, 72 | 'refresh_token=refresh_token&client_id=client_id&' 73 | 'client_secret=client_secret&scope=login' 74 | ) 75 | return 200, {}, json.dumps({"access_token": "foo"}) 76 | 77 | c = Client( 78 | client_id="client_id", 79 | client_secret="client_secret" 80 | ) 81 | responses.add_callback( 82 | responses.POST, 83 | 'https://v2.steemconnect.com/api/oauth2/token/', 84 | callback=request_callback, 85 | ) 86 | 87 | c.refresh_access_token("refresh_token", "login") 88 | 89 | @responses.activate 90 | def test_me(self): 91 | 92 | def request_callback(request): 93 | self.assertEqual( 94 | request.url, 95 | "https://v2.steemconnect.com/api/me/") 96 | 97 | return 200, {}, json.dumps({"access_token": "foo"}) 98 | 99 | c = Client( 100 | access_token="foo" 101 | ) 102 | responses.add_callback( 103 | responses.POST, 104 | 'https://v2.steemconnect.com/api/me/', 105 | callback=request_callback, 106 | ) 107 | 108 | c.me() 109 | 110 | def test_op_upvote(self): 111 | vote = Vote( 112 | "voter", "author", "permlink", 100) 113 | 114 | self.assertEqual(vote.voter, "voter") 115 | self.assertEqual(vote.author, "author") 116 | self.assertEqual(vote.permlink, "permlink") 117 | self.assertEqual(vote.weight, 10000) 118 | 119 | def test_op_downvote(self): 120 | vote = Vote( 121 | "voter", "author", "permlink", -50) 122 | 123 | self.assertEqual(vote.voter, "voter") 124 | self.assertEqual(vote.author, "author") 125 | self.assertEqual(vote.permlink, "permlink") 126 | self.assertEqual(vote.weight, -5000) 127 | 128 | def test_comment(self): 129 | comment = Comment( 130 | "author", 131 | "permlink", 132 | "body", 133 | title="title", 134 | json_metadata={"app": "testapp/0.0.1", "tags": ["test"]}, 135 | ) 136 | 137 | self.assertEqual(comment.author, "author") 138 | self.assertEqual(comment.permlink, "permlink") 139 | self.assertEqual(comment.body, "body") 140 | self.assertEqual(comment.title, "title") 141 | self.assertEqual( 142 | comment.json_metadata, 143 | json.dumps({"app": "testapp/0.0.1", "tags": ["test"]})) 144 | 145 | def test_comment_options(self): 146 | comment_options = CommentOptions( 147 | "author", 148 | "permlink", 149 | allow_curation_rewards=False, 150 | ) 151 | 152 | self.assertEqual(comment_options.author, "author") 153 | self.assertEqual(comment_options.permlink, "permlink") 154 | self.assertEqual( 155 | comment_options.allow_curation_rewards, False) 156 | 157 | def test_comment_options_comment_injection(self): 158 | comment = Comment( 159 | "author", 160 | "permlink", 161 | "body", 162 | title="title", 163 | json_metadata={"app": "testapp/0.0.1", "tags": ["test"]}, 164 | ) 165 | 166 | comment_options = CommentOptions( 167 | parent_comment=comment, 168 | allow_curation_rewards=False, 169 | ) 170 | 171 | self.assertEqual(comment_options.author, "author") 172 | self.assertEqual(comment_options.permlink, "permlink") 173 | self.assertEqual( 174 | comment_options.allow_curation_rewards, False) 175 | 176 | def test_delete_comment(self): 177 | delete_comment = DeleteComment( 178 | "author", "permlink" 179 | ) 180 | 181 | self.assertEqual(delete_comment.author, "author") 182 | self.assertEqual(delete_comment.permlink, "permlink") 183 | 184 | def test_follow(self): 185 | follow = Follow("follower", "following") 186 | 187 | self.assertEqual(follow.follower, "follower") 188 | self.assertEqual(follow.following, "following") 189 | self.assertEqual(follow.what, ["blog",]) 190 | 191 | def test_unfollow(self): 192 | unfollow = Unfollow("follower", "following") 193 | 194 | self.assertEqual(unfollow.follower, "follower") 195 | self.assertEqual(unfollow.following, "following") 196 | self.assertEqual(unfollow.what, []) 197 | 198 | def test_mute(self): 199 | ignore = Mute("follower", "following") 200 | 201 | self.assertEqual(ignore.follower, "follower") 202 | self.assertEqual(ignore.following, "following") 203 | self.assertEqual(ignore.what, ["ignore"]) 204 | 205 | def test_reblog(self): 206 | resteem = Resteem("account", "author", "permlink") 207 | 208 | self.assertEqual(resteem.account, "account") 209 | self.assertEqual(resteem.author, "author") 210 | self.assertEqual(resteem.permlink, "permlink") 211 | 212 | def test_claim_reward_balance(self): 213 | claim_reward_balance = ClaimRewardBalance( 214 | 'account', '0.000 STEEM', '1.500 SBD', '1132.996000 VESTS' 215 | ) 216 | 217 | self.assertEqual(claim_reward_balance.account, "account") 218 | self.assertEqual(claim_reward_balance.reward_steem, "0.000 STEEM") 219 | self.assertEqual(claim_reward_balance.reward_sbd, "1.500 SBD") 220 | self.assertEqual( 221 | claim_reward_balance.reward_vests, "1132.996000 VESTS") 222 | 223 | def test_custom_json(self): 224 | 225 | custom_json = CustomJson( 226 | [], 227 | ["author"], 228 | "testcustomjson", 229 | {"foo": "bar"} 230 | ) 231 | 232 | self.assertEqual(custom_json.required_auth, []) 233 | self.assertEqual(custom_json.required_posting_auths, ["author"]) 234 | self.assertEqual(custom_json.custom_operation_id, "testcustomjson") 235 | self.assertEqual(custom_json.structure, {"foo": "bar"}) 236 | 237 | def test_hot_sign(self): 238 | c = Client() 239 | 240 | sign_url = c.hot_sign( 241 | "transfer", 242 | { 243 | "to": "turbot", 244 | "amount": "0.001 SBD", 245 | "memo": "selam", 246 | }, 247 | redirect_uri="http://localhost" 248 | ) 249 | 250 | self.assertEqual(sign_url, "https://v2.steemconnect.com/sign/tra" 251 | "nsfer?to=turbot&amount=0.001+SBD&me" 252 | "mo=selam&redirect_uri=http%3A%2F%2F" 253 | "localhost") 254 | 255 | def test_hot_sign_without_redirect_uri(self): 256 | c = Client() 257 | 258 | sign_url = c.hot_sign( 259 | "transfer", 260 | { 261 | "to": "turbot", 262 | "amount": "0.001 SBD", 263 | "memo": "selam", 264 | }, 265 | ) 266 | 267 | self.assertEqual(sign_url, "https://v2.steemconnect.com/sign/tra" 268 | "nsfer?to=turbot&amount=0.001+SBD&me" 269 | "mo=selam") 270 | 271 | 272 | 273 | if __name__ == '__main__': 274 | unittest.main() --------------------------------------------------------------------------------