From dd617a365f7dc3f1e94e0d1312c989ccd67f696e Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Fri, 30 Jun 2023 08:05:28 -0500 Subject: [PATCH 01/15] build: use pyproject.toml for dependency management over requirements.txt adds the ability to split out testing dependencies from the application runtime dependencies --- Dockerfile | 9 +++----- pyproject.toml | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 41 ----------------------------------- 3 files changed, 59 insertions(+), 47 deletions(-) create mode 100644 pyproject.toml delete mode 100755 requirements.txt diff --git a/Dockerfile b/Dockerfile index ddf2df0..c967829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,12 +29,9 @@ RUN tar jxf phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ rm phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ mv phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/phantomjs -RUN pip install --upgrade pip - -ADD ./requirements.txt /sirius/requirements.txt -RUN pip install --no-cache-dir -r requirements.txt - -RUN pip install honcho +RUN pip install --upgrade pip setuptools wheel +ADD ./pyproject.toml /sirius/pyproject.toml +RUN pip install --no-cache-dir . EXPOSE 5000 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b13751a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,56 @@ +[build-system] +requires = ["setuptools", "wheel"] + +[project] +name = "sirius" +readme = "README.md" +requires-python = ">=3.7,<3.8" +version = "0.0.0" +dependencies = [ + "Flask==1.1.1", + "Flask-Bootstrap==3.3.7.1", + "Flask-Cors==3.0.8", + "Flask-Dance==2.2.0", + "Flask-Login==0.4.1", + "Flask-Migrate==2.5.2", + "Flask-SQLAlchemy==2.4.0", + "Flask-Script==2.0.6", + "Flask-Sockets==0.2.1", + "Flask-WTF==0.14.2", + "Jinja2==2.10.1", + "Mako==1.1.0", + "MarkupSafe==1.1.1", + "Pillow==6.1.0", + "SQLAlchemy==1.3.8", + "WTForms==2.2.1", + "Werkzeug==0.15.6", + "alembic==1.1.0", + "argparse==1.2.1", + "backports.ssl-match-hostname==3.7.0.1", + "blinker==1.4", + "gevent==1.4.0", + "gevent-websocket==0.10.1", + "greenlet==0.4.15", + "gunicorn==19.9.0", + "httplib2==0.13.1", + "itsdangerous==1.1.0", + "nose==1.3.7", + "oauth2==1.9.0.post1", + "oauthlib==3.1.0", + "psycopg2-binary==2.8.3", + "pycrypto==2.6.1", + "redis==3.3.8", + "requests==2.22.0", + "requests-oauthlib==1.2.0", + "selenium==3.141.0", + "six==1.12.0", + "twitter==1.18.0", + "websocket-client==0.56.0", + "honcho", +] + +[project.optional-dependencies] +test = [ + "snapshottest @ git+git://github.com/syrusakbary/snapshottest.git@4ac2b4fb09e9e7728bebb11967c164a914775d1d#snapshottest", + "Flask-Testing==0.7.1", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100755 index 93e819e..0000000 --- a/requirements.txt +++ /dev/null @@ -1,41 +0,0 @@ -Flask==1.1.1 -Flask-Bootstrap==3.3.7.1 -Flask-Cors==3.0.8 -Flask-Dance==2.2.0 -Flask-Login==0.4.1 -Flask-Migrate==2.5.2 -Flask-SQLAlchemy==2.4.0 -Flask-Script==2.0.6 -Flask-Sockets==0.2.1 -Flask-Testing==0.7.1 -Flask-WTF==0.14.2 -Jinja2==2.10.1 -Mako==1.1.0 -MarkupSafe==1.1.1 -Pillow==6.1.0 -SQLAlchemy==1.3.8 -WTForms==2.2.1 -Werkzeug==0.15.6 -alembic==1.1.0 -argparse==1.2.1 -backports.ssl-match-hostname==3.7.0.1 -blinker==1.4 -gevent==1.4.0 -gevent-websocket==0.10.1 -greenlet==0.4.15 -gunicorn==19.9.0 -httplib2==0.13.1 -itsdangerous==1.1.0 -nose==1.3.7 -oauth2==1.9.0.post1 -oauthlib==3.1.0 -psycopg2-binary==2.8.3 -pycrypto==2.6.1 -redis==3.3.8 -requests==2.22.0 -requests-oauthlib==1.2.0 -selenium==3.141.0 -six==1.12.0 -git+git://github.com/syrusakbary/snapshottest.git@4ac2b4fb09e9e7728bebb11967c164a914775d1d#snapshottest -twitter==1.18.0 -websocket-client==0.56.0 From 91ff61574cb52298c21fb991ca31fd5f0a22e517 Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Fri, 30 Jun 2023 08:06:47 -0500 Subject: [PATCH 02/15] build: update docker image to bullseye from buster --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c967829..cb3617a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.4-slim-buster +FROM python:3.7-slim-bullseye WORKDIR /sirius From 9c86da51d1618e056b1b0912d11b5d2614eaed77 Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Tue, 4 Jul 2023 18:56:25 -0500 Subject: [PATCH 03/15] feat(twitter): update twitter auth to use oauth2 flow --- .dockerignore | 4 + .gitignore | 3 +- README.md | 9 +- pyproject.toml | 6 +- sirius/config.py | 40 +++++---- sirius/web/twitter.py | 185 ++++++++++++++++++++---------------------- 6 files changed, 127 insertions(+), 120 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e6f9255 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.circleci +notes +docker-compose* +*.md diff --git a/.gitignore b/.gitignore index 4123d6a..efd80eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Custom venv/ -.env +.env* +!.env.example certs/ diff --git a/README.md b/README.md index 8fad313..97ec8d4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ docker-compose -f docker-compose.yml -f docker-compose.development.yml up Or if you have your own database, you can configure the `DEV_DATABASE_URL` environment variable in `.env`, and then simply run: -``` +```sh docker-compose up ``` @@ -57,9 +57,10 @@ docker-compose up The server can be configured with the following variables: -``` -TWITTER_CONSUMER_KEY=... -TWITTER_CONSUMER_SECRET=... +```properties +TWITTER_OAUTH_CLIENT_KEY=... +TWITTER_OAUTH_CLIENT_SECRET=... +OAUTH_REDIRECT_URI=... FLASK_CONFIG=... DATABASE_URL=... ``` diff --git a/pyproject.toml b/pyproject.toml index b13751a..8c961d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,6 @@ dependencies = [ "Flask==1.1.1", "Flask-Bootstrap==3.3.7.1", "Flask-Cors==3.0.8", - "Flask-Dance==2.2.0", "Flask-Login==0.4.1", "Flask-Migrate==2.5.2", "Flask-SQLAlchemy==2.4.0", @@ -35,13 +34,12 @@ dependencies = [ "httplib2==0.13.1", "itsdangerous==1.1.0", "nose==1.3.7", - "oauth2==1.9.0.post1", - "oauthlib==3.1.0", + "oauthlib==3.2.2", "psycopg2-binary==2.8.3", "pycrypto==2.6.1", "redis==3.3.8", "requests==2.22.0", - "requests-oauthlib==1.2.0", + "requests-oauthlib==1.3.1", "selenium==3.141.0", "six==1.12.0", "twitter==1.18.0", diff --git a/sirius/config.py b/sirius/config.py index 99ad23d..0b1d417 100755 --- a/sirius/config.py +++ b/sirius/config.py @@ -1,41 +1,52 @@ import os + basedir = os.path.abspath(os.path.dirname(__file__)) class Config: - SECRET_KEY = os.environ.get('SECRET_KEY') or 'this is the lp2 secret' + SECRET_KEY = os.environ.get("SECRET_KEY") or "this is the lp2 secret" SQLALCHEMY_COMMIT_ON_TEARDOWN = True SQLALCHEMY_TRACK_MODIFICATIONS = False # printer not seen for 60 seconds: Mark offline. PRINTER_OFFLINE_CUTOFF_SECONDS = 60 + TWITTER_OAUTH_CLIENT_KEY = os.getenv("TWITTER_OAUTH_CLIENT_KEY") + TWITTER_OAUTH_CLIENT_SECRET = os.getenv("TWITTER_OAUTH_CLIENT_SECRET") + OAUTH_REDIRECT_URI = os.getenv("OAUTH_REDIRECT_URI") @staticmethod def init_app(app): pass + class TestConfig(Config): DEBUG = False - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ - 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') + SQLALCHEMY_DATABASE_URI = os.environ.get( + "DATABASE_URL" + ) or "sqlite:///" + os.path.join(basedir, "data-dev.sqlite") TESTING = True WTF_CSRF_ENABLED = False LOGIN_DISABLED = False + class DevelopmentConfig(Config): DEBUG = True - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ - 'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite') + SQLALCHEMY_DATABASE_URI = os.environ.get( + "DATABASE_URL" + ) or "sqlite:///" + os.path.join(basedir, "data-dev.sqlite") + class ProductionConfig(Config): - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ - 'sqlite:///' + os.path.join(basedir, 'data.sqlite') + SQLALCHEMY_DATABASE_URI = os.environ.get( + "DATABASE_URL" + ) or "sqlite:///" + os.path.join(basedir, "data.sqlite") @classmethod def init_app(cls, app): Config.init_app(app) + class HerokuConfig(ProductionConfig): - SSL_DISABLE = bool(os.environ.get('SSL_DISABLE')) + SSL_DISABLE = bool(os.environ.get("SSL_DISABLE")) DEBUG = True @classmethod @@ -44,21 +55,22 @@ def init_app(cls, app): # handle proxy server headers from werkzeug.contrib.fixers import ProxyFix + app.wsgi_app = ProxyFix(app.wsgi_app) # log to stderr import logging from logging import StreamHandler + file_handler = StreamHandler() file_handler.setLevel(logging.WARNING) app.logger.addHandler(file_handler) config = { - 'development': DevelopmentConfig, - 'production': ProductionConfig, - 'heroku': HerokuConfig, - 'test': TestConfig, - - 'default': DevelopmentConfig + "development": DevelopmentConfig, + "production": ProductionConfig, + "heroku": HerokuConfig, + "test": TestConfig, + "default": DevelopmentConfig, } diff --git a/sirius/web/twitter.py b/sirius/web/twitter.py index 5156e12..f144b64 100644 --- a/sirius/web/twitter.py +++ b/sirius/web/twitter.py @@ -1,51 +1,75 @@ -import os -import datetime -import flask import logging -from gevent import pool + +from requests_oauthlib import OAuth2Session +import requests +import flask import flask_login as login -from flask_dance.consumer import oauth_authorized, oauth_error -from flask_dance.contrib.twitter import make_twitter_blueprint, twitter -import twitter as twitter_api from sirius.models import user as user_model from sirius.models.db import db logger = logging.getLogger(__name__) -api_key=os.environ.get('TWITTER_CONSUMER_KEY', 'DdrpQ1uqKuQouwbCsC6OMA4oF') -api_secret=os.environ.get('TWITTER_CONSUMER_SECRET', 'S8XGuhptJ8QIJVmSuIk7k8wv3ULUfMiCh9x1b19PmKSsBh1VDM') +blueprint = flask.Blueprint("twitter", __name__) + +auth_url = "https://twitter.com/i/oauth2/authorize" +token_url = "https://api.twitter.com/2/oauth2/token" + +scopes = ["tweet.read", "users.read", "offline.access"] + +oauth_session: OAuth2Session +code_challenge: str +code_verifier: str -# TODO move the consumer_key/secret to flask configuration. The -# current key is a test app that redirects to 127.0.0.1:8000. -blueprint = make_twitter_blueprint( - api_key=api_key, - api_secret=api_secret, -) -# @blueprint.route('/twitter/login') -# def twitter_login(): -# # Clear token, see https://github.com/mitsuhiko/flask-oauth/issues/48: -# flask.session.pop('twitter_token', None) -# flask.session.pop('twitter_screen_name', None) +def make_token(): + global oauth_session + global code_verifier + global code_challenge + + client_id = flask.current_app.config.get("TWITTER_OAUTH_CLIENT_KEY") + redirect_uri = flask.current_app.config.get("OAUTH_REDIRECT_URI") + + oauth_session = OAuth2Session(client_id, redirect_uri=redirect_uri, scope=scopes) + code_verifier = oauth_session._client.create_code_verifier(43) + code_challenge = oauth_session._client.create_code_challenge( + code_verifier, code_challenge_method="S256" + ) + + +def get_self(): + access_token = flask.session.get("twitter_token").get("access_token") + resp = requests.get( + url="https://api.twitter.com/2/users/me", + headers={ + "Authorization": f"Bearer {access_token}", + "Content-Type": "application/json", + }, + ) + if not resp.ok: + rule = flask.request.url_rule + msg = f"Error from {rule.endpoint}!" + logger.debug(msg) + flask.flash(msg, category="error") + return flask.redirect("/") -# return twitter.authorize(callback=flask.url_for('twitter_oauth.oauth_authorized', -# next=flask.request.args.get('next') or flask.request.referrer or None)) + return resp.json().get("data") -@blueprint.route('/twitter/logout') + +@blueprint.route("/twitter/logout") def twitter_logout(): - flask.session.pop('user_id', None) - flask.flash('You were signed out') - return flask.redirect('/') + flask.session.pop("user_id", None) + flask.flash("You were signed out") + return flask.redirect("/") -def process_authorization(token, token_secret, screen_name, next_url): +def process_authorization(token, next_url): """Process the incoming twitter oauth data. Validation has already succeeded at this point and we're just doing the book-keeping.""" - flask.session['twitter_token'] = (token, token_secret) - flask.session['twitter_screen_name'] = screen_name - + flask.session["twitter_token"] = token + screen_name = get_self().get("username") + flask.session["twitter_screen_name"] = screen_name oauth = user_model.TwitterOAuth.query.filter_by( screen_name=screen_name, ).first() @@ -60,15 +84,10 @@ def process_authorization(token, token_secret, screen_name, next_url): oauth = user_model.TwitterOAuth( user=new_user, screen_name=screen_name, - token=token, - token_secret=token_secret, - last_friend_refresh=datetime.datetime.utcnow(), + token=token.get("access_token"), + token_secret=token.get("access_token"), ) - # Fetch friends list from twitter. TODO: error handling. - friends = get_friends(new_user) - oauth.friends = friends - db.session.add(new_user) db.session.add(oauth) db.session.commit() @@ -80,68 +99,40 @@ def process_authorization(token, token_secret, screen_name, next_url): return flask.redirect(next_url) -def get_friends(user): - return get_friends_using_tokens(oauth_token=user.twitter_oauth.token, oauth_token_secret=user.twitter_oauth.token_secret) - -def get_friends_using_tokens(oauth_token, oauth_token_secret): - api = twitter_api.Twitter(auth=twitter_api.OAuth( - oauth_token, - oauth_token_secret, - api_key, - api_secret, - )) - # Twitter allows lookup of 100 users at a time so we need to - # chunk: - chunk = lambda l, n: [l[x:x+n] for x in range(0, len(l), n)] - friend_ids = list(api.friends.ids()['ids']) - - greenpool = pool.Pool(4) - - # Look up in parallel. Note that twitter has pretty strict 15 - # requests/second rate limiting. - friends = [] - for result in greenpool.imap( - lambda ids: api.users.lookup(user_id=','.join(str(id) for id in ids)), - chunk(friend_ids, 100)): - for r in result: - friends.append(user_model.Friend( - screen_name=r['screen_name'], - name=r['name'], - profile_image_url=r['profile_image_url'], - )) - - return sorted(friends) - - -@oauth_authorized.connect_via(blueprint) -def twitter_logged_in(blueprint, token): - # TODO: I think this comes via sesstion these days? - next_url = flask.request.args.get('next') or '/' +@blueprint.route("/twitter") +def twitter_logged_in(): + make_token() - if token is None: - flask.flash("Twitter didn't authorize our sign-in request.", category="error") + authorization_url, state = oauth_session.authorization_url( + auth_url, code_challenge=code_challenge, code_challenge_method="S256" + ) + flask.session["oauth_state"] = state + return flask.redirect(authorization_url) + + +@blueprint.route("/twitter/authorized", methods=["GET"]) +def twitter_callback(): + client_secret = flask.current_app.config.get("TWITTER_OAUTH_CLIENT_SECRET") + next_url = "/" + code = flask.request.args.get("code") + error = flask.request.args.get("error") + if error is not None: + logger.debug("twitter_callback: we got a problem") + rule = flask.request.url_rule + msg = f"OAuth error from {rule.endpoint}! message={error}" + logger.debug(msg) + flask.flash(msg, category="error") return flask.redirect(next_url) - process_authorization( - token['oauth_token'], - token['oauth_token_secret'], - token['screen_name'], - next_url, + token = oauth_session.fetch_token( + token_url=token_url, + client_secret=client_secret, + code_verifier=code_verifier, + code=code, ) - return False - -# notify on OAuth provider error -@oauth_error.connect_via(blueprint) -def twitter_error(blueprint, message, response): - logger.debug("we got a problem") - msg = ( - "OAuth error from {name}! " - "message={message} response={response}" - ).format( - name=blueprint.name, - message=message, - response=response, - ) - logger.debug(msg) - flask.flash(msg, category="error") \ No newline at end of file + if token is None: + flask.flash("Twitter didn't authorize our sign-in request.", category="error") + return flask.redirect(next_url) + + return process_authorization(token, next_url) From 2c5df56b005453a544a324247923bebfbb309fd4 Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Tue, 4 Jul 2023 19:02:19 -0500 Subject: [PATCH 04/15] feat(friends): remove friends functionality and references the twitter api is becoming prohibitive to use so removing friends reduces dependency BREAKING CHANGE: --- README.md | 14 +-- .../versions/89a031bd9657_remove_friends.py | 46 ++++++++++ sirius/models/user.py | 87 +++++-------------- sirius/testing/base.py | 16 ++-- sirius/web/external_api.py | 38 ++++---- sirius/web/landing.py | 71 ++++----------- sirius/web/printer_print.py | 70 +++++++-------- sirius/web/test_friends.py | 68 --------------- sirius/web/test_oauth_flow.py | 14 +-- 9 files changed, 144 insertions(+), 280 deletions(-) create mode 100644 migrations/versions/89a031bd9657_remove_friends.py delete mode 100644 sirius/web/test_friends.py diff --git a/README.md b/README.md index 97ec8d4..0f5d2ef 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ DATABASE_URL=... These can be set in the `.env` file, and an example is available in `.env.sample` in your checkout. -### Creating fake printers and friends +### Creating fake printers Resetting the actual hardware all the time gets a bit tiresome so there's a fake command that creates unclaimed fake little printers: @@ -84,18 +84,6 @@ Created printer Functionally there is no difference between resetting and creating a new printer so we don't distinguish between the two. -To create a fake friend from twitter who signed up do this: - -```console -$ ./manage.py fake user stephenfry -``` - -You can also claim a printer in somebody else's name: - -```console -$ ./manage.py fake claim b7235a2b432585eb quentinsf 342f-eyh0-korc-msej testprinter -``` - ## Sirius Architecture ### Layers diff --git a/migrations/versions/89a031bd9657_remove_friends.py b/migrations/versions/89a031bd9657_remove_friends.py new file mode 100644 index 0000000..c4be086 --- /dev/null +++ b/migrations/versions/89a031bd9657_remove_friends.py @@ -0,0 +1,46 @@ +"""Remove friends + +Revision ID: 89a031bd9657 +Revises: 44c4368c2db1 +Create Date: 2023-07-04 18:44:13.041005 + +""" + +# revision identifiers, used by Alembic. +revision = "89a031bd9657" +down_revision = "44c4368c2db1" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint(None, "claim_code", ["claim_code"]) + op.create_foreign_key(None, "print_key", "print_key", ["parent_id"], ["id"]) + op.create_unique_constraint(None, "printer", ["used_claim_code"]) + op.drop_column("twitter_o_auth", "last_friend_refresh") + op.drop_column("twitter_o_auth", "friends") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "twitter_o_auth", + sa.Column("friends", postgresql.BYTEA(), autoincrement=False, nullable=True), + ) + op.add_column( + "twitter_o_auth", + sa.Column( + "last_friend_refresh", + postgresql.TIMESTAMP(), + autoincrement=False, + nullable=True, + ), + ) + op.drop_constraint(None, "printer", type_="unique") + op.drop_constraint(None, "print_key", type_="foreignkey") + op.drop_constraint(None, "claim_code", type_="unique") + # ### end Alembic commands ### diff --git a/sirius/models/user.py b/sirius/models/user.py index cb20160..1687c9b 100644 --- a/sirius/models/user.py +++ b/sirius/models/user.py @@ -12,8 +12,12 @@ from sirius.models import hardware -class CannotChangeOwner(Exception): pass -class ClaimCodeInUse(Exception): pass +class CannotChangeOwner(Exception): + pass + + +class ClaimCodeInUse(Exception): + pass class User(db.Model): @@ -25,10 +29,11 @@ class User(db.Model): # providers. For now we just copy the twitter handle. username = db.Column(db.String) twitter_oauth = db.relationship( - 'TwitterOAuth', uselist=False, backref=db.backref('user')) + "TwitterOAuth", uselist=False, backref=db.backref("user") + ) def __repr__(self): - return 'User {}'.format(self.username) + return "User {}".format(self.username) # Flask-login interface: @property @@ -43,7 +48,9 @@ def get_id(self): return self.id def generate_api_key(self): - self.api_key = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32)) + self.api_key = "".join( + random.choice(string.ascii_letters + string.digits) for _ in range(32) + ) def claim_printer(self, claim_code, name): """Claiming can happen before the printer "calls home" for the first @@ -59,8 +66,8 @@ def claim_printer(self, claim_code, name): if hcc is not None and hcc.by != self: raise ClaimCodeInUse( - "Claim code {} already claimed by {}".format( - claim_code, hcc.by)) + "Claim code {} already claimed by {}".format(claim_code, hcc.by) + ) if hcc is None: hcc = hardware.ClaimCode( @@ -77,8 +84,7 @@ def claim_printer(self, claim_code, name): # Check whether we've seen this printer and if so: connect it # to claim code and make it "owned" but *only* if it does not # have an owner yet. - printer_query = hardware.Printer.query.filter_by( - hardware_xor=hardware_xor) + printer_query = hardware.Printer.query.filter_by(hardware_xor=hardware_xor) printer = printer_query.first() if printer is None: return @@ -86,10 +92,13 @@ def claim_printer(self, claim_code, name): if printer.owner is not None and printer.owner != self: raise CannotChangeOwner( "Printer {} already owned by {}. Cannot claim for {}.".format( - printer, printer.owner, self)) + printer, printer.owner, self + ) + ) - assert printer_query.count() == 1, \ - "hardware xor collision: {}".format(hardware_xor) + assert printer_query.count() == 1, "hardware xor collision: {}".format( + hardware_xor + ) printer.used_claim_code = claim_code printer.hardware_xor = hardware_xor @@ -98,40 +107,6 @@ def claim_printer(self, claim_code, name): db.session.add(printer) return printer - def signed_up_friends(self): - """ - A "friend" is someone this user follows on twitter. - - :returns: 2-tuple of (all friends, list of people who can print on - this user's printer) - """ - friends = self.twitter_oauth.friends - if not friends: - return [], [] - return friends, User.query.filter( - User.username.in_(x.screen_name for x in friends)) - - def friends_printers(self): - """ - :returns: List of printers this user can print on. - """ - # TODO(tom): Querying the full database is O(N) and will not - # scale. Replace the model with a N:M relationship. - sn = self.twitter_oauth.screen_name - reverse_friend_ids = [ - x.user_id for x in TwitterOAuth.query.all() - for y in (x.friends or []) - if sn == y.screen_name] - - # Avoid expensive sql by checking for list. - if not reverse_friend_ids: - return [] - - return hardware.Printer.query.filter( - hardware.Printer.owner_id.in_(reverse_friend_ids)) - - -Friend = collections.namedtuple('Friend', 'screen_name name profile_image_url') class TwitterOAuth(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -141,23 +116,3 @@ class TwitterOAuth(db.Model): token = db.Column(db.String) token_secret = db.Column(db.String) - - # Twitter comes with a 15 requests / 15 minutes rate. To avoid - # people DOS'ing our service we allow only one refresh per hour. - last_friend_refresh = db.Column(db.DateTime) - - # List of Friend objects (see named tuple above this class) - friends = db.Column(db.PickleType()) - - def seconds_to_next_refresh(self, utcnow=None): - if utcnow is None: - utcnow = datetime.datetime.utcnow() - - if self.last_friend_refresh is None: - return 0 - - seconds_since_refresh = ( - utcnow - self.last_friend_refresh - ).total_seconds() - 3600 - - return abs(min(seconds_since_refresh, 0)) diff --git a/sirius/testing/base.py b/sirius/testing/base.py index 0f7cdcc..2d1eb52 100644 --- a/sirius/testing/base.py +++ b/sirius/testing/base.py @@ -8,11 +8,10 @@ # pylint: disable=no-member class Base(testing.TestCase): - """Common base class for tests that require database interactions. - """ + """Common base class for tests that require database interactions.""" def create_app(self): - app = webapp.create_app('test') + app = webapp.create_app("test") return app def setUp(self): @@ -22,11 +21,10 @@ def setUp(self): self.testuser = user.User( username="testuser", twitter_oauth=user.TwitterOAuth( - screen_name='testuser', - friends=[], + screen_name="testuser", token="token", token_secret="token_secret", - ) + ), ) db.session.add(self.testuser) db.session.flush() @@ -42,9 +40,9 @@ def autologin(self): # Flask session doesn't pick up the cookies if I call just # login_user. The login_user call needs to be embedded in a # request. - @self.app.route('/autologin') + @self.app.route("/autologin") def autologin(): login.login_user(self.testuser) - return '' + return "" - self.client.get('/autologin') + self.client.get("/autologin") diff --git a/sirius/web/external_api.py b/sirius/web/external_api.py index d591616..3786791 100644 --- a/sirius/web/external_api.py +++ b/sirius/web/external_api.py @@ -1,12 +1,10 @@ import io import datetime +import json + import flask import flask_login as login from flask import request -import flask_wtf -import wtforms -import base64 -import json from sirius.models.db import db from sirius.models import hardware @@ -15,12 +13,12 @@ from sirius.protocol import messages from sirius.coding import image_encoding from sirius.coding import templating -from sirius import stats -blueprint = flask.Blueprint('external_api', __name__) +blueprint = flask.Blueprint("external_api", __name__) + # TODO : need a refactor as this is code duplication from printer_print -@blueprint.route('/ext_api/v1/printer//print_html', methods=['POST']) +@blueprint.route("/ext_api/v1/printer//print_html", methods=["POST"]) @login.login_required def print_html(printer_id): printer = hardware.Printer.query.get(printer_id) @@ -28,31 +26,26 @@ def print_html(printer_id): flask.abort(404) # PERMISSIONS - # the printer must either belong to this user, or be - # owned by a friend + # the printer must either belong to this user if printer.owner.id == login.current_user.id: # fine pass - elif printer.id in [p.id for p in login.current_user.friends_printers()]: - # fine - pass else: flask.abort(404) request.get_data() task = json.loads(request.data) - if not task['message'] or not task['face']: + if not task["message"] or not task["face"]: flask.abort(500) pixels = image_encoding.default_pipeline( templating.default_template( - task['message'], - from_name=login.current_user.username + task["message"], from_name=login.current_user.username ) ) hardware_message = None - if task['face'] == "noface": + if task["face"] == "noface": hardware_message = messages.SetDeliveryAndPrintNoFace( device_address=printer.device_address, pixels=pixels, @@ -63,11 +56,11 @@ def print_html(printer_id): pixels=pixels, ) - # If a printer is "offline" then we won't find the printer # connected and success will be false. success, next_print_id = protocol_loop.send_message( - printer.device_address, hardware_message) + printer.device_address, hardware_message + ) # Store the same message in the database. png = io.BytesIO() @@ -81,15 +74,16 @@ def print_html(printer_id): # We know immediately if the printer wasn't online. if not success: - model_message.failure_message = 'Printer offline' + model_message.failure_message = "Printer offline" model_message.response_timestamp = datetime.datetime.utcnow() db.session.add(model_message) response = {} if success: - response['status'] = 'Sent your message to the printer!' + response["status"] = "Sent your message to the printer!" else: - response['status'] = ("Could not send message because the " - "printer {} is offline.").format(printer.name) + response["status"] = ( + "Could not send message because the " "printer {} is offline." + ).format(printer.name) return json.dumps(response) diff --git a/sirius/web/landing.py b/sirius/web/landing.py index eee5288..855337a 100644 --- a/sirius/web/landing.py +++ b/sirius/web/landing.py @@ -2,22 +2,20 @@ import flask_wtf import wtforms import flask_login as login -import datetime from sirius.coding import claiming -from sirius.web import twitter -blueprint = flask.Blueprint('landing', __name__) +blueprint = flask.Blueprint("landing", __name__) class ClaimForm(flask_wtf.FlaskForm): claim_code = wtforms.StringField( - 'Claim code', + "Claim code", validators=[wtforms.validators.DataRequired()], ) printer_name = wtforms.StringField( - 'Name your printer', + "Name your printer", validators=[wtforms.validators.DataRequired()], ) @@ -30,17 +28,15 @@ def validate_claim_code(self, field): ) -class TwitterRefreshFriendsForm(flask_wtf.FlaskForm): - "CSRF-only form." - -@blueprint.route('/about') +@blueprint.route("/about") def about(): - return flask.render_template('about.html') + return flask.render_template("about.html") + -@blueprint.route('/') +@blueprint.route("/") def landing(): if not login.current_user.is_authenticated: - return flask.render_template('landing.html') + return flask.render_template("landing.html") return overview() @@ -50,24 +46,10 @@ def overview(): user = login.current_user my_printers = user.printers.all() - friends, signed_up_friends = user.signed_up_friends() - form = TwitterRefreshFriendsForm() + return flask.render_template("overview.html", my_printers=my_printers) - friends_printers = user.friends_printers() - - return flask.render_template( - 'overview.html', - form=form, - my_printers=my_printers, - signed_up_friends=list(signed_up_friends), - friends=friends, - friends_printers=list(friends_printers), - seconds_to_next_refresh=user.twitter_oauth.seconds_to_next_refresh(), - last_friend_refresh=user.twitter_oauth.last_friend_refresh, - ) - -@blueprint.route('///claim', methods=['GET', 'POST']) +@blueprint.route("///claim", methods=["GET", "POST"]) @login.login_required def claim(user_id, username): user = login.current_user @@ -76,17 +58,13 @@ def claim(user_id, username): form = ClaimForm() if form.validate_on_submit(): - user.claim_printer( - form.claim_code.data, - form.printer_name.data) - return flask.redirect(flask.url_for('.landing')) - - return flask.render_template( - 'claim.html', - form=form, - ) + user.claim_printer(form.claim_code.data, form.printer_name.data) + return flask.redirect(flask.url_for(".landing")) + + return flask.render_template("claim.html", form=form) -@blueprint.route('///generate_api_key', methods=['POST']) + +@blueprint.route("///generate_api_key", methods=["POST"]) @login.login_required def generate_api_key(user_id, username): user = login.current_user @@ -94,19 +72,4 @@ def generate_api_key(user_id, username): assert username == user.username user.generate_api_key() - return flask.redirect(flask.url_for('.landing')) - -@blueprint.route('///twitter-friend-refresh', methods=['POST']) -@login.login_required -def twitter_friend_refresh(user_id, username): - user = login.current_user - assert user_id == login.current_user.get_id() - assert username == login.current_user.username - - assert user.twitter_oauth.seconds_to_next_refresh() == 0 - - # TODO error handling when hitting twitter rate limit ... - login.current_user.twitter_oauth.friends = twitter.get_friends(login.current_user) - login.current_user.twitter_oauth.last_friend_refresh = datetime.datetime.utcnow() - - return flask.redirect(flask.url_for('.landing')) + return flask.redirect(flask.url_for(".landing")) diff --git a/sirius/web/printer_print.py b/sirius/web/printer_print.py index 2bf8c42..daf155d 100644 --- a/sirius/web/printer_print.py +++ b/sirius/web/printer_print.py @@ -1,113 +1,107 @@ import io -import datetime +import base64 + import flask import flask_login as login import flask_wtf import wtforms -import base64 -from sirius.models.db import db from sirius.models import hardware -from sirius.models import messages as model_messages -from sirius.protocol import protocol_loop -from sirius.protocol import messages from sirius.coding import image_encoding from sirius.coding import templating from sirius import stats -blueprint = flask.Blueprint('printer_print', __name__) +blueprint = flask.Blueprint("printer_print", __name__) class PrintForm(flask_wtf.FlaskForm): target_printer = wtforms.SelectField( - 'Printer', + "Printer", coerce=int, validators=[wtforms.validators.DataRequired()], ) face = wtforms.SelectField( - 'Face', + "Face", coerce=str, validators=[wtforms.validators.DataRequired()], ) message = wtforms.TextAreaField( - 'Message', + "Message", validators=[wtforms.validators.DataRequired()], ) @login.login_required -@blueprint.route('/printer//print', methods=['GET', 'POST']) +@blueprint.route("/printer//print", methods=["GET", "POST"]) def printer_print(printer_id): printer = hardware.Printer.query.get(printer_id) if printer is None: flask.abort(404) # PERMISSIONS - # the printer must either belong to this user, or be - # owned by a friend + # the printer must either belong to this user if printer.owner.id == login.current_user.id: # fine pass - elif printer.id in [p.id for p in login.current_user.friends_printers()]: - # fine - pass else: flask.abort(404) form = PrintForm() # Note that the form enforces access permissions: People can't - # submit a valid printer-id that's not owned by the user or one of - # the user's friends. - choices = [ - (x.id, x.name) for x in login.current_user.printers - ] + [ - (x.id, x.name) for x in login.current_user.friends_printers() - ] + # submit a valid printer-id that's not owned by the user. + choices = [(x.id, x.name) for x in login.current_user.printers] form.target_printer.choices = choices form.face.choices = [("default", "Default face"), ("noface", "No face")] # Set default printer on get - if flask.request.method != 'POST': + if flask.request.method != "POST": form.target_printer.data = printer.id form.face.data = "default" if form.validate_on_submit(): try: printer.print_html( - html=form.message.data, - from_name='@'+login.current_user.username + html=form.message.data, + from_name="@" + login.current_user.username, ) - flask.flash('Sent your message to the printer!') + flask.flash("Sent your message to the printer!") except hardware.Printer.OfflineError: flask.flash( - "Could not send message because the printer {} is offline.".format(printer.name), - 'error' + "Could not send message because the printer {} is offline.".format( + printer.name + ), + "error", ) - return flask.redirect(flask.url_for( - 'printer_overview.printer_overview', - printer_id=printer.id)) + return flask.redirect( + flask.url_for("printer_overview.printer_overview", printer_id=printer.id) + ) return flask.render_template( - 'printer_print.html', + "printer_print.html", printer=printer, form=form, ) -@blueprint.route('///printer//preview', methods=['POST']) +@blueprint.route( + "///printer//preview", methods=["POST"] +) @login.login_required def preview(user_id, username, printer_id): assert user_id == login.current_user.id assert username == login.current_user.username - message = flask.request.data.decode('utf-8') + message = flask.request.data.decode("utf-8") pixels = image_encoding.default_pipeline( - templating.default_template(message, from_name=login.current_user.username)) + templating.default_template(message, from_name=login.current_user.username) + ) png = io.BytesIO() pixels.save(png, "PNG") - stats.inc('printer.preview') + stats.inc("printer.preview") - return ''.format(base64.b64encode(png.getvalue()).decode('utf-8')) + return ''.format( + base64.b64encode(png.getvalue()).decode("utf-8") + ) diff --git a/sirius/web/test_friends.py b/sirius/web/test_friends.py deleted file mode 100644 index 1c7b691..0000000 --- a/sirius/web/test_friends.py +++ /dev/null @@ -1,68 +0,0 @@ -from sirius.models.db import db -from sirius.models import user -from sirius.models import hardware -from sirius.testing import base - - -# pylint: disable=no-member -class TestFriends(base.Base): - - def setUp(self): - base.Base.setUp(self) - db.create_all() - u0 = user.User(username='u0') - u1 = user.User(username='u1') - oa0 = user.TwitterOAuth( - user=u0, - screen_name='twitter0', - token='token', - token_secret='token_secret', - friends=[user.Friend('twitter1', 't1', ''), user.Friend('twitter2', 't2', '')], - ) - oa1 = user.TwitterOAuth( - user=u1, - screen_name='twitter1', - token='token', - token_secret='token_secret', - friends=[user.Friend('twitter2', 't2', '')], - ) - db.session.add_all([u0, u1, oa0, oa1]) - db.session.commit() - self.u0 = u0 - self.u1 = u1 - - # Register a printer for user so we can test friends printers - u0.claim_printer('vcty-8s95-40hq-6k1z', 'u0 printer') - hardware.Printer.phone_home('dd5ebbcce362f584') - - u1.claim_printer('pmo0-bjf1-ste8-51c1', 'u1 printer') - hardware.Printer.phone_home('35fef3793dce4692') - - - def tearDown(self): - db.session.remove() - db.drop_all() - - - def test_signed_up_friends(self): - f0, _ = self.u0.signed_up_friends() - f1, _ = self.u1.signed_up_friends() - - self.assertCountEqual(f0, [ - user.Friend('twitter1', 't1', ''), - user.Friend('twitter2', 't2', '')]) - - self.assertCountEqual(f1, [ - user.Friend('twitter2', 't2', '')]) - - - def test_friends_printers(self): - fp0 = list(self.u0.friends_printers()) - fp1 = list(self.u1.friends_printers()) - # u0 follows u1 but the reverse is not true. That means u1 - # should be able to print on u0's printer but u0 should not be - # able to print on u1's printer - self.assertEqual(len(fp0), 0) - self.assertEqual(len(fp1), 1) - - self.assertEqual(fp1[0].name, 'u0 printer') diff --git a/sirius/web/test_oauth_flow.py b/sirius/web/test_oauth_flow.py index 6fc9852..ae352fe 100644 --- a/sirius/web/test_oauth_flow.py +++ b/sirius/web/test_oauth_flow.py @@ -6,10 +6,6 @@ from sirius.web import twitter -# Mock network-accessing function: -twitter.get_friends = lambda x: [] - - # pylint: disable=no-member class TestOAuthFlow(testing.TestCase): def setUp(self): @@ -21,16 +17,14 @@ def tearDown(self): db.drop_all() def create_app(self): - app = webapp.create_app('test') + app = webapp.create_app("test") return app def test_oauth_authorized(self): self.assertEqual(login.current_user.is_authenticated, False) twitter.process_authorization( - 'token', - 'secret_token', - 'test_screen_name', - '/next_url', + "token", + "/next_url", ) - self.assertEqual(login.current_user.username, 'test_screen_name') + self.assertEqual(login.current_user.username, "test_screen_name") self.assertEqual(login.current_user.is_authenticated, True) From d67323d6e6e9e3b019e147ab2d4532af2ecd086d Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 5 Jul 2023 22:39:31 +0100 Subject: [PATCH 05/15] Rollback the docker image upgrade --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cb3617a..c967829 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7-slim-bullseye +FROM python:3.7.4-slim-buster WORKDIR /sirius From 77017d755fc061a05d7c73c2102f9477ac628423 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 5 Jul 2023 22:39:48 +0100 Subject: [PATCH 06/15] Specify packages for setuptools --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8c961d9..528fdd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,3 +52,7 @@ test = [ "snapshottest @ git+git://github.com/syrusakbary/snapshottest.git@4ac2b4fb09e9e7728bebb11967c164a914775d1d#snapshottest", "Flask-Testing==0.7.1", ] + +[tool.setuptools.packages.find] +include = ["sirius*"] + From 72c14d9e68778b66ccbe603fece9c755f7ae5901 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 5 Jul 2023 22:40:01 +0100 Subject: [PATCH 07/15] modernise circle ci config --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index edf72ad..a502525 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: machine: - image: ubuntu-1604:201903-01 + image: ubuntu-2204:2023.04.2 steps: - checkout - run: From ff56e10b44556bfa697116c05a6233e0a66ce25d Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Wed, 5 Jul 2023 22:44:21 +0100 Subject: [PATCH 08/15] Ensure test deps are installed --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c967829..9c16e72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN tar jxf phantomjs-2.1.1-linux-x86_64.tar.bz2 && \ RUN pip install --upgrade pip setuptools wheel ADD ./pyproject.toml /sirius/pyproject.toml -RUN pip install --no-cache-dir . +RUN pip install --no-cache-dir .[test] EXPOSE 5000 From 916d262bac99820c1d033f92658f0ff43d85e22b Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Thu, 6 Jul 2023 12:36:27 -0500 Subject: [PATCH 09/15] build: match docker python base image to heroku buildpack runtime version, update snapshottest url --- Dockerfile | 2 +- pyproject.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9c16e72..25b404f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.4-slim-buster +FROM python:3.7.10-slim-buster WORKDIR /sirius diff --git a/pyproject.toml b/pyproject.toml index 528fdd1..fbc9107 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,10 +49,9 @@ dependencies = [ [project.optional-dependencies] test = [ - "snapshottest @ git+git://github.com/syrusakbary/snapshottest.git@4ac2b4fb09e9e7728bebb11967c164a914775d1d#snapshottest", + "snapshottest @ git+https://github.com/syrusakbary/snapshottest.git@1.0.0a0", "Flask-Testing==0.7.1", ] [tool.setuptools.packages.find] include = ["sirius*"] - From a48badce030d8bbbf9c00d788de3965b83286799 Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Thu, 6 Jul 2023 12:49:55 -0500 Subject: [PATCH 10/15] test(oauth_flow): fix access_token format in test --- sirius/web/test_oauth_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sirius/web/test_oauth_flow.py b/sirius/web/test_oauth_flow.py index ae352fe..b23c958 100644 --- a/sirius/web/test_oauth_flow.py +++ b/sirius/web/test_oauth_flow.py @@ -23,7 +23,7 @@ def create_app(self): def test_oauth_authorized(self): self.assertEqual(login.current_user.is_authenticated, False) twitter.process_authorization( - "token", + {"access_token": "token"}, "/next_url", ) self.assertEqual(login.current_user.username, "test_screen_name") From 61b935b6aecfd03f606c4f521c31510e7950e7aa Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Thu, 6 Jul 2023 13:49:02 -0500 Subject: [PATCH 11/15] test: mock twitter api call --- sirius/web/test_oauth_flow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sirius/web/test_oauth_flow.py b/sirius/web/test_oauth_flow.py index b23c958..f554bf0 100644 --- a/sirius/web/test_oauth_flow.py +++ b/sirius/web/test_oauth_flow.py @@ -1,3 +1,5 @@ +from unittest import mock + import flask_testing as testing import flask_login as login @@ -20,7 +22,9 @@ def create_app(self): app = webapp.create_app("test") return app - def test_oauth_authorized(self): + @mock.patch("twitter.get_self") + def test_oauth_authorized(self, get_self_mock): + get_self_mock.return_value = {"username": "test_screen_name"} self.assertEqual(login.current_user.is_authenticated, False) twitter.process_authorization( {"access_token": "token"}, From d645514d0aea2037335c0e769f5a4db3b24f384b Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Thu, 6 Jul 2023 14:18:41 -0500 Subject: [PATCH 12/15] test: fix mock in oauth test --- sirius/web/test_oauth_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sirius/web/test_oauth_flow.py b/sirius/web/test_oauth_flow.py index f554bf0..d2acf70 100644 --- a/sirius/web/test_oauth_flow.py +++ b/sirius/web/test_oauth_flow.py @@ -22,7 +22,7 @@ def create_app(self): app = webapp.create_app("test") return app - @mock.patch("twitter.get_self") + @mock.patch("sirius.web.twitter.get_self") def test_oauth_authorized(self, get_self_mock): get_self_mock.return_value = {"username": "test_screen_name"} self.assertEqual(login.current_user.is_authenticated, False) From 294b8d19c80370b52f9a8cbd346d2ae53c958165 Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Thu, 6 Jul 2023 14:38:49 -0500 Subject: [PATCH 13/15] build: add setup.py for heroku detection --- setup.py | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6a3ee3c --- /dev/null +++ b/setup.py @@ -0,0 +1,2 @@ +# needed for heroku deploy +# see https://github.com/heroku/heroku-buildpack-python/pull/834#issuecomment-1242217467 From 6802e73e8ce15ed6659803b9396c8b4595421b50 Mon Sep 17 00:00:00 2001 From: Phillip Ferentinos Date: Thu, 6 Jul 2023 14:42:59 -0500 Subject: [PATCH 14/15] fix(migration): remove constraint migrations --- migrations/versions/89a031bd9657_remove_friends.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/migrations/versions/89a031bd9657_remove_friends.py b/migrations/versions/89a031bd9657_remove_friends.py index c4be086..da2e251 100644 --- a/migrations/versions/89a031bd9657_remove_friends.py +++ b/migrations/versions/89a031bd9657_remove_friends.py @@ -17,9 +17,6 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_unique_constraint(None, "claim_code", ["claim_code"]) - op.create_foreign_key(None, "print_key", "print_key", ["parent_id"], ["id"]) - op.create_unique_constraint(None, "printer", ["used_claim_code"]) op.drop_column("twitter_o_auth", "last_friend_refresh") op.drop_column("twitter_o_auth", "friends") # ### end Alembic commands ### @@ -40,7 +37,4 @@ def downgrade(): nullable=True, ), ) - op.drop_constraint(None, "printer", type_="unique") - op.drop_constraint(None, "print_key", type_="foreignkey") - op.drop_constraint(None, "claim_code", type_="unique") # ### end Alembic commands ### From da5a626525af579171c4a6166b0996cc972580a7 Mon Sep 17 00:00:00 2001 From: Joe Rickerby Date: Fri, 7 Jul 2023 15:10:17 +0100 Subject: [PATCH 15/15] Use requirements.txt as this feels more ideomatic to me --- setup.py => requirements.txt | 1 + 1 file changed, 1 insertion(+) rename setup.py => requirements.txt (95%) diff --git a/setup.py b/requirements.txt similarity index 95% rename from setup.py rename to requirements.txt index 6a3ee3c..28c1da1 100644 --- a/setup.py +++ b/requirements.txt @@ -1,2 +1,3 @@ # needed for heroku deploy # see https://github.com/heroku/heroku-buildpack-python/pull/834#issuecomment-1242217467 +-e .