tag attributes, or unset if none
+ \x [on|off|auto] toggle expanded output (currently off)
+
+Connection
+ \c[onnect] {[DBNAME|- USER|- HOST|- PORT|-] | conninfo}
+ connect to new database (currently "urlshortener_dev")
+ \conninfo display information about current connection
+ \encoding [ENCODING] show or set client encoding
+ \password [USERNAME] securely change the password for a user
+
+Operating System
+ \cd [DIR] change the current working directory
+ \setenv NAME [VALUE] set or unset environment variable
+ \timing [on|off] toggle timing of commands (currently off)
+ \! [COMMAND] execute command in shell or start interactive shell
+
+Variables
+ \prompt [TEXT] NAME prompt user to set internal variable
+ \set [NAME [VALUE]] set internal variable, or list all if no parameters
+ \unset NAME unset (delete) internal variable
+
+Large Objects
+ \lo_export LOBOID FILE
+ \lo_import FILE [COMMENT]
+ \lo_list
+ \lo_unlink LOBOID large object operations
diff --git a/gunicorn b/gunicorn
new file mode 100755
index 0000000..c51599d
--- /dev/null
+++ b/gunicorn
@@ -0,0 +1,3 @@
+#! /bin/bash
+
+gunicorn --env "TRUSTED_HOSTS=http://localhost:8080" --workers 2 --reload --bind :8081 wsgi:app
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100755
index 0000000..879e94c
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,7 @@
+nose
+coverage
+restfulpy>=0.41.3
+hashids
+gunicorn
+webtest
+bddrest
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..df8e0ba
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,44 @@
+import re
+from os.path import join, dirname
+from setuptools import setup, find_packages
+
+
+# reading package version (same way the sqlalchemy does)
+with open(join(dirname(__file__), 'urlshortener', '__init__.py')) as v_file:
+ package_version = re.compile('.*__version__ = \'(.*?)\'', re.S)\
+ .match(v_file.read()).group(1)
+
+
+dependencies = [
+ 'restfulpy >= 0.41.3',
+ 'hashids',
+ 'nanohttp',
+ 'oauth2client',
+ 'requests',
+ 'sqlalchemy',
+
+ # Deployment
+ 'gunicorn',
+
+ # testing
+ 'webtest',
+ 'nose',
+ 'bddrest'
+]
+
+
+setup(
+ name='urlshortener',
+ version=package_version,
+ author='Mohammad',
+ author_email='mohammadsheikhian70@gmail.com',
+ install_requires=dependencies,
+ packages=find_packages(),
+ test_suite='urlshortener.tests',
+ entry_points={
+ 'console_scripts': [
+ 'urlshortener = urlshortener:urlshortener.cli_main'
+ ]
+ }
+)
+
diff --git a/urlshortener.egg-info/PKG-INFO b/urlshortener.egg-info/PKG-INFO
new file mode 100644
index 0000000..1fa17e9
--- /dev/null
+++ b/urlshortener.egg-info/PKG-INFO
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: urlshortener
+Version: 0.1.0-planning.0
+Summary: UNKNOWN
+Home-page: UNKNOWN
+Author: Mohammad
+Author-email: mohammadsheikhian70@gmail.com
+License: UNKNOWN
+Description: UNKNOWN
+Platform: UNKNOWN
diff --git a/urlshortener.egg-info/SOURCES.txt b/urlshortener.egg-info/SOURCES.txt
new file mode 100644
index 0000000..894715a
--- /dev/null
+++ b/urlshortener.egg-info/SOURCES.txt
@@ -0,0 +1,24 @@
+README.md
+urlshortener/__init__.py
+urlshortener.egg-info/PKG-INFO
+urlshortener.egg-info/SOURCES.txt
+urlshortener.egg-info/dependency_links.txt
+urlshortener.egg-info/entry_points.txt
+urlshortener.egg-info/requires.txt
+urlshortener.egg-info/top_level.txt
+urlshortener/basedata/__init__.py
+urlshortener/controllers/__init__.py
+urlshortener/controllers/auth.py
+urlshortener/controllers/helpers.py
+urlshortener/controllers/root.py
+urlshortener/controllers/urls.py
+urlshortener/makomodules/__init__.py
+urlshortener/makomodules/index.mak.py
+urlshortener/makomodules/successfully.mak.py
+urlshortener/models/__init__.py
+urlshortener/models/urls.py
+urlshortener/tests/__init__.py
+urlshortener/tests/helpers.py
+urlshortener/tests/test_auth.py
+urlshortener/tests/test_root.py
+urlshortener/tests/test_urls.py
\ No newline at end of file
diff --git a/urlshortener.egg-info/dependency_links.txt b/urlshortener.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/urlshortener.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/urlshortener.egg-info/entry_points.txt b/urlshortener.egg-info/entry_points.txt
new file mode 100644
index 0000000..6681f2c
--- /dev/null
+++ b/urlshortener.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+urlshortener = urlshortener:urlshortener.cli_main
+
diff --git a/urlshortener.egg-info/requires.txt b/urlshortener.egg-info/requires.txt
new file mode 100644
index 0000000..235a2d1
--- /dev/null
+++ b/urlshortener.egg-info/requires.txt
@@ -0,0 +1,8 @@
+restfulpy>=0.41.3
+hashids
+nanohttp
+oauth2client
+gunicorn
+webtest
+nose
+bddrest
diff --git a/urlshortener.egg-info/top_level.txt b/urlshortener.egg-info/top_level.txt
new file mode 100644
index 0000000..6b72629
--- /dev/null
+++ b/urlshortener.egg-info/top_level.txt
@@ -0,0 +1 @@
+urlshortener
diff --git a/urlshortener/__init__.py b/urlshortener/__init__.py
new file mode 100755
index 0000000..cd6070b
--- /dev/null
+++ b/urlshortener/__init__.py
@@ -0,0 +1,50 @@
+from os.path import dirname, join
+
+from restfulpy.authentication import Authenticator
+
+from restfulpy import Application as BaseApplication
+from restfulpy.orm import DBSession
+from urlshortener.models.member import Member
+
+from urlshortener.models.urls import Url
+from .controllers.root import Root
+__version__ = '0.1.0-planning.0'
+
+
+class Application(BaseApplication):
+ builtin_configuration = """
+ messaging:
+ default_sender: NueMDv.
+ template_dirs:
+ - %(root_path)s/urlshortener/templates
+
+ """
+
+ def __init__(self, application_name='urlshortener', root=Root()):
+ super().__init__(
+ application_name,
+ root=root,
+ root_path=join(dirname(__file__), '..'),
+ version=__version__
+ )
+
+ # noinspection PyArgumentList
+ def insert_basedata(self): # pragma: no cover
+ raise NotImplementedError()
+
+ # noinspection PyArgumentList
+ def insert_mockup(self):
+ url = Url(url='http://www.varzesh3.com')
+ member = Member(
+ given_name='mohammad',
+ family_name='sheikhian',
+ email='mohammadsheikhian70@gmail.com',
+ google_access_token='fgbsdibfosdnfosd',
+ google_refresh_token='fgbdyugbdsiubgdufig'
+ )
+ DBSession.add(url)
+ DBSession.add(member)
+ DBSession.commit()
+
+
+urlshortener = Application()
diff --git a/urlshortener/__pycache__/__init__.cpython-36.pyc b/urlshortener/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..bf5fd74
Binary files /dev/null and b/urlshortener/__pycache__/__init__.cpython-36.pyc differ
diff --git a/urlshortener/basedata/__init__.py b/urlshortener/basedata/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/urlshortener/basedata/client_secrets.json b/urlshortener/basedata/client_secrets.json
new file mode 100644
index 0000000..2d5fea9
--- /dev/null
+++ b/urlshortener/basedata/client_secrets.json
@@ -0,0 +1 @@
+{"web":{"client_id":"781215968670-qg3g1nadrm9k2rp8nrhcai73sa5o3fiq.apps.googleusercontent.com","project_id":"urlshortener-205611","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"oj5qITnaVreCDFrXYATj9BtO","redirect_uris":["http://localhost:8080/auth"]}}
\ No newline at end of file
diff --git a/urlshortener/controllers/__init__.py b/urlshortener/controllers/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/urlshortener/controllers/__pycache__/__init__.cpython-36.pyc b/urlshortener/controllers/__pycache__/__init__.cpython-36.pyc
new file mode 100755
index 0000000..1b8d3e4
Binary files /dev/null and b/urlshortener/controllers/__pycache__/__init__.cpython-36.pyc differ
diff --git a/urlshortener/controllers/__pycache__/auth.cpython-36.pyc b/urlshortener/controllers/__pycache__/auth.cpython-36.pyc
new file mode 100644
index 0000000..c35d2c3
Binary files /dev/null and b/urlshortener/controllers/__pycache__/auth.cpython-36.pyc differ
diff --git a/urlshortener/controllers/__pycache__/helpers.cpython-36.pyc b/urlshortener/controllers/__pycache__/helpers.cpython-36.pyc
new file mode 100644
index 0000000..89e634c
Binary files /dev/null and b/urlshortener/controllers/__pycache__/helpers.cpython-36.pyc differ
diff --git a/urlshortener/controllers/__pycache__/root.cpython-36.pyc b/urlshortener/controllers/__pycache__/root.cpython-36.pyc
new file mode 100644
index 0000000..d587c8f
Binary files /dev/null and b/urlshortener/controllers/__pycache__/root.cpython-36.pyc differ
diff --git a/urlshortener/controllers/__pycache__/urls.cpython-36.pyc b/urlshortener/controllers/__pycache__/urls.cpython-36.pyc
new file mode 100644
index 0000000..b671328
Binary files /dev/null and b/urlshortener/controllers/__pycache__/urls.cpython-36.pyc differ
diff --git a/urlshortener/controllers/auth.py b/urlshortener/controllers/auth.py
new file mode 100644
index 0000000..a5063ce
--- /dev/null
+++ b/urlshortener/controllers/auth.py
@@ -0,0 +1,88 @@
+import requests
+import ast
+import json as json_library
+from os.path import join, dirname, abspath
+
+from restfulpy.principal import JwtPrincipal
+
+from restfulpy.orm import DBSession
+from nanohttp import RestController, text, HttpFound, context, json, settings,\
+ HttpForbidden, HttpNotFound, HttpBadRequest
+import google_auth_oauthlib.flow
+
+from urlshortener.models.member import Member
+
+client_secret_file = join(abspath(join(dirname(__file__), '..')),
+ 'basedata/client_secrets.json')
+json_data = open(client_secret_file).read()
+client_secret_data = json_library.loads(json_data)['web']
+
+
+class Auth(RestController):
+
+ @json
+ def get(self):
+
+ if(context.query_string.get('code') is None) or \
+ (client_secret_data['client_id'] is None) or \
+ (client_secret_data['client_secret'] is None):
+ raise HttpBadRequest
+
+ body_token = dict()
+ body_token['grant_type'] = 'authorization_code'
+ body_token['code'] = context.query_string.get('code')
+ body_token['client_id'] = client_secret_data['client_id']
+ body_token['redirect_uri'] = settings.redirect_uri_auth
+ body_token['client_secret'] = client_secret_data['client_secret']
+
+ response_token = requests.post(
+ settings.auth_google_uri_token,
+ body_token
+ )
+
+ if response_token.status_code != 200:
+ raise HttpForbidden()
+
+ response_get_profile = requests.get(
+ settings.oauth_url_google_api,
+ headers={
+ 'Authorization':
+ 'OAuth ' +
+ json_library.loads(response_token.text)['access_token']
+ }
+ )
+
+ if response_get_profile.status_code != 200:
+ raise HttpNotFound()
+
+ profile = json_library.loads(response_get_profile.text)
+
+ if 'refresh_token' in response_token.text:
+ member = Member(
+ given_name=profile['given_name'],
+ family_name=profile['family_name'],
+ email=profile['email'],
+ google_access_token=
+ json_library.loads(response_token.text)['access_token'],
+ google_refresh_token=
+ json_library.loads(response_token.text)['refresh_token']
+ )
+ DBSession.add(member)
+ DBSession.commit()
+
+ return dict(authorization=JwtPrincipal(profile).dump().decode("utf-8"))
+
+ @text
+ def post(self):
+
+ flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
+ client_secret_file,
+ scopes=[settings.oauth_google_scope],
+ redirect_uri=settings.redirect_uri_auth
+ )
+
+ authorization_url, state = flow.authorization_url(
+ access_type='offline',
+ include_granted_scopes='true')
+
+ raise HttpFound(authorization_url)
diff --git a/urlshortener/controllers/helpers.py b/urlshortener/controllers/helpers.py
new file mode 100755
index 0000000..6582b68
--- /dev/null
+++ b/urlshortener/controllers/helpers.py
@@ -0,0 +1,32 @@
+from os.path import join, abspath, dirname
+
+import functools
+from mako.lookup import TemplateLookup
+from nanohttp import action
+
+here = abspath(join(dirname(__file__), '..'))
+lookup = TemplateLookup(directories=[join(here, 'templates')],
+ module_directory=join(here, 'makomodules'))
+
+
+def render_template(func, template_name):
+
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+
+ result = func(*args, **kwargs)
+ if hasattr(result, 'to_dict'):
+ result = result.to_dict()
+ elif not isinstance(result, dict):
+ raise ValueError('The result must be an instance of dict, not: %s' %
+ type(result))
+
+ template_ = lookup.get_template(template_name)
+ return template_.render(**result)
+
+ return wrapper
+
+
+template = functools.partial(action, content_type='text/html',
+ inner_decorator=render_template)
+
diff --git a/urlshortener/controllers/root.py b/urlshortener/controllers/root.py
new file mode 100755
index 0000000..5d26694
--- /dev/null
+++ b/urlshortener/controllers/root.py
@@ -0,0 +1,77 @@
+from hashids import Hashids
+
+from nanohttp import json, RestController, context
+from restfulpy.controllers import JsonPatchControllerMixin
+from restfulpy.orm import DBSession
+
+import urlshortener
+from urlshortener.controllers.auth import Auth
+from urlshortener.controllers.urls import Urls
+from urlshortener.models.member import Member
+from urlshortener.models.urls import Url
+from .helpers import template
+
+
+hashids = Hashids(salt="url shortener")
+
+
+class ApiV1(JsonPatchControllerMixin, RestController):
+
+ @json
+ def version(self):
+ return {
+ 'version': urlshortener.__version__
+ }
+
+
+class Root(RestController):
+ auth = Auth()
+ urls = Urls()
+
+ @template('index.mak')
+ def get(self):
+
+ if context.identity is None:
+ return dict(
+ name='',
+ family=''
+ )
+
+ member = DBSession.query(Member).\
+ filter_by(email=context.identity.email).one_or_none()
+ if member is None:
+ return dict(
+ name='',
+ family=''
+ )
+
+ return dict(
+ name=context.identity.payload['name'],
+ family=context.identity.payload['family']
+ )
+
+ @template('successfully.mak')
+ def post(self):
+ url = context.form.get('url')
+ if not url.startswith('http'):
+ url = f'http://{url}'
+
+ url_exist = DBSession.query(Url).filter_by(url=url).one_or_none()
+
+ if url_exist is None:
+ url_exist = Url(url=url)
+ DBSession.add(url_exist)
+ DBSession.commit()
+
+ hash_id = hashids.encode(url_exist.id)
+
+ return dict(hash_id=hash_id)
+
+
+if __name__ == '__main__':
+ from nanohttp import quickstart, configure
+ configure()
+ try:
+ quickstart(Root())
+ except KeyboardInterrupt:
+ print('CTLR+C just pressed')
diff --git a/urlshortener/controllers/urls.py b/urlshortener/controllers/urls.py
new file mode 100644
index 0000000..3fd05ee
--- /dev/null
+++ b/urlshortener/controllers/urls.py
@@ -0,0 +1,24 @@
+from nanohttp import RestController, HttpBadRequest, HttpNotFound, HttpFound, \
+ text
+from restfulpy.orm import DBSession
+from hashids import Hashids
+
+from urlshortener.models.urls import Url
+
+hashids = Hashids(salt="url shortener")
+
+
+class Urls(RestController):
+
+ @text
+ def get(self, hash_id):
+ try:
+ db_id, = hashids.decode(hash_id)
+ except ValueError:
+ raise HttpBadRequest()
+
+ url = DBSession.query(Url).filter_by(id=db_id).one_or_none()
+ if url is None:
+ raise HttpNotFound()
+
+ raise HttpFound(url.url)
diff --git a/urlshortener/document/auth_get.md b/urlshortener/document/auth_get.md
new file mode 100644
index 0000000..daaf269
--- /dev/null
+++ b/urlshortener/document/auth_get.md
@@ -0,0 +1,25 @@
+## GET
+
+### GET /auth
+
+### Query Strings
+
+Name | Example
+--- | ---
+state | DAMDzeJImyByVNSdUOVMzy5moo77JZ
+code | 4/AAA_ieAH5x0_nAl45U03Iom6Ut2bcMV-oN8pYrnZtGUYKDpovHVmjpCbWu4zBdUYrLtb6JMguFf_E2tyxqLo_vo
+scope | https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/plus.me https://www.googleapis.com/auth/userinfo.email
+
+### Response: 200 OK
+
+#### Headers
+
+* Content-Type: application/json; charset=utf-8
+* Content-Length: 597
+
+#### Body
+
+```json
+"eyJhbGciOiJIUzI1NiIsImlhdCI6MTUyOTM5MzAyNiwiZXhwIjoxNTI5NDc5NDI2fQ.eyJpZCI6IjEwNTIwMjU2NjM2NjAxMTk1NzM5MiIsImVtYWlsIjoibW9oYW1tYWRzaGVpa2hpYW43MEBnbWFpbC5jb20iLCJ2ZXJpZmllZF9lbWFpbCI6dHJ1ZSwibmFtZSI6Ik1vaGFtbWFkIFNoZWlraGlhbiIsImdpdmVuX25hbWUiOiJNb2hhbW1hZCIsImZhbWlseV9uYW1lIjoiU2hlaWtoaWFuIiwibGluayI6Imh0dHBzOi8vcGx1cy5nb29nbGUuY29tLzEwNTIwMjU2NjM2NjAxMTk1NzM5MiIsInBpY3R1cmUiOiJodHRwczovL2xoNC5nb29nbGV1c2VyY29udGVudC5jb20vLTlnblZRTlpxdWYlMjBnL0FBQUFBQUFBQUFJL0FBQUFBQUFBQUQwL2c0QUFNa3pZa3BjL3Bob3RvLmpwZyIsImdlbmRlciI6Im1hbGUiLCJsb2NhbGUiOiJmYSJ9.XNZR_-0igGluXjr5e-iBfJyF_1GWNFXJA5pSNPmkubg"
+```
+
diff --git a/urlshortener/document/auth_post.md b/urlshortener/document/auth_post.md
new file mode 100644
index 0000000..ef1f7df
--- /dev/null
+++ b/urlshortener/document/auth_post.md
@@ -0,0 +1,21 @@
+## POST
+
+### POST /auth
+
+Create redirect url
+
+### Response: 302 Found
+
+#### Headers
+
+* Location: https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=781215968670-qg3g1nadrm9k2rp8nrhcai73sa5o3fiq.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email&state=RNl2BPwc7CpnMqVwZq4fXQJJ6yRC5T&access_type=offline&include_granted_scopes=true
+* Content-Type: text/plain; charset=utf-8
+* Content-Length: 30
+
+#### Body
+
+```
+Found
+Object moved temporarily
+```
+
diff --git a/urlshortener/document/root_get_with_authorization.md b/urlshortener/document/root_get_with_authorization.md
new file mode 100644
index 0000000..b2d02d1
--- /dev/null
+++ b/urlshortener/document/root_get_with_authorization.md
@@ -0,0 +1,38 @@
+## GET
+
+### GET /
+
+### Request Headers
+
+* AUTHORIZATION: eyJhbGciOiJIUzI1NiIsImlhdCI6MTUyOTM5MTA0MiwiZXhwIjoxNTI5NDc3NDQyfQ.eyJlbWFpbCI6Im1vaGFtbWFkc2hlaWtoaWFuNzBAZ21haWwuY29tIiwibmFtZSI6Im1vaGFtbWFkIiwiZmFtaWx5Ijoic2hlaWtoaWFuIiwiaWQiOjEsInNlc3Npb25JZCI6MSwicm9sZXMiOlsiYWRtaW4iXX0.YnodWvTOTOjCMxZPCsAXJCzRtyIaS8rN98nb4VjM9bc
+
+### Response: 200 OK
+
+#### Headers
+
+* X-Identity: 1
+* Content-Type: text/html; charset=utf-8
+* Content-Length: 506
+
+#### Body
+
+```
+
+Url shortener
+
+
+
Profile: mohammad sheikhian
+
+
+
+
+
+
+```
+
diff --git a/urlshortener/document/root_get_without_authorization.md b/urlshortener/document/root_get_without_authorization.md
new file mode 100644
index 0000000..325b178
--- /dev/null
+++ b/urlshortener/document/root_get_without_authorization.md
@@ -0,0 +1,33 @@
+## GET
+
+### GET /
+
+### Response: 200 OK
+
+#### Headers
+
+* Content-Type: text/html; charset=utf-8
+* Content-Length: 489
+
+#### Body
+
+```
+
+Url shortener
+
+
+
Profile:
+
+
+
+
+
+
+```
+
diff --git a/urlshortener/document/root_post.md b/urlshortener/document/root_post.md
new file mode 100644
index 0000000..64bb3ad
--- /dev/null
+++ b/urlshortener/document/root_post.md
@@ -0,0 +1,36 @@
+## POST
+
+### POST /
+
+### Form
+
+Name | Example
+--- | ---
+url | www.varzesh3.com
+
+### Response: 200 OK
+
+#### Headers
+
+* Content-Type: text/html; charset=utf-8
+* Content-Length: 414
+
+#### Body
+
+```
+
+Url shortener
+
+
+
+
+
+
+```
+
diff --git a/urlshortener/document/urls_get.md b/urlshortener/document/urls_get.md
new file mode 100644
index 0000000..176a3b0
--- /dev/null
+++ b/urlshortener/document/urls_get.md
@@ -0,0 +1,21 @@
+## GET
+
+### GET /urls/zK
+
+Found url using by hash id
+
+### Response: 302 Found
+
+#### Headers
+
+* Location: http://www.varzesh3.com
+* Content-Type: text/plain; charset=utf-8
+* Content-Length: 30
+
+#### Body
+
+```
+Found
+Object moved temporarily
+```
+
diff --git a/urlshortener/makomodules/__init__.py b/urlshortener/makomodules/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/urlshortener/makomodules/__pycache__/__init__.cpython-36.pyc b/urlshortener/makomodules/__pycache__/__init__.cpython-36.pyc
new file mode 100755
index 0000000..c56ddc0
Binary files /dev/null and b/urlshortener/makomodules/__pycache__/__init__.cpython-36.pyc differ
diff --git a/urlshortener/makomodules/__pycache__/index.mak.cpython-36.pyc b/urlshortener/makomodules/__pycache__/index.mak.cpython-36.pyc
new file mode 100644
index 0000000..59cdc1d
Binary files /dev/null and b/urlshortener/makomodules/__pycache__/index.mak.cpython-36.pyc differ
diff --git a/urlshortener/makomodules/__pycache__/successfully.mak.cpython-36.pyc b/urlshortener/makomodules/__pycache__/successfully.mak.cpython-36.pyc
new file mode 100644
index 0000000..1b87031
Binary files /dev/null and b/urlshortener/makomodules/__pycache__/successfully.mak.cpython-36.pyc differ
diff --git a/urlshortener/makomodules/index.mak.py b/urlshortener/makomodules/index.mak.py
new file mode 100644
index 0000000..b0635b7
--- /dev/null
+++ b/urlshortener/makomodules/index.mak.py
@@ -0,0 +1,37 @@
+# -*- coding:ascii -*-
+from mako import runtime, filters, cache
+UNDEFINED = runtime.UNDEFINED
+STOP_RENDERING = runtime.STOP_RENDERING
+__M_dict_builtin = dict
+__M_locals_builtin = locals
+_magic_number = 10
+_modified_time = 1529302078.870025
+_enable_loop = True
+_template_filename = '/home/mohammad/workspace/urlshortener/urlshortener/templates/index.mak'
+_template_uri = 'index.mak'
+_source_encoding = 'ascii'
+_exports = []
+
+
+def render_body(context,**pageargs):
+ __M_caller = context.caller_stack._push_frame()
+ try:
+ __M_locals = __M_dict_builtin(pageargs=pageargs)
+ name = context.get('name', UNDEFINED)
+ family = context.get('family', UNDEFINED)
+ __M_writer = context.writer()
+ __M_writer('\nUrl shortener\n\n \n
Profile: ')
+ __M_writer(str(name))
+ __M_writer(' ')
+ __M_writer(str(family))
+ __M_writer('
\n \n \n \n\n\n')
+ return ''
+ finally:
+ context.caller_stack._pop_frame()
+
+
+"""
+__M_BEGIN_METADATA
+{"filename": "/home/mohammad/workspace/urlshortener/urlshortener/templates/index.mak", "uri": "index.mak", "source_encoding": "ascii", "line_map": {"16": 0, "23": 1, "24": 5, "25": 5, "26": 5, "27": 5, "33": 27}}
+__M_END_METADATA
+"""
diff --git a/urlshortener/makomodules/successfully.mak.py b/urlshortener/makomodules/successfully.mak.py
new file mode 100644
index 0000000..2843e33
--- /dev/null
+++ b/urlshortener/makomodules/successfully.mak.py
@@ -0,0 +1,36 @@
+# -*- coding:ascii -*-
+from mako import runtime, filters, cache
+UNDEFINED = runtime.UNDEFINED
+STOP_RENDERING = runtime.STOP_RENDERING
+__M_dict_builtin = dict
+__M_locals_builtin = locals
+_magic_number = 10
+_modified_time = 1527584278.3637245
+_enable_loop = True
+_template_filename = '/home/mohammad/workspace/urlshortener/urlshortener/templates/successfully.mak'
+_template_uri = 'successfully.mak'
+_source_encoding = 'ascii'
+_exports = []
+
+
+def render_body(context,**pageargs):
+ __M_caller = context.caller_stack._push_frame()
+ try:
+ __M_locals = __M_dict_builtin(pageargs=pageargs)
+ hash_id = context.get('hash_id', UNDEFINED)
+ __M_writer = context.writer()
+ __M_writer('\nUrl shortener\n\n \n \n\n\n')
+ return ''
+ finally:
+ context.caller_stack._pop_frame()
+
+
+"""
+__M_BEGIN_METADATA
+{"filename": "/home/mohammad/workspace/urlshortener/urlshortener/templates/successfully.mak", "uri": "successfully.mak", "source_encoding": "ascii", "line_map": {"16": 0, "22": 1, "23": 7, "24": 7, "25": 8, "26": 8, "32": 26}}
+__M_END_METADATA
+"""
diff --git a/urlshortener/models/__init__.py b/urlshortener/models/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/urlshortener/models/__pycache__/__init__.cpython-36.pyc b/urlshortener/models/__pycache__/__init__.cpython-36.pyc
new file mode 100644
index 0000000..52d5160
Binary files /dev/null and b/urlshortener/models/__pycache__/__init__.cpython-36.pyc differ
diff --git a/urlshortener/models/__pycache__/member.cpython-36.pyc b/urlshortener/models/__pycache__/member.cpython-36.pyc
new file mode 100644
index 0000000..f3cbb93
Binary files /dev/null and b/urlshortener/models/__pycache__/member.cpython-36.pyc differ
diff --git a/urlshortener/models/__pycache__/urls.cpython-36.pyc b/urlshortener/models/__pycache__/urls.cpython-36.pyc
new file mode 100644
index 0000000..5744984
Binary files /dev/null and b/urlshortener/models/__pycache__/urls.cpython-36.pyc differ
diff --git a/urlshortener/models/member.py b/urlshortener/models/member.py
new file mode 100644
index 0000000..056a38a
--- /dev/null
+++ b/urlshortener/models/member.py
@@ -0,0 +1,13 @@
+from sqlalchemy import Integer, String
+from restfulpy.orm import DeclarativeBase, Field
+
+
+class Member(DeclarativeBase):
+ __tablename__ = 'members'
+
+ id = Field(Integer, primary_key=True, autoincrement=True)
+ given_name = Field(String(50))
+ family_name = Field(String(50))
+ email = Field(String(50))
+ google_access_token = Field(String(500))
+ google_refresh_token = Field(String(500))
diff --git a/urlshortener/models/urls.py b/urlshortener/models/urls.py
new file mode 100755
index 0000000..2213bed
--- /dev/null
+++ b/urlshortener/models/urls.py
@@ -0,0 +1,14 @@
+from sqlalchemy import Integer, String
+from restfulpy.orm import DeclarativeBase, Field
+
+
+class Url(DeclarativeBase):
+ __tablename__ = 'urls'
+
+ id = Field(Integer, primary_key=True, autoincrement=True)
+ url = Field(String(50))
+
+ # def __repr__(self):
+ # return "" % (
+ # self.url
+ # )
diff --git a/urlshortener/templates/index.mak b/urlshortener/templates/index.mak
new file mode 100755
index 0000000..184bfa9
--- /dev/null
+++ b/urlshortener/templates/index.mak
@@ -0,0 +1,16 @@
+
+Url shortener
+
+
+
Profile: ${name} ${family}
+
+
+
+
+
diff --git a/urlshortener/templates/successfully.mak b/urlshortener/templates/successfully.mak
new file mode 100755
index 0000000..d66605b
--- /dev/null
+++ b/urlshortener/templates/successfully.mak
@@ -0,0 +1,13 @@
+
+Url shortener
+
+
+
+
+
diff --git a/urlshortener/tests/__init__.py b/urlshortener/tests/__init__.py
new file mode 100755
index 0000000..e69de29
diff --git a/urlshortener/tests/__pycache__/__init__.cpython-36.pyc b/urlshortener/tests/__pycache__/__init__.cpython-36.pyc
new file mode 100755
index 0000000..1d2676a
Binary files /dev/null and b/urlshortener/tests/__pycache__/__init__.cpython-36.pyc differ
diff --git a/urlshortener/tests/__pycache__/helpers.cpython-36.pyc b/urlshortener/tests/__pycache__/helpers.cpython-36.pyc
new file mode 100644
index 0000000..89f60b6
Binary files /dev/null and b/urlshortener/tests/__pycache__/helpers.cpython-36.pyc differ
diff --git a/urlshortener/tests/helpers.py b/urlshortener/tests/helpers.py
new file mode 100755
index 0000000..430e907
--- /dev/null
+++ b/urlshortener/tests/helpers.py
@@ -0,0 +1,84 @@
+from os.path import join, dirname, abspath
+
+from restfulpy.authentication import Authenticator
+
+from nanohttp import settings
+from restfulpy.testing import WebAppTestCase
+from bddrest.authoring import given
+
+from urlshortener import Application
+
+client_secret_file = join(abspath(join(dirname(__file__), '..')),
+ 'basedata/client_secrets.json')
+
+document_directory = join(abspath(join(dirname(__file__), '..')), 'document')
+
+
+class BDDTestClass(WebAppTestCase):
+ application = Application()
+ login_token = None
+
+ @classmethod
+ def configure_app(cls):
+ super().configure_app()
+ settings.merge('''
+ logging:
+ loggers:
+ default:
+ level: debug
+ ''')
+
+ @classmethod
+ def mockup(cls):
+ cls.application.insert_mockup()
+
+ def given(self, *args, **kwargs):
+ if self.login_token:
+ headers = kwargs.setdefault('headers', [])
+ headers.append(('AUTHORIZATION', self.login_token))
+ return given(self.application, *args, **kwargs)
+
+
+class MockupApplication(Application):
+ def insert_basedata(self):
+ pass
+
+ builtin_configuration = '''
+ db:
+ test_url: postgresql://postgres:postgres@localhost/restfulpy_test
+ administrative_url: postgresql://postgres:postgres@localhost/postgres
+ logging:
+ loggers:
+ default:
+ level: critical
+ '''
+
+ def __init__(self, application_name, root):
+ super().__init__(
+ application_name,
+ root=root
+ )
+ self.__authenticator__ = Authorization()
+
+ def configure(self, files=None, context=None, **kwargs):
+ _context = dict(
+ process_name='restfulpy_unittests'
+ )
+ if context:
+ _context.update(context)
+ super().configure(files=files, context=_context, **kwargs)
+
+
+class Authorization(Authenticator):
+
+ def validate_credentials(self, credentials):
+ pass
+
+ def create_refresh_principal(self, member_id=None):
+ pass
+
+ def create_principal(self, member_id=None, session_id=None, **kwargs):
+ pass
+
+ def authenticate_request(self):
+ pass
diff --git a/urlshortener/tests/test_auth.py b/urlshortener/tests/test_auth.py
new file mode 100644
index 0000000..2fc582a
--- /dev/null
+++ b/urlshortener/tests/test_auth.py
@@ -0,0 +1,160 @@
+import unittest
+from contextlib import contextmanager
+
+from bddrest.authoring import then, response
+from restfulpy.testing.mockup import http_server
+from urlshortener.tests.helpers import MockupApplication, document_directory
+from nanohttp import RestController, settings, json, context, HttpBadRequest, \
+ HttpUnauthorized
+
+from urlshortener.tests.helpers import BDDTestClass
+
+
+class Token(RestController):
+ @json
+ def post(self):
+ if '4/AAA' in context.form.get('code'):
+ return dict(
+ access_token='ya29.GlzVBYKNxVGGMl6euQ6U-_QIhylTdqoYXxW3MHOXL7\
+ r6WmO2xx_wkBht6TT6OIP0eoDjcQIm3Y6JXmAExohf7GU3xuhs6cF9EcL5DbT\
+ owmmH-nBlVE6Uop2IftiFtQ',
+ refresh_token='1/Yn_cvrK7qeJ9yNQcl77dHNqrqO1Q_ySU5MhibfXkvOo3\
+ vZct44-X5rUdvCRM9jJE'
+ )
+ elif '5/AAA' in context.form.get('code'):
+ return dict(
+ access_token='ya30.GlzVBYKNxVGGMl6euQ6U-_QIhylTdqoYXxW3MHOXL7\
+ r6WmO2xx_wkBht6TT6OIP0eoDjcQIm3Y6JXmAExohf7GU3xuhs6cF9EcL5DbT\
+ owmmH-nBlVE6Uop2IftiFtQ'
+ )
+ else:
+ raise HttpBadRequest()
+
+
+class Profile(RestController):
+
+ @json
+ def get(self):
+ access_token = context.environ.get('HTTP_AUTHORIZATION')
+
+ if 'ya29.GlzVBYKNxVGGMl6euQ6U' in access_token:
+ return {
+ 'id': '105202566366011957392',
+ 'email': 'mohammadsheikhian70@gmail.com',
+ 'verified_email': True,
+ 'name': 'Mohammad Sheikhian',
+ 'given_name': 'Mohammad',
+ 'family_name': 'Sheikhian',
+ 'link': 'https://plus.google.com/105202566366011957392',
+ 'picture': 'https://lh4.googleusercontent.com/-9gnVQNZquf%20g'
+ '/AAAAAAAAAAI/AAAAAAAAAD0/g4AAMkzYkpc/photo.jpg',
+ 'gender': 'male',
+ 'locale': 'fa'
+ }
+ else:
+ raise HttpUnauthorized()
+
+
+class Root(RestController):
+ token = Token()
+ profile = Profile()
+
+
+@contextmanager
+def oauth_mockup_server(root_controller):
+ app = MockupApplication('root', root_controller)
+ with http_server(app) as (server, url):
+ settings.merge(f'''
+ tokenizer:
+ url: {url}
+ ''')
+ yield app
+
+
+class AuthTestCase(BDDTestClass):
+
+ def test_auth_post(self):
+
+ call = dict(
+ title='POST',
+ description='Create redirect url',
+ url='/auth',
+ verb='POST',
+ autodoc=f'{document_directory}/auth_post.md'
+ )
+ with self.given(**call):
+ then(response.status_code == 302)
+
+ def test_auth_get(self):
+ with oauth_mockup_server(Root()):
+ settings.mockup_server_url = settings.tokenizer['url']
+ settings.auth_google_uri_token = f'{settings.mockup_server_url}' \
+ f'/token'
+ settings.oauth_url_google_api = f'{settings.mockup_server_url}' \
+ f'/profile'
+
+ call = dict(
+ title='GET',
+ description='Key value error code or state or scope',
+ url='/auth',
+ verb='GET',
+ query={}
+ )
+ with self.given(**call):
+ then(response.status_code == 400)
+
+ call = dict(
+ title='GET',
+ description='invalid code or state or scope',
+ url='/auth',
+ verb='GET',
+ query={
+ 'state': 'DAMDzeJImyByVNSdUOVMzy5moo77JZ',
+ 'code': '_ieAH5x0_nAl45U03Iom6Ut2bcMV-oN8pYrnZtGUYKD'
+ 'povHVmjpCbWu4zBdUYrLtb6JMguFf_E2tyxqLo_vo',
+ 'scope': 'https://www.googleapis.com/auth/userinfo.profile'
+ ' https://www.googleapis.com/auth/plus.me '
+ 'https://www.googleapis.com/auth/userinfo.email'
+ }
+ )
+ with self.given(**call):
+ then(response.status_code == 403)
+
+ call = dict(
+ title='GET',
+ description='',
+ url='/auth',
+ verb='GET',
+ query={
+ 'state': 'DAMDzeJImyByVNSdUOVMzy5moo77JZ',
+ 'code': '5/AAA_ieAH5x0_nAl45U03Iom6Ut2bcMV-oN8pYrnZtGU'
+ 'YKDpovHVmjpCbWu4zBdUYrLtb6JMguFf_E2tyxqLo_vo',
+ 'scope': 'https://www.googleapis.com/auth/userinfo.profile'
+ ' https://www.googleapis.com/auth/plus.me '
+ 'https://www.googleapis.com/auth/userinfo.email'
+ }
+ )
+ with self.given(**call):
+ then(response.status_code == 404)
+
+ call = dict(
+ title='GET',
+ description='',
+ url='/auth',
+ verb='GET',
+ query={
+ 'state': 'DAMDzeJImyByVNSdUOVMzy5moo77JZ',
+ 'code': '4/AAA_ieAH5x0_nAl45U03Iom6Ut2bcMV-oN8pYrnZtGU'
+ 'YKDpovHVmjpCbWu4zBdUYrLtb6JMguFf_E2tyxqLo_vo',
+ 'scope': 'https://www.googleapis.com/auth/userinfo.profile'
+ ' https://www.googleapis.com/auth/plus.me '
+ 'https://www.googleapis.com/auth/userinfo.email'
+ },
+ autodoc=f'{document_directory}/auth_get.md'
+ )
+ with self.given(**call):
+ then(response.status_code == 200)
+
+
+if __name__ == '__main__': # pragma: no cover
+ unittest.main()
diff --git a/urlshortener/tests/test_root.py b/urlshortener/tests/test_root.py
new file mode 100644
index 0000000..4f23bdb
--- /dev/null
+++ b/urlshortener/tests/test_root.py
@@ -0,0 +1,59 @@
+import unittest
+
+from bddrest.authoring import then, response
+from restfulpy.principal import JwtPrincipal
+
+from urlshortener.tests.helpers import BDDTestClass, document_directory
+
+
+class RootTestCase(BDDTestClass):
+
+ def test_root(self):
+
+ principal = JwtPrincipal(dict(
+ email='mohammadsheikhian70@gmail.com',
+ name='mohammad',
+ family='sheikhian',
+ id=1,
+ sessionId=1,
+ roles=['admin']
+ ))
+ self.login_token = principal.dump().decode("utf-8")
+
+ call = dict(
+ title='GET',
+ description='',
+ url='/',
+ verb='GET',
+ autodoc=f'{document_directory}/root_get_with_authorization.md'
+ )
+ with self.given(**call):
+ then(response.status_code == 200)
+
+ self.login_token = None
+ call = dict(
+ title='GET',
+ description='',
+ url='/',
+ verb='GET',
+ autodoc=f'{document_directory}/root_get_without_authorization.md'
+ )
+ with self.given(**call):
+ then(response.status_code == 200)
+
+ call = dict(
+ title='POST',
+ description='',
+ url='/',
+ verb='POST',
+ form={
+ 'url': 'www.varzesh3.com'
+ },
+ autodoc=f'{document_directory}/root_post.md'
+ )
+ with self.given(**call):
+ then(response.status_code == 200)
+
+
+if __name__ == '__main__': # pragma: no cover
+ unittest.main()
diff --git a/urlshortener/tests/test_urls.py b/urlshortener/tests/test_urls.py
new file mode 100644
index 0000000..2c7301c
--- /dev/null
+++ b/urlshortener/tests/test_urls.py
@@ -0,0 +1,42 @@
+import unittest
+
+from bddrest.authoring import then, response
+
+from urlshortener.tests.helpers import BDDTestClass, document_directory
+
+
+class UrlsTestCase(BDDTestClass):
+
+ def test_urls(self):
+
+ call = dict(
+ title='GET',
+ description='Found url using by hash id',
+ url='/urls/zK',
+ verb='GET',
+ autodoc=f'{document_directory}/urls_get.md'
+ )
+ with self.given(**call):
+ then(response.status_code == 302)
+
+ call = dict(
+ title='GET',
+ description='Invalid hash id',
+ url='/urls/lP5',
+ verb='GET'
+ )
+ with self.given(**call):
+ then(response.status_code == 400)
+
+ call = dict(
+ title='GET',
+ description='Not found url with hash id',
+ url='/urls/lP',
+ verb='GET'
+ )
+ with self.given(**call):
+ then(response.status_code == 404)
+
+
+if __name__ == '__main__': # pragma: no cover
+ unittest.main()