Skip to content

Commit 56bcd63

Browse files
committedJun 6, 2020
init
0 parents  commit 56bcd63

18 files changed

+706
-0
lines changed
 

‎.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.secret/

‎README.rst

Whitespace-only changes.

‎announce/__init__.py

Whitespace-only changes.

‎announce/__main__.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import os
2+
import json
3+
import sys
4+
from textwrap import dedent
5+
from shutil import copyfile
6+
import datetime
7+
import calendar
8+
from pathlib import Path
9+
import argparse
10+
from announce.core import announce, new_event
11+
12+
parser = argparse.ArgumentParser()
13+
parser.add_argument("--event", default=None, help="What event are we talking about?")
14+
parser.add_argument(
15+
"--announce", default=False, action="store_true", help="Should we announce?"
16+
)
17+
parser.add_argument("--secret", default=".secret", help="Where to cache credentials?")
18+
parser.add_argument(
19+
"--new", default=False, action="store_true", help="Create a new event"
20+
)
21+
22+
args = parser.parse_args()
23+
if not os.path.exists(args.secret):
24+
os.mkdir(args.secret)
25+
if args.announce:
26+
if args.event is None:
27+
print("Please specify --event")
28+
sys.exit(0)
29+
announce(Path(args.event), Path(args.secret))
30+
elif args.new:
31+
new_event()

‎announce/auth.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import os
2+
import pickle
3+
import requests
4+
from requests_oauthlib import OAuth1Session
5+
from google.auth.transport.requests import Request
6+
from google_auth_oauthlib.flow import InstalledAppFlow
7+
from googleapiclient.discovery import build
8+
from urllib.parse import urlparse, parse_qs, urlencode
9+
from announce import const
10+
11+
12+
def refresh_linkedin(sessions, path):
13+
API_KEY = os.environ.get("LINKEDIN_CLIENT_ID")
14+
API_SECRET = os.environ.get("LINKEDIN_CLIENT_SECRET")
15+
RETURN_URL = "http://localhost:9126/code"
16+
session = sessions.get("linkedin")
17+
headers = {"Authorization": f"Bearer {session.get('access_token')}"}
18+
r = requests.get("https://api.linkedin.com/v2/me", headers=headers)
19+
if r.json().get("id"):
20+
return
21+
22+
url = "https://www.linkedin.com/oauth/v2/authorization/?" + urlencode(
23+
{
24+
"response_type": "code",
25+
"client_id": API_KEY,
26+
"redirect_uri": RETURN_URL,
27+
"scope": "w_member_social r_liteprofile",
28+
}
29+
)
30+
print("Visit this url:", url)
31+
q = input("Enter redirected url: ")
32+
code = parse_qs(urlparse(q).query).get("code")[0]
33+
r = requests.get(
34+
"https://www.linkedin.com/oauth/v2/accessToken",
35+
params={
36+
"grant_type": "authorization_code",
37+
"code": code,
38+
"redirect_uri": RETURN_URL,
39+
"client_id": API_KEY,
40+
"client_secret": API_SECRET,
41+
},
42+
)
43+
access_token = r.json().get("access_token")
44+
sessions["linkedin"] = {"access_token": access_token, "code": code}
45+
46+
47+
def refresh_google(sessions, path):
48+
SCOPES = ["https://www.googleapis.com/auth/calendar"]
49+
creds = None
50+
if os.path.exists(path / "googletoken.pickle"):
51+
with open(path / "googletoken.pickle", "rb") as token:
52+
creds = pickle.load(token)
53+
# If there are no (valid) credentials available, let the user log in.
54+
if not creds or not creds.valid:
55+
if creds and creds.expired and creds.refresh_token:
56+
creds.refresh(Request())
57+
else:
58+
flow = InstalledAppFlow.from_client_secrets_file(
59+
path / "credentials.json", SCOPES
60+
)
61+
creds = flow.run_local_server(port=0)
62+
# Save the credentials for the next run
63+
with open(path / "googletoken.pickle", "wb") as token:
64+
pickle.dump(creds, token)
65+
service = build("calendar", "v3", credentials=creds)
66+
sessions["google"] = service
67+
68+
69+
def refresh_twitter(sessions, path):
70+
request_token_url = "https://api.twitter.com/oauth/request_token"
71+
access_token_url = "https://api.twitter.com/oauth/access_token"
72+
authorize_url = "https://api.twitter.com/oauth/authorize"
73+
base_url = const.tw
74+
session = sessions.get("twitter")
75+
try:
76+
params = {"include_rts": 1, "count": 10}
77+
r = session.get(f"{base_url}/statuses/home_timeline.json", params=params)
78+
print("Found cached twitter credentials")
79+
except Exception:
80+
print("Refreshing twitter auth")
81+
client_key = os.environ.get("TWITTER_CONSUMER_KEY")
82+
client_secret = os.environ.get("TWITTER_CONSUMER_SECRET")
83+
oauth = OAuth1Session(client_key=client_key, client_secret=client_secret)
84+
fetch_response = oauth.fetch_request_token(request_token_url)
85+
resource_owner_key = fetch_response.get("oauth_token")
86+
resource_owner_secret = fetch_response.get("oauth_token_secret")
87+
authorization_url = oauth.authorization_url(authorize_url)
88+
print("Please visit this url: %s", authorization_url)
89+
verifier = input("Enter pin: ")
90+
oauth = OAuth1Session(
91+
client_key,
92+
client_secret=client_secret,
93+
resource_owner_key=resource_owner_key,
94+
resource_owner_secret=resource_owner_secret,
95+
verifier=verifier,
96+
)
97+
oauth_tokens = oauth.fetch_access_token(access_token_url)
98+
session = oauth
99+
params = {"include_rts": 1, "count": 10}
100+
r = session.get(f"{base_url}/statuses/home_timeline.json", params=params)
101+
102+
sessions["twitter"] = session
103+
104+
105+
def get_sessions(path):
106+
sessions = {}
107+
if os.path.exists(path / "sessions.pickle"):
108+
with open(path / "sessions.pickle", "rb") as fl:
109+
sessions = pickle.load(fl)
110+
# TODO(thesage21): linkedin app permissions need approval
111+
refresh_linkedin(sessions, path)
112+
# refresh_twitter(sessions, path)
113+
# refresh_google(sessions, path)
114+
# ----------------------------
115+
with open(path / "sessions.pickle", "wb") as fl:
116+
pickle.dump(sessions, fl)
117+
return sessions

‎announce/const.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from collections import namedtuple
2+
3+
email = "pyjaipur.india@gmail.com"
4+
tw = "https://api.twitter.com/1.1"
5+
tw_upload = "https://upload.twitter.com/1.1"
6+
format = "D MMMM YYYY HH:mm:ss Z"
7+
Event = namedtuple(
8+
"Event",
9+
"title start end short description poster add_to_cal call tweet_id email_sent linkedin_id",
10+
)
11+
12+
mailing_list_email = "pyjaipur@python.org"
13+
linkedin_org_id = 14380746

‎announce/core.py

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import os
2+
import calendar
3+
from textwrap import dedent
4+
import pendulum
5+
import json
6+
from functools import lru_cache
7+
from pathlib import Path
8+
from announce.auth import get_sessions
9+
from announce.platforms import twitter, google, website, mailinglist, linkedin
10+
from announce import const
11+
12+
13+
@lru_cache()
14+
def get_event(event_path):
15+
with open(event_path / "meta.json", "r") as fl:
16+
meta = json.loads(fl.read())
17+
with open(event_path / "text.txt", "r") as fl:
18+
text = fl.read()
19+
poster = None
20+
if os.path.exists(event_path / "poster.png"):
21+
poster = open(event_path / "poster.png", "rb")
22+
elif os.path.exists(event_path / "poster.jpeg"):
23+
poster = open(event_path / "poster.jpeg", "rb")
24+
ev = const.Event(
25+
meta["title"],
26+
pendulum.parse(meta["start"]),
27+
pendulum.parse(meta["end"]),
28+
meta["short"],
29+
text,
30+
poster,
31+
meta.get("add_to_cal"),
32+
meta.get("call"),
33+
meta.get("tweet_id"),
34+
meta.get("email_sent"),
35+
meta.get("linkedin_id"),
36+
)
37+
return ev
38+
39+
40+
def update_event(event_path, event):
41+
with open(event_path / "meta.json", "w") as fl:
42+
ev = dict(event._asdict())
43+
ev["start"] = ev["start"].to_iso8601_string()
44+
ev["end"] = ev["end"].to_iso8601_string()
45+
ev.pop("poster")
46+
fl.write(json.dumps(ev, indent=2))
47+
with open(event_path / "text.txt", "w") as fl:
48+
fl.write(event.description)
49+
50+
51+
def new_event():
52+
this_year = str(pendulum.now().year)
53+
this_month = str(pendulum.now().month)
54+
year = input(f"Year ({this_year}):")
55+
year = year.strip() if year.strip() else this_year
56+
month = input(f"Month ({this_month}):")
57+
month = month.strip() if month.strip() else this_month
58+
date = input("Date: ").strip()
59+
os.makedirs(Path(".") / "events" / year / month / date, exist_ok=True)
60+
# ------------------
61+
default_title = f"{calendar.month_name[int(month)]} meetup"
62+
title = input(f"Event title ({default_title}):")
63+
title = title if title else default_title
64+
default_start = "11:00:00"
65+
start = input(f"Start time ({default_start}):")
66+
start = start if start else default_start
67+
start = pendulum.datetime(
68+
int(year),
69+
int(month),
70+
int(date),
71+
int(start.split(":")[0]),
72+
int(start.split(":")[1]),
73+
int(start.split(":")[2]),
74+
tz="Asia/Kolkata",
75+
)
76+
default_end = "12:00:00"
77+
end = input(f"End time ({default_end}):")
78+
end = end if end else default_end
79+
end = pendulum.datetime(
80+
int(year),
81+
int(month),
82+
int(date),
83+
int(end.split(":")[0]),
84+
int(end.split(":")[1]),
85+
int(end.split(":")[2]),
86+
tz="Asia/Kolkata",
87+
)
88+
default_short = "TBD"
89+
short = input(f"Short description ({default_short}):")
90+
short = short if short else default_short
91+
root = Path(".") / "events" / year / month / date
92+
with open(root / "meta.json", "w") as fl:
93+
fl.write(
94+
json.dumps(
95+
{
96+
"title": title,
97+
"start": start.to_iso8601_string(),
98+
"short": short,
99+
"end": end.to_iso8601_string(),
100+
}
101+
)
102+
)
103+
with open(root / "text.txt", "w") as fl:
104+
fl.write(
105+
dedent(
106+
"""\
107+
Please use pyjaipur.org/#call to join the call.
108+
#pyjaipur #event
109+
"""
110+
)
111+
)
112+
print(f"Created {root/'text.txt'} for description. Please fill it up")
113+
poster = input(f"Copy monthly meetup poster as poster for this event? (Y/n)")
114+
if poster.strip().lower() in ("", "y"):
115+
os.symlink(
116+
Path(".")
117+
/ "website"
118+
/ "src"
119+
/ "images"
120+
/ "assets"
121+
/ "monthly_meetup_thumbnail.jpeg",
122+
root / "poster.jpeg",
123+
)
124+
print("Created symlink to monthly meetup poster for this event")
125+
print("To announce this event please use:")
126+
print(f" python -m announce --event events/{year}/{month}/{date} --announce")
127+
128+
129+
def announce(event_path, session_cache_path):
130+
"""
131+
Provide a path to the event folder. This is usually some path like `events/2020/5/21`.
132+
Session cache path is where the session cache is stored.
133+
"""
134+
event = get_event(event_path)
135+
sessions = get_sessions(session_cache_path)
136+
# TODO(thesage21): linkedin app permissions need approval
137+
r = linkedin.run(sessions.get("linkedin"), event)
138+
# event = google.run(sessions["google"], event)
139+
# event = twitter.run(sessions["twitter"], event)
140+
# event = mailinglist.run(sessions.get("mailinglist"), event)
141+
# event = website.run(sessions.get("website"), event)
142+
update_event(event_path, event)

‎announce/models.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from sqlalchemy import (
2+
create_engine,
3+
Column,
4+
Integer,
5+
String,
6+
DateTime,
7+
ForeignKey,
8+
)
9+
from sqlalchemy.dialects.postgresql import JSON
10+
from sqlalchemy.ext.declarative import declarative_base
11+
from sqlalchemy.orm import sessionmaker
12+
from sqlalchemy.ext.hybrid import hybrid_property
13+
import bottle_tools as bt
14+
15+
engine = create_engine(database_url)
16+
Base = declarative_base()
17+
18+
19+
class Image(Base):
20+
__tablename__ = "image"
21+
id = Column(Integer, primary_key=True)
22+
path = Column(String)
23+
24+
25+
class Cred(Base):
26+
__tablename__ = "creds"
27+
id = Column(Integer, primary_key=True)
28+
name = Column(String)
29+
var = Column(String)
30+
31+
32+
class Event(Base):
33+
__tablename__ = "event"
34+
id = Column(Integer, primary_key=True)
35+
title = Column(String)
36+
start = Column(DateTime)
37+
end = Column(DateTime)
38+
description = Column(String)
39+
image_id = Column(Integer, ForeignKey("image.id"))
40+
actions_done = Column(JSON)
41+
42+
def asdict(self):
43+
return dict(
44+
eventid=self.id,
45+
title=self.title,
46+
end=self.start.to_iso8601_string(),
47+
end=self.end.to_iso8601_string(),
48+
description=self.description,
49+
imageid=self.image_id,
50+
actions_done=self.actions_done,
51+
)
52+
53+
54+
Base.metadata.create_all(engine)
55+
Session = sessionmaker(engine)
56+
bt.common_kwargs.update({"Event": Event, "Image": Image})

‎announce/platforms/google.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import string
2+
import json
3+
import random
4+
import pendulum
5+
import pickle
6+
import os.path
7+
from urllib.parse import urlparse, parse_qs, urlencode
8+
from announce import const
9+
10+
11+
def run(session, event):
12+
if event.add_to_cal is not None:
13+
return event
14+
service = session
15+
body = {
16+
"summary": event.title,
17+
"description": event.short,
18+
"visibility": "public",
19+
"start": {
20+
"dateTime": event.start.to_iso8601_string(),
21+
"timeZone": "Asia/Kolkata",
22+
},
23+
"end": {"dateTime": event.end.to_iso8601_string(), "timeZone": "Asia/Kolkata",},
24+
}
25+
26+
calevent = service.events().insert(calendarId="primary", body=body).execute()
27+
link = calevent.get("htmlLink")
28+
add_to_cal = "https://calendar.google.com/event?" + urlencode(
29+
{
30+
"action": "TEMPLATE",
31+
"tmeid": parse_qs(urlparse(link).query)["eid"][0],
32+
"tmsrc": const.email,
33+
"scp": "ALL",
34+
}
35+
)
36+
conf = (
37+
service.events()
38+
.patch(
39+
calendarId="primary",
40+
eventId=calevent.get("id"),
41+
body={
42+
"conferenceData": {
43+
"createRequest": {
44+
"requestId": f"pyj-{''.join(random.sample(string.ascii_lowercase, 10))}"
45+
}
46+
}
47+
},
48+
sendNotifications=True,
49+
conferenceDataVersion=1,
50+
)
51+
.execute()
52+
)
53+
call_link = conf.get("hangoutLink")
54+
return const.Event(
55+
**{**event._asdict(), "add_to_cal": add_to_cal, "call": call_link}
56+
)

‎announce/platforms/linkedin.py

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import requests
2+
from announce import const
3+
4+
5+
def run(session, event):
6+
headers = {"Authorization": f"Bearer {session.get('access_token')}"}
7+
# r = requests.get(
8+
# f"https://api.linkedin.com/v2/organizations/{const.linkedin_org_id}",
9+
# headers=headers,
10+
# )
11+
# This must not be done. we need an org post.
12+
# r = requests.get("https://api.linkedin.com/v2/me", headers=headers)
13+
# uid = r.json()["id"]
14+
uid = const.linkedin_org_id
15+
data = {
16+
"author": f"urn:li:organization:{uid}",
17+
"lifecycleState": "PUBLISHED",
18+
"specificContent": {
19+
"com.linkedin.ugc.ShareContent": {
20+
"shareCommentary": {"text": event.description},
21+
"shareMediaCategory": "NONE",
22+
}
23+
},
24+
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
25+
}
26+
if event.poster:
27+
r = requests.post(
28+
"https://api.linkedin.com/v2/assets?action=registerUpload",
29+
json={
30+
"registerUploadRequest": {
31+
"recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
32+
"owner": f"urn:li:organization:{uid}",
33+
"serviceRelationships": [
34+
{
35+
"relationshipType": "OWNER",
36+
"identifier": "urn:li:userGeneratedContent",
37+
}
38+
],
39+
}
40+
},
41+
headers=headers,
42+
)
43+
j = r.json()
44+
asset = j["value"]["asset"]
45+
url = j["value"]["uploadMechanism"]
46+
url = url["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"][
47+
"uploadUrl"
48+
]
49+
event.poster.seek(0)
50+
h = {
51+
"Accept": "*/*",
52+
**headers,
53+
}
54+
r = requests.put(url, data=event.poster.read(), headers=h)
55+
data["specificContent"]["com.linkedin.ugc.ShareContent"][
56+
"shareMediaCategory"
57+
] = "IMAGE"
58+
data["specificContent"]["com.linkedin.ugc.ShareContent"]["media"] = [
59+
{"status": "READY", "media": asset, "title": {"text": event.title},}
60+
]
61+
r = requests.post(
62+
"https://api.linkedin.com/v2/ugcPosts", json=data, headers=headers,
63+
)
64+
print(r.json())
65+
return const.Event(**{**event._asdict(), "linkedin_id": r.json()["id"]})

‎announce/platforms/mailinglist.py

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import os
2+
import smtplib
3+
from textwrap import dedent
4+
import json
5+
from announce import const
6+
7+
8+
def send(*, to, subject, message):
9+
server = smtplib.SMTP("smtp.gmail.com", 587)
10+
server.ehlo()
11+
server.starttls()
12+
server.login(const.email, os.environ.get("GMAIL_PWD"))
13+
14+
body = "\r\n".join(
15+
[
16+
"To: %s" % to,
17+
"From: PyJaipur <%s>" % const.email,
18+
"Subject: %s" % subject,
19+
"",
20+
message,
21+
]
22+
)
23+
try:
24+
server.sendmail(const.email, [to], body)
25+
except Exception as e:
26+
log.exception(e)
27+
server.quit()
28+
29+
30+
def run(session, event):
31+
message = f"Hello,\n\n{event.description}\n\nThanks,\nPyJaipur"
32+
send(to=const.mailing_list_email, subject=event.title, message=message)
33+
return const.Event(**{**(event._asdict()), "email_sent": True})

‎announce/platforms/meetup.py

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
def run(session, event):
2+
return event

‎announce/platforms/twitter.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from announce import const
2+
3+
4+
def run(session, event):
5+
if event.tweet_id is not None:
6+
return event
7+
data = {
8+
"status": f"{event.short}\n\nUse pyjaipur.org/#call to join in.🙂",
9+
"enable_dmcommands": True,
10+
}
11+
if event.poster is not None:
12+
r = session.post(
13+
f"{const.tw_upload}/media/upload.json", files={"media": event.poster}
14+
)
15+
if r.status_code == 200:
16+
mid = r.json().get("media_id_string")
17+
data["media_ids"] = mid
18+
tweet = session.post(f"{const.tw}/statuses/update.json", data=data)
19+
return const.Event(**{**event._asdict(), "tweet_id": tweet.json()["id"]})

‎announce/platforms/website.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
def run(session, event):
2+
with open("website/src/.events.html", "r") as fl:
3+
html = fl.read()
4+
start = event.start.format("DD MMMM YYYY HH:mm:ss") + " GMT+5:30"
5+
if start in html:
6+
return event
7+
mark = "<!-- announce-new-event-after-this -->"
8+
idx = html.find(mark) + len(mark)
9+
ev_html = f"""
10+
11+
<div class='alert'>
12+
<strong>{event.title}</strong><br>
13+
<time>{start}</time><br>
14+
<a class='badge' target="_blank" href="{event.add_to_cal}">Add to calendar</a><br>
15+
<a class='badge' href='{event.call}'>Call</a>
16+
<hr>
17+
{event.short}
18+
</div>
19+
20+
"""
21+
with open("website/src/.events.html", "w") as fl:
22+
fl.write(html[:idx] + ev_html + html[idx:])
23+
return event

‎announce/server.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import bottle
2+
from bottle_tools import fill_args
3+
from importlib import import_module
4+
from announce.models import Session
5+
6+
7+
class AutoSession:
8+
name = "auto_session"
9+
api = 2
10+
11+
def apply(self, callback, route):
12+
@wraps(callback)
13+
def wrapper(*a, **kw):
14+
bottle.request.session = Session()
15+
try:
16+
return callback(*a, **kw)
17+
finally:
18+
bottle.request.session.close()
19+
20+
return wrapper
21+
22+
def setup(self, app):
23+
self.app = app
24+
25+
26+
app = bottle.Bottle()
27+
app.install(AutoSession())
28+
29+
30+
@app.post("/event")
31+
def event_view(Event):
32+
ev = Event()
33+
bottle.request.session.add(ev)
34+
bottle.request.session.commit()
35+
return {"eventid": ev.id}
36+
37+
38+
@app.get("/event")
39+
@fill_args
40+
def event_view(eventid: str, Event):
41+
ev = bottle.request.session.query(Event).filter_by(id=eventid)
42+
return {"eventid": ev.id}
43+
44+
45+
@app.post("/image")
46+
def image_view(Image):
47+
im = Image()
48+
# save and generate image path
49+
im.path = "."
50+
bottle.request.session.add(im)
51+
bottle.request.session.commit()
52+
return {"eventid": ev.id}
53+
54+
55+
@app.get("/creds")
56+
def see_creds(Cred):
57+
return {}
58+
59+
60+
@app.post("/creds")
61+
def update_creds(Cred):
62+
pass
63+
64+
65+
@app.get("/action")
66+
@fill_args
67+
def action_list(eventid: str = None):
68+
actions = [
69+
"twitter.tweet",
70+
"linkedin.event",
71+
"google.event",
72+
"meetup.event",
73+
"telegram.announce",
74+
"website.card",
75+
"mailinglist.email",
76+
]
77+
if eventid is not None:
78+
event = bottle.request.session.query(Event).filter_by(id=eventid)
79+
actions = [ac for ac in actions if not event.actions_done.get(action, False)]
80+
81+
return {"available": actions}
82+
83+
84+
@app.post("/action")
85+
@fill_args
86+
def action_view(
87+
eventid: str,
88+
imageid: str,
89+
title: str,
90+
start: str,
91+
end: str,
92+
description: str,
93+
action: str,
94+
Event,
95+
Image,
96+
):
97+
key = f"announce.platforms.{action}"
98+
event = bottle.request.session.query(Event).filter_by(id=eventid)
99+
if event.actions_done.get(key, False):
100+
return {"reason": "Action already done."}
101+
im = (
102+
bottle.request.session.query(Image).filter_by(id=imageid)
103+
if imageid is not None
104+
else None
105+
)
106+
fn = import_module(key)
107+
fn(
108+
event=event,
109+
title=title,
110+
start=start,
111+
end=end,
112+
description=description,
113+
image=im,
114+
)
115+
ev.actions_done[key] = True
116+
bottle.request.session.commit()
117+
return {}

‎pyproject.toml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[tool.poetry]
2+
name = "announce"
3+
version = "0.1.0"
4+
description = "Code used to announce pyjaipur events"
5+
authors = ["arjoonn sharma <arjoonn.94@gmail.com>"]
6+
7+
[tool.poetry.dependencies]
8+
python = "^3.6"
9+
requests = "^2.23.0"
10+
requests-oauthlib = "^1.3.0"
11+
google-api-python-client = "^1.8.4"
12+
google-auth-httplib2 = "^0.0.3"
13+
google-auth-oauthlib = "^0.4.1"
14+
pendulum = "^2.1.0"
15+
oauth2client = "^4.1.3"
16+
Jinja2 = "^2.11.2"
17+
bottle = "^0.12.18"
18+
bottle-tools = "^2019.12.22"
19+
20+
[tool.poetry.dev-dependencies]
21+
pytest = "^5.2"
22+
black = "^19.10b0"
23+
24+
[build-system]
25+
requires = ["poetry>=0.12"]
26+
build-backend = "poetry.masonry.api"

‎tests/__init__.py

Whitespace-only changes.

‎tests/test_announce.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from announce import __version__
2+
3+
4+
def test_version():
5+
assert __version__ == '0.1.0'

0 commit comments

Comments
 (0)
Please sign in to comment.