Skip to content

Commit

Permalink
new: Gmail implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Frankbz committed Nov 10, 2024
1 parent 3bae233 commit 060276a
Show file tree
Hide file tree
Showing 15 changed files with 511 additions and 18 deletions.
54 changes: 54 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
version: 2.1

executors:
my-docker-executor:
docker:
- image: cimg/python:3.11.10

jobs:
build:
executor: my-docker-executor
steps:
- checkout

- run:
name: Get uv via curl and install it
command: |
curl -LsSf https://astral.sh/uv/install.sh | sh
- persist_to_workspace:
root: /home/circleci/
paths:
- .cargo

test:
executor: my-docker-executor
steps:
- checkout

- attach_workspace:
at: /home/circleci/

- run:
name: Set-up dependencies path
command: echo 'export PATH=$HOME/.cargo/bin:$PATH' >> $BASH_ENV

- run:
name: Set-up project dependencies
command: uv sync

- run:
name: Format python files with ruff
command: uv run ruff check . --fix

- run:
name: Run test files
command: uv run pytest

workflows:
build_and_test:
jobs:
- build
- test:
requires:
- build
18 changes: 10 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
/*.json
/email_examples
__pycache__
/maigic.db
/secrets.sh
/secret.sh
/secrets
/credentials.json
**/*.json
**/email_examples
**/__pycache__
**/maigic.db
**/secrets.sh
**/secret.sh
**/secrets
**/credentials.json
**/.pytest_cache
**/.ruff_cache
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
Collaboration doc for this initiative: https://docs.google.com/document/d/1duhM5Ufkq_doBnMvHo65S7cplmUJw4KqQvDRfZlmj7s/edit
# mAIgic

This project will organize life by tracking messages and information that need follow-ups and reminding the user to take follow-ups. It will also provide a search based on individuals.

The project aims to leverage AI to track and remind for follow up messages for systems like Slack, Trello, Whatsapp, Gmail, Google Docs comments, etc.

# Technical tools

- Programming Language: Python
- Project dependency management tool: uv
- Project linter: ruff
- Project tester: pytest
- Continuous Integration (CI) tool: circleCI

# Instructions

## Setup

- Install `uv` on your system with command with `curl -LsSf https://astral.sh/uv/install.sh | sh`.
- `cd` into the cloned repository and execute `uv sync` (This will install all the dependencies).
- Activate the environment `venv` by executing `source ./.venv/bin/activate`.

## Processes

- To check the formatting standard with ruff (linter) execute either `uv run ruff check .` or `ruff check .` from the root directory of the project.
- To test the code, execute either `uv run pytest .` or `pytest .` from the root directory of the project.

Note: One can run tools like ruff and pytest independently or can run them through `uv`.

## Commit Style Guide

- Request to `strictly adhere` to the suggested commit style guide.
- The project follows Udacity's [Commit Style Guide](https://udacity.github.io/git-styleguide/).
- Reason:
- It is simple, elegant, concise and effective.
- It does not have many rules that could create confusion but yet have just enough to keep things working smoothly through clear and concise communication.

## GitHub Workflow

- Members of same team can preferably `clone` the repository.
- Make sure to push new changes to `dev` remote branch.
- Create a `Pull Request` and the changes would be reviewed and merged to the `main` remote branch. `Review` includes code, code quality, code standards, commit style guides, and Pull Request descriptions. Consistent code standards and documentation would be aided by `ruff`.
- `main` branch serves as production branch which would accumulate new changes, features and bug-fixes from `dev` branch.
- Would appreciate if you open `issues` whenever you come across any. Issues can be bugs, proposed features, performance / code improvement proposals, etc.

## Requesting access to project and CI space

- Send your`github username` to become collaborators to the project.
- Send your `email id` used to `register with circleCI` to get access to the circleCI organization to manage CI workflows and triggers. You will receive an invitation in the provided email's inbox to join the circleCI organization.
- Currently, the `magic2` CircleCI project is attached to this project.
- Collaboration doc for this initiative: https://docs.google.com/document/d/1duhM5Ufkq_doBnMvHo65S7cplmUJw4KqQvDRfZlmj7s/edit

Note: Request to keep all `communication` in the Google Chats Project Group.

## Trello Setup

- Create a Trello account.
- Go to https://trello.com/power-ups/admin to create a new Power-up.
- Check the new power-up, click `API key` on the left side panel.
- Copy the API key and visit https://trello.com/1/authorize?expiration=1day&name=yourAppName&scope=read,write&response_type=token&key=your_api_key, replace `your_api_key` with your own api key. The expiration and the name can be changed if needed.
- Create a .env file, input your API key and token:

```
TRELLO_API_KEY=your_api_key
TRELLO_OAUTH_TOKEN=your_token
```

- Run TrelloManager.py and you will see the test result.

# License

mAIgic has a MIT-style license, as found in the [LICENSE](LICENSE) file.
4 changes: 2 additions & 2 deletions assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ def handle_reply(self, thread_ts: str, reply: str) -> str:
return result
else:
raise RuntimeError(f"item not found for {thread_ts}")
except:
LOGGER.exception("")
except Exception as error_msg:
LOGGER.exception(str(error_msg))


if __name__ == "__main__":
Expand Down
14 changes: 14 additions & 0 deletions config/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
context = 'I am Satyam Chatrola.
My personal email id is [email protected].

When I first receive an email, I want you to analyze it respond with the analysis in json as follows:

* "author"
* "time_received" in iso format
** "urgent": bool
** "important": bool
** "spam": bool
* "summary" : summary of the content
* "action": proposed next action

Then answer my follow up questions in markdown supported by slack api.'
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ dependencies = [
"aiohttp>=3.10.8",
"asyncio>=3.4.3",
"humanreadable>=0.4.0",
"pytest>=8.3.3",
"pytest-mock>=3.14.0",
]

[tool.uv]
Expand Down
1 change: 1 addition & 0 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# init file
34 changes: 34 additions & 0 deletions src/gmail/API.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import Optional, Generator
from datetime import datetime
from .client import Gmail

# Create a single instance of Gmail
_gmail = Gmail()


def query(
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
max_results: int = 10000,
) -> Generator[dict, None, None]:
"""Query Gmail messages with filters."""
return _gmail.query(start_date, end_date, max_results)


def get_message(message_id: str) -> dict:
"""Fetch a specific email message by ID."""
return _gmail.get_message(message_id)


def get_labels() -> list[dict]:
"""Fetch all labels in the user's mailbox."""
return _gmail.get_labels()


def search(query: str, max_results: int = 100) -> Generator[dict, None, None]:
"""Search emails using Gmail's query syntax."""
return _gmail.search(query, max_results)


# Export only the public functions
__all__ = ["query", "get_message", "get_labels", "search"]
105 changes: 100 additions & 5 deletions src/gmail/client.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
import os.path
from typing import ClassVar
from datetime import datetime
from typing import Optional, Generator, Any
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
import os
import logging

LOGGER = logging.getLogger(__name__)


class Gmail:
"""A class to interact with Gmail using the Gmail API.
This class provides methods to authenticate with Gmail and perform
various operations like fetching and filtering emails.
"""

SCOPES: ClassVar[list[str]] = ["https://www.googleapis.com/auth/gmail.readonly"]

def __init__(self) -> None:
"""Initialize Gmail client with authenticated credentials."""
self._client = self.authenticate()

def authenticate(self):
def authenticate(self) -> Any:
"""Authenticate with Gmail API using OAuth 2.0.
Returns:
An authenticated Gmail API service object.
Raises:
FileNotFoundError: If credentials file is not found.
ValueError: If authentication fails.
"""
creds = None
if os.path.exists("token.json"):
creds = Credentials.from_authorized_user_file("token.json", self.SCOPES)
Expand All @@ -29,17 +51,90 @@ def authenticate(self):
token.write(creds.to_json())
return build("gmail", "v1", credentials=creds, cache_discovery=False)

def query(self):
# we can add start_date, end_date to fetch commands in slack bot
def query(
self,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
max_results: int = 10000,
) -> Generator[dict, None, None]:
"""Query Gmail messages with filters.
Args:
start_date: Only return messages after this date
end_date: Only return messages before this date
max_results: Maximum number of messages to return
Yields:
Dict containing message metadata
Search operators used:
after:YYYY/MM/DD - Messages after date
before:YYYY/MM/DD - Messages before date
"""
query_parts = []

if start_date:
query_parts.append(f"after:{start_date.strftime('%Y/%m/%d')}")

if end_date:
query_parts.append(f"before:{end_date.strftime('%Y/%m/%d')}")

query_string = " AND ".join(query_parts) if query_parts else ""

results = (
self._client.users()
.messages()
.list(userId="me", labelIds=["INBOX"], maxResults=10000)
.list(userId="me", maxResults=max_results, q=query_string)
.execute()
)
messages = results.get("messages", [])

messages = results.get("messages", [])
for message in messages:
yield message

def get_message(self, message_id: str):
def get_message(self, message_id: str) -> dict:
"""Fetch a specific email message by ID.
Args:
message_id: The ID of the message to fetch.
Returns:
Dict containing the message details.
"""
return self._client.users().messages().get(userId="me", id=message_id).execute()

def get_labels(self) -> list[dict]:
"""Fetch all labels in the user's mailbox.
Returns:
List of dictionaries containing label information.
"""
results = self._client.users().labels().list(userId="me").execute()
return results.get("labels", [])

def search(
self,
query: str,
max_results: int = 100,
) -> Generator[dict, None, None]:
"""Search emails using Gmail's query syntax.
Args:
query: Gmail search query string.
max_results: Maximum number of results to return.
Yields:
Dict containing message information.
"""
results = (
self._client.users()
.messages()
.list(userId="me", maxResults=max_results, q=query)
.execute()
)

messages = results.get("messages", [])
for message in messages:
yield message
2 changes: 1 addition & 1 deletion src/gmail/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def body(self) -> str:
if "<html" in decoded:
decoded = html2text(decoded)
return decoded
except Exception as e:
except Exception:
pprint(self._part["body"])
raise
return ""
Expand Down
3 changes: 2 additions & 1 deletion src/slack/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@

class Slack:
def __init__(self) -> None:
client = WebClient(token=os.getenv("SLACK_BOT_TOKEN"))
# replaced client with self.client as client was not used anywhere
self.client = WebClient(token=os.getenv("SLACK_BOT_TOKEN"))

def send(self, thread: str | None, text: str) -> None:
r = self.client.chat_postMessage(
Expand Down
1 change: 1 addition & 0 deletions test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Loading

0 comments on commit 060276a

Please sign in to comment.