Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
dcd2a92
Rewrite to use Flask, support Python 3
benley Nov 4, 2022
3b903ba
More python 3 & flask updates
benley Nov 4, 2022
6f5531f
Rewrite to use ndb datastore
benley Nov 4, 2022
5ce9c01
Fix python2->3 cmp sorting thing
benley Nov 5, 2022
1b9a66c
cleanup app.yaml
benley Nov 7, 2022
d979f0b
Use google cloud logging library
benley Nov 7, 2022
b5f59fa
Always use SSL
benley Nov 9, 2022
8488bde
Fix /admin/update_settings, fix hostname setting
benley Nov 9, 2022
6b23405
use POST for /admin/update_settings
benley Nov 9, 2022
2e3e4c8
Swap some RuntimeErrors for http 403 responses
benley Nov 9, 2022
6876ef6
Finally got a working shell.nix for this project
benley Nov 15, 2022
94ce9ac
Fix email & chat reminder handlers
benley Nov 30, 2022
e6da913
Better error handling in update_snippet
benley Dec 2, 2022
b89c866
2to3 on *_test.py
benley Dec 2, 2022
c06a7e7
Progress on slack bot and fixing tests
benley Dec 2, 2022
fa6ff22
Authenticate slack users by their slack ID, don't trust emails
benley Dec 6, 2022
a8f7a58
Only use cloud logging if we're running on appengine
benley Dec 6, 2022
4971076
Fix hide/unhide/delete users (requires javascript)
benley Dec 7, 2022
7ba298b
Fix sending text to hipchat (untested, but probably works)
benley Dec 7, 2022
f7290ee
Add warmup requests handler, set min_instances=1
benley Dec 7, 2022
8a9ce91
Prune unused code
benley Dec 8, 2022
16307e7
Start to appease pyright
benley Dec 8, 2022
a04c1c3
Remove eventual-consistency workarounds
benley Dec 8, 2022
dc287cc
Fix most of the tests!
benley Jan 3, 2023
60d9965
Fix the rest of the tests!
benley Jan 3, 2023
d51f397
Suppress that one deprecation warning
benley Jan 4, 2023
b92c18c
Use time-machine consistently, fix up super() calls
benley Jan 5, 2023
08ab6e4
more docs links
benley Jan 5, 2023
831ba0a
Pin nixpkgs with niv, fix entrypoints
benley Jun 12, 2023
5067dd9
Send friday reminder at 1pm Pacific
benley Jun 12, 2023
7fe8423
fix static URLs in local dev, add make typecheck
benley Jun 12, 2023
375ca43
Use Procfile for local dev with ndb emulator
benley Jun 12, 2023
e7ccdda
Code review feedback, round 1
benley Jul 24, 2023
8b0220a
Whoops, restore the missing wrap_wsgi_app
benley Jul 24, 2023
28f2ae4
Simplify sort_by method, drop cmp_to_key
benley Jul 24, 2023
09f78d1
Replace _TODAY_FN with datetime.datetime.now
benley Jul 24, 2023
6935f7b
Merge remote-tracking branch 'origin/master' into python3-flask-ndb
benley Jul 26, 2023
0bb2d01
Use flask blueprints to register /slack
benley Jul 26, 2023
8292e44
pep8 nits
benley Jul 26, 2023
7f460fb
Call datetime.now() only once per function
benley Jul 26, 2023
b9670ea
Another pep8 nit
benley Jul 26, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
use nix;
6 changes: 6 additions & 0 deletions .gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.git
.DS_Store
*.pyc
*.nix
.envrc
#!include:.gitignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
*.pyc
.direnv
__pycache__
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
.PHONY: serve test_deps test check appcfg-update deploy

serve:
dev_appserver.py --log_level=debug . --host=0.0.0.0
honcho start

test_deps:
pip install -r requirements-dev.txt

test check:
python -m unittest discover -p '*_test.py'
test:
pytest

typecheck:
pyright

appcfg-update deploy:
gcloud app deploy --project "${APP}"
gcloud app deploy

create-indexes:
gcloud datastore indexes create index.yaml
2 changes: 2 additions & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
datastore: gcloud beta emulators datastore start
web: $(gcloud beta emulators datastore env-init); python main.py
34 changes: 16 additions & 18 deletions app.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
runtime: python27
threadsafe: yes
api_version: 1
runtime: python310
default_expiration: "365d"
app_engine_apis: true

# Slack has a very short timeout on API callbacks, so we need to keep at least 1
# process running for it
automatic_scaling:
min_instances: 1

# min_instances requires warmup requests to work
inbound_services:
- warmup

handlers:
- url: /static
Expand All @@ -13,23 +21,13 @@ handlers:
upload: static/favicon.ico

- url: /admin/.*
script: snippets.application
script: auto
login: admin
secure: always

- url: .*
script: snippets.application

skip_files:
- .git
- .DS_Store
- .*.pyc
- url: /.*
script: auto
secure: always

builtins:
- remote_api: on

libraries:
- name: jinja2
version: "2.6"
# This also brings in webapp2_extras:
- name: webapp2
version: "2.5.1"
35 changes: 35 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""conftest - loaded automatically by the pytest runner"""

from unittest.mock import MagicMock

from google.cloud import ndb
from google.cloud.ndb import _datastore_api
from InMemoryCloudDatastoreStub.datastore_stub import LocalDatastoreStub
import pytest
from _pytest.monkeypatch import MonkeyPatch


@pytest.fixture(autouse=True)
def ndb_stub(monkeypatch: MonkeyPatch) -> LocalDatastoreStub:
stub = LocalDatastoreStub()
monkeypatch.setattr(_datastore_api, "stub", MagicMock(return_value=stub))
return stub


@pytest.fixture(autouse=True)
def ndb_context(init_ndb_env_vars):
client = ndb.Client()
with client.context() as context:
yield context


@pytest.fixture(autouse=True)
def init_ndb_env_vars(monkeypatch: MonkeyPatch) -> None:
"""Set environment variables for the test ndb client.

Initializing an ndb Client in a test env requires some environment variables
to be set. For now, these are just garbage values intended to give the
library _something_ (we don't expect them to actually work yet)
"""
monkeypatch.setenv("DATASTORE_EMULATOR_HOST", "localhost")
monkeypatch.setenv("DATASTORE_DATASET", "datastore-stub-test")
2 changes: 1 addition & 1 deletion cron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ cron:

- description: snippets chat -- early reminder to write snippets
url: /admin/send_friday_reminder_chat
schedule: every friday 16:00
schedule: every friday 13:00
timezone: US/Pacific

- description: snippets email -- reminder to write snippets
Expand Down
32 changes: 32 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3

import snippets

import google.appengine.api
import google.cloud.ndb


class NDBMiddleware:
"""WSGI middleware to wrap the app in Google Cloud NDB context"""
def __init__(self, app):
self.app = app
self.client = google.cloud.ndb.Client()

def __call__(self, environ, start_response):
with self.client.context():
return self.app(environ, start_response)

app = snippets.app
app.wsgi_app = google.appengine.api.wrap_wsgi_app(app.wsgi_app)
app.wsgi_app = NDBMiddleware(app.wsgi_app)


if __name__ == '__main__':
# This is used when running locally only. When deploying to Google App
# Engine, a webserver process such as Gunicorn will serve the app. You
# can configure startup instructions by adding `entrypoint` to app.yaml.
#
# To control listening IP and port, set SERVER_NAME in the environment.
# e.g. SERVER_NAME=127.0.0.1:8080
# Default is to listen on 127.0.0.1:5000
app.run(debug=True)
75 changes: 38 additions & 37 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import hashlib
import os

from google.appengine.ext import db
from google.cloud import ndb
from google.appengine.api import users


Expand All @@ -21,54 +21,55 @@
# support that later.


class User(db.Model):
class User(ndb.Model):
"""User preferences."""
created = db.DateTimeProperty()
last_modified = db.DateTimeProperty(auto_now=True)
email = db.StringProperty(required=True) # The key to this record
is_hidden = db.BooleanProperty(default=False) # hide 'empty' snippets
category = db.StringProperty(default=NULL_CATEGORY) # groups snippets
uses_markdown = db.BooleanProperty(default=True) # interpret snippet text
private_snippets = db.BooleanProperty(default=False) # private by default?
wants_email = db.BooleanProperty(default=True) # get nag emails?
created = ndb.DateTimeProperty(auto_now_add=True)
last_modified = ndb.DateTimeProperty(auto_now=True)
email = ndb.StringProperty(required=True) # The key to this record
is_hidden = ndb.BooleanProperty(default=False) # hide 'empty' snippets
category = ndb.StringProperty(default=NULL_CATEGORY) # groups snippets
uses_markdown = ndb.BooleanProperty(default=True) # interpret snippet text
private_snippets = ndb.BooleanProperty(default=False) # private by default?
wants_email = ndb.BooleanProperty(default=True) # get nag emails?
# TODO(csilvers): make a ListProperty instead.
wants_to_view = db.TextProperty(default='all') # comma-separated list
display_name = db.TextProperty(default='') # display name of the user
wants_to_view = ndb.TextProperty(default='all') # comma-separated list
display_name = ndb.TextProperty(default='') # Display name of the user
slack_id = ndb.StringProperty(default='') # Slack member ID (not nickname!)


class Snippet(db.Model):
class Snippet(ndb.Model):
"""Every snippet is identified by the monday of the week it goes with."""
created = db.DateTimeProperty()
last_modified = db.DateTimeProperty(auto_now=True)
display_name = db.StringProperty() # display name of the user
email = db.StringProperty(required=True) # week+email: key to this record
week = db.DateProperty(required=True) # the monday of the week
text = db.TextProperty()
private = db.BooleanProperty(default=False) # snippet is private?
is_markdown = db.BooleanProperty(default=False) # text is markdown?
created = ndb.DateTimeProperty(auto_now_add=True)
last_modified = ndb.DateTimeProperty(auto_now=True)
display_name = ndb.StringProperty() # display name of the user
email = ndb.StringProperty(required=True) # week+email: key to this record
week = ndb.DateProperty(required=True) # the monday of the week
text = ndb.TextProperty()
private = ndb.BooleanProperty(default=False) # snippet is private?
is_markdown = ndb.BooleanProperty(default=False) # text is markdown?

@property
def email_md5_hash(self):
m = hashlib.md5()
m.update(self.email)
m.update(self.email.encode('utf-8'))
return m.hexdigest()


class AppSettings(db.Model):
class AppSettings(ndb.Model):
"""Application-wide preferences."""
created = db.DateTimeProperty()
last_modified = db.DateTimeProperty(auto_now=True)
created = ndb.DateTimeProperty(auto_now_add=True)
last_modified = ndb.DateTimeProperty(auto_now=True)
# Application settings
domains = db.StringListProperty(required=True)
hostname = db.StringProperty(required=True) # used for emails
default_private = db.BooleanProperty(default=False) # new-user default
default_markdown = db.BooleanProperty(default=True) # new-user default
default_email = db.BooleanProperty(default=True) # new-user default
domains = ndb.StringProperty(repeated=True)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you also want required=True here still.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still needs to be done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to figure out why I changed this in the first place; iirc there was some kind of bootstrapping issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohhhhh, it's because fields cannot have both repeated=True and required=True. Which option would you prefer?

  • StringProperty with required=True (as before), parsed into a list at runtime
  • (current) StringProperty with repeated=True, and we can make it effectively required in our code
  • JsonProperty with required=True and a validator function to guarantee a list of strings

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I think what you have now is good then.

The important thing to me is backward-compat. As long as db.StringListProperty is wire-compatible with ndb.StringProperty(repeated=True) -- which I think it is -- then this is a good change by me!

hostname = ndb.StringProperty(required=True) # used for emails
default_private = ndb.BooleanProperty(default=False) # new-user default
default_markdown = ndb.BooleanProperty(default=True) # new-user default
default_email = ndb.BooleanProperty(default=True) # new-user default
# Chat and email settings
email_from = db.StringProperty(default='')
slack_channel = db.StringProperty(default='')
slack_token = db.StringProperty(default='')
slack_slash_token = db.StringProperty(default='')
email_from = ndb.StringProperty(default='')
slack_channel = ndb.StringProperty(default='')
slack_token = ndb.StringProperty(default='')
slack_slash_token = ndb.StringProperty(default='')

@staticmethod
def get(create_if_missing=False, domains=None):
Expand All @@ -79,12 +80,12 @@ def get(create_if_missing=False, domains=None):
are initialized with the given value for 'domains'. The new
entity is *not* put to the datastore.
"""
retval = AppSettings.get_by_key_name('global_settings')
retval = AppSettings.get_by_id('global_settings')
if retval:
return retval
elif create_if_missing:
# We default to sending email, and having it look like it's
# comint from the current user. We add a '+snippets' in there
# coming from the current user. We add a '+snippets' in there
# to allow for filtering
email_address = users.get_current_user().email()
email_address = email_address.replace('@', '+snippets@')
Expand All @@ -93,7 +94,7 @@ def get(create_if_missing=False, domains=None):
# you accessed the site on here.
hostname = '%s://%s' % (os.environ.get('wsgi.url_scheme', 'http'),
os.environ['HTTP_HOST'])
return AppSettings(key_name='global_settings',
return AppSettings(id='global_settings',
created=datetime.datetime.now(),
domains=domains,
hostname=hostname,
Expand Down
14 changes: 14 additions & 0 deletions nix/sources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"nixpkgs": {
"branch": "nixos-22.11",
"description": "Nix Packages collection",
"homepage": null,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "eef86b8a942913a828b9ef13722835f359deef29",
"sha256": "1ig0mc7f2n1pxzd5y9m4vz38zp515vn5s576kwhkhj62zdw99p2v",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/eef86b8a942913a828b9ef13722835f359deef29.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}
Loading