From 4407c1fcedad7395a476846bc4bdb6287d131053 Mon Sep 17 00:00:00 2001 From: abasile Date: Mon, 28 Mar 2022 09:36:04 +0200 Subject: [PATCH 1/5] Adds a module for CRUD operations for the job board. --- db.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 db.py diff --git a/db.py b/db.py new file mode 100644 index 0000000..7068ddf --- /dev/null +++ b/db.py @@ -0,0 +1,124 @@ +import json +import os +from abc import ABC, abstractmethod +from dataclasses import asdict, dataclass +from typing import Optional + +import urllib3 + + +@dataclass +class JobListing: + Description: str + URL: str + Company: Optional[str] = None + Position: Optional[str] = None + Salary: Optional[str] = None + + def from_txt(text_message): + """ + EXAMPLE: + >>> txt = 'Acme | rockstar ninja | $80k-$170k;Acme is a cool company that does cool things;https://example.com' + >>> JobListing.from_txt(txt) + JobListing(Description='Acme | rockstar ninja | $80k-$170k', URL='https://example.com', Company=None, Position=None, Salary=None) + """ + new_job_description, company_description, new_job_link = text_message.split(";") + return JobListing(Description=new_job_description, URL=new_job_link) + + + +class JobBoardDbBase(ABC): + """ + An abstract class for CRUD operations + """ + + @abstractmethod + def create(): + ... + @abstractmethod + def read(): + ... + @abstractmethod + def update(): + ... + @abstractmethod + def delete(): + ... + +class JobBoardDbAirTable(JobBoardDbBase): + """ + An AirTable manager + """ + def __init__(self): + self.key = os.getenv('EMEATECH_AIRTABLE_API_KEY') + if self.key is None: + raise Exception("Define the env variable EMEA_AIRTABLE_API") + self.url = "https://api.airtable.com/v0/appTgR7p7sKXHvip2/Table%201" + + def create(self, data:JobListing): + http = urllib3.PoolManager() + + body = {"records":[{"fields": asdict(data)}]} + + # Sending a GET request and getting back response as HTTPResponse object. + resp = http.request( + "POST", + self.url, + body=json.dumps(body), + headers={ + 'Authorization': f'Bearer {self.key}', + 'Content-Type': 'application/json', + } + ) + + return json.loads(resp.data) + + def read(self): + http = urllib3.PoolManager() + + resp = http.request( + "GET", + self.url, + headers={'Authorization': f'Bearer {self.key}'} + ) + if resp.status == 200: + return resp.data + else: + raise Exception(resp.status) + + def update(): + raise NotImplementedError() + + def delete(self,row_id:str): + http = urllib3.PoolManager() + + resp = http.request( + "DELETE", + self.url, + fields={'records[]':row_id}, + headers={'Authorization': f'Bearer {self.key}'} + ) + return json.loads(resp.data) + + + +if __name__ == "__main__": + import doctest + doctest.testmod() + board = JobBoardDbAirTable() + # list all jobs + jobs = json.loads(board.read()) + for job in jobs['records']: + if job['fields']: + fields = JobListing(**job['fields']) + print(f"* **{fields.Company} | {fields.Position} | {fields.Salary}.** {fields.Description} {fields.URL}") + + # create a new job + new_job = board.create( + JobListing('Acme', 'XY', 'You should drink coffee all day.', '100kUSD', 'http://acme.com/recruiting') + ) + print(new_job) + # delete a job + deleted = board.delete(new_job['records'][0]['id']) + assert deleted['records'][0]['deleted'] == True + From 6b700fd2b7385af113eac99d859481d19daf993a Mon Sep 17 00:00:00 2001 From: abasile Date: Mon, 28 Mar 2022 09:36:17 +0200 Subject: [PATCH 2/5] Adds env vars to README. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d222924..d5299b7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # emeatech_bot Super hacky initial implementation of a Telegram bot to do tasks such as adding jobs and news to emeatech.org. + +# ENV VARIABLES + +EMEA_TELEGRAM_TOKEN = '' + +EMEATECH_AIRTABLE_API_KEY = '' From 09b16c3c1b5c3728fe04e5865114ecf8f71693bd Mon Sep 17 00:00:00 2001 From: abasile Date: Mon, 28 Mar 2022 12:09:49 +0200 Subject: [PATCH 3/5] Adds gitignore file. --- .gitignore | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe33a8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,152 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file From ed08db6325b8758a8d73abc9b50097a5714ee44c Mon Sep 17 00:00:00 2001 From: abasile Date: Mon, 28 Mar 2022 12:46:29 +0200 Subject: [PATCH 4/5] Write to AirTable from bot. --- db.py | 72 +++++++++++++++++++++++++++++--------------------- emeatechbot.py | 28 +++++++++++++------- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/db.py b/db.py index 7068ddf..26209b7 100644 --- a/db.py +++ b/db.py @@ -3,8 +3,7 @@ from abc import ABC, abstractmethod from dataclasses import asdict, dataclass from typing import Optional - -import urllib3 +from urllib import request, parse @dataclass @@ -20,10 +19,26 @@ def from_txt(text_message): EXAMPLE: >>> txt = 'Acme | rockstar ninja | $80k-$170k;Acme is a cool company that does cool things;https://example.com' >>> JobListing.from_txt(txt) - JobListing(Description='Acme | rockstar ninja | $80k-$170k', URL='https://example.com', Company=None, Position=None, Salary=None) + JobListing(Description='Acme | rockstar ninja | $80k-$170k', URL='https://example.com', Company='Acme', Position='rockstar ninja', Salary='$80k-$170k') """ new_job_description, company_description, new_job_link = text_message.split(";") - return JobListing(Description=new_job_description, URL=new_job_link) + company, position, salary = new_job_description.split(' | ') + return JobListing( + Description=new_job_description, + URL=new_job_link, + Company=company, + Position=position, + Salary=salary + ) + + def to_md(self)->str: + """ + EXAMPLE: + >>> job = JobListing(Description='Acme | rockstar ninja | $80k-$170k', URL='https://example.com', Company='Acme', Position='rockstar ninja', Salary='$80k-$180k') + >>> job.to_md() + '* **Acme | rockstar ninja | $80k-$180k.** Acme | rockstar ninja | $80k-$170k https://example.com' + """ + return f"* **{self.Company} | {self.Position} | {self.Salary}.** {self.Description} {self.URL}" @@ -56,49 +71,46 @@ def __init__(self): self.url = "https://api.airtable.com/v0/appTgR7p7sKXHvip2/Table%201" def create(self, data:JobListing): - http = urllib3.PoolManager() body = {"records":[{"fields": asdict(data)}]} - # Sending a GET request and getting back response as HTTPResponse object. - resp = http.request( - "POST", + req = request.Request( self.url, - body=json.dumps(body), + method='POST', + data=json.dumps(body).encode(), headers={ 'Authorization': f'Bearer {self.key}', 'Content-Type': 'application/json', } ) - return json.loads(resp.data) + return json.loads(request.urlopen(req).read()) + def read(self): - http = urllib3.PoolManager() - - resp = http.request( - "GET", - self.url, - headers={'Authorization': f'Bearer {self.key}'} - ) - if resp.status == 200: - return resp.data - else: - raise Exception(resp.status) + try: + req = request.Request( + self.url, + headers={'Authorization': f'Bearer {self.key}'}, + method='GET' + ) + + return request.urlopen(req).read() + except: + raise Exception(resp) def update(): raise NotImplementedError() def delete(self,row_id:str): - http = urllib3.PoolManager() - - resp = http.request( - "DELETE", - self.url, - fields={'records[]':row_id}, + body = {'records[]':row_id} + req = request.Request( + f"{self.url}/?" + parse.urlencode(body), + method='DELETE', headers={'Authorization': f'Bearer {self.key}'} ) - return json.loads(resp.data) + with request.urlopen(req) as response: + return json.loads(response.read()) @@ -111,11 +123,11 @@ def delete(self,row_id:str): for job in jobs['records']: if job['fields']: fields = JobListing(**job['fields']) - print(f"* **{fields.Company} | {fields.Position} | {fields.Salary}.** {fields.Description} {fields.URL}") + print(fields.to_md()) # create a new job new_job = board.create( - JobListing('Acme', 'XY', 'You should drink coffee all day.', '100kUSD', 'http://acme.com/recruiting') + JobListing(Description='You should drink coffee all day', Company='ACME', Position='ninja', Salary='$100k', URL='http://acme.com/recruiting') ) print(new_job) # delete a job diff --git a/emeatechbot.py b/emeatechbot.py index 3cb6c78..1d0e666 100644 --- a/emeatechbot.py +++ b/emeatechbot.py @@ -1,9 +1,19 @@ +from db import JobListing +from pathlib import Path import os -from telegram.ext import Updater, CommandHandler, MessageHandler, Filters -import os + +from db import JobBoardDbAirTable +from telegram.ext import CommandHandler, Filters, MessageHandler, Updater TOKEN = os.getenv("EMEA_TELEGRAM_TOKEN") +news_file_path = Path('news.md') + +job_file_path = Path('jobs.md') + +board = JobBoardDbAirTable() + + def start(update, context): context.bot.send_message(chat_id=update.effective_chat.id, text="Hi! I respond by echoing messages. Give it a try!") @@ -11,9 +21,9 @@ def echo(update, context): text = update.message.text if text.startswith("n "): print("updating news") - with open("/home/tasks/emeatech.org/docs/news/index.md") as f: + with job_file_path.open('r') as f: existing = f.read() - with open("/home/tasks/emeatech.org/docs/news/index.md", "w") as f: + with job_file_path.open('w') as f: link = text[2:] bullet = f"* [{link}]({link})\n" replacetext = "" @@ -25,14 +35,14 @@ def echo(update, context): elif text.startswith("j "): try: - with open("/home/tasks/emeatech.org/docs/jobs/index.md") as f: + with job_file_path.open('r') as f: existing = f.read() - with open("/home/tasks/emeatech.org/docs/jobs/index.md", "w") as f: + with job_file_path.open('w') as f: try: - new_job = text[2:] - new_job_description, company_description, new_job_link = new_job.split(";") - existing = existing.replace("## Listings", f"## Listings\n\n * **{new_job_description}.** {company_description}. More information [here]({new_job_link}).") + new_job = JobListing.from_txt(text[2:]) + existing = existing.replace("## Listings", f"## Listings\n\n * **{new_job.Description}.** {new_job.Description}. More information [here]({new_job.URL}).") f.write(existing) + board.create(new_job) context.bot.send_message(chat_id=update.effective_chat.id, text="Posted your job. Thanks!") except Exception as e: print("Exception in writing new jobs") From 79023caf87522887c8a98f2ad24b6d69854997c6 Mon Sep 17 00:00:00 2001 From: abasile Date: Mon, 28 Mar 2022 12:47:06 +0200 Subject: [PATCH 5/5] Adds requirements. --- requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d02065d --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +python-telegram-bot==13.11 \ No newline at end of file