-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add resend notification endpoint #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ed90b6
474fa58
9a9912c
b3848aa
e148896
59e57e5
ea156a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| # AGENTS.md file | ||
|
|
||
| ## Python Virtual Environment (venv) | ||
|
|
||
| The Python virtual environment for this repository is typically located in `.venv`. | ||
|
|
||
| ## Formatting | ||
|
|
||
| Always format the code before commiting using, making sure that the Python code is properly formatted using: | ||
|
|
||
| ```bash | ||
| pip install black | ||
| black . | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
|
||
| To run the custom suite of unit test for Dropbox Notifier use the following sequence of commands that will install dependencies | ||
| and run the appropriate test suite (last command). | ||
|
|
||
| Try to run the unit tests whenever making changes to the codebase, before commiting new code. | ||
|
|
||
| ```bash | ||
| pip install -r requirements.txt | ||
| pip install -r extra.txt | ||
| python setup.py test | ||
| ``` | ||
|
|
||
| ## Style Guide | ||
|
|
||
| - Always update `CHANGELOG.md` according to semantic versioning, mentioning your changes in the unreleased section. | ||
| - Write commit messages using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). | ||
| - Never bump the internal package version in `setup.py`. This is handled automatically by the release process. | ||
| - Python files use CRLF as the line ending. | ||
| - The implementation should be done in Python 2.7+ and compatible with Python 3.13. | ||
| - No type annotations should exist in the `.py` files and if the exist they should isolated in th `.pyi` files. | ||
| - The style should respect the black formatting. | ||
| - The implementation should be done in a way that is compatible with the existing codebase. | ||
| - Prefer `item not in list` over `not item in list`. | ||
| - Prefer `item == None` over `item is None`. | ||
| - The commenting style of the project is unique, try to keep commenting style consistent. | ||
| - Use Python docstrings with the `:type:`, `:args:`, `:rtype:`, `:return:`, etc. structure and with a newline after the docstring end (`"""`). | ||
|
|
||
| ## Commit Messages | ||
|
|
||
| This project follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) with the following structure: | ||
|
|
||
| ```text | ||
| <type>: <description> | ||
|
|
||
| <body> | ||
| ``` | ||
|
|
||
| ### Commit Types | ||
|
|
||
| | Type | Description | | ||
| | ---------- | ------------------------------------------------------- | | ||
| | `feat` | A new feature or functionality | | ||
| | `fix` | A bug fix | | ||
| | `docs` | Documentation only changes | | ||
| | `refactor` | Code change that neither fixes a bug nor adds a feature | | ||
| | `chore` | Maintenance tasks, dependency updates, build changes | | ||
| | `test` | Adding or updating tests | | ||
| | `version` | Version bump commits (reserved for releases) | | ||
|
|
||
| ### Guidelines | ||
|
|
||
| - Use lowercase for the type prefix. | ||
| - Use imperative mood in the description (e.g., "Add feature" not "Added feature"). | ||
| - Keep the first line under 50 characters. | ||
| - Reference issue/PR numbers when applicable using `(#123)` at the end. | ||
| - For version releases, use the format `version: X.Y.Z`. | ||
| - Add an extra newline between subject and body. | ||
| - Make the body a series of bullet points about the commit. | ||
| - Be descriptive always making use of the body of the message. | ||
|
|
||
| ### Examples | ||
|
|
||
| ```text | ||
| feat: Add user authentication with OAuth 2.0 support (#138) | ||
| fix: Resolve race condition in database connection pool | ||
| docs: Add API endpoint documentation for v2 routes | ||
| refactor: Extract validation logic into reusable utility module | ||
| chore: Update dependencies to latest stable versions | ||
| test: Add integration tests for payment processing flow | ||
| version: 1.8.0 | ||
| ``` | ||
|
|
||
| ## Pre-Commit Checklist | ||
|
|
||
| Before committing, ensure that the following operations items check: | ||
|
|
||
| - [ ] Code is formatted with `black .` | ||
| - [ ] Tests pass: `python setup.py test` | ||
| - [ ] CHANGELOG.md is updated in [Unreleased] section | ||
| - [ ] No debugging print statements or commented-out code | ||
| - [ ] CRLF line endings are preserved | ||
| - [ ] No type annotations in .py files (use .pyi if needed) | ||
|
|
||
| ## New Release | ||
|
|
||
| To create a new release follow the following steps: | ||
|
|
||
| - Make sure that both the tests pass and the code formatting are valid. | ||
| - Increment (look at `CHANGELOG.md` for semver changes) the `version` value in `setup.py`, and then the `def _version(self):` return value in `src/dropbox_notifier/main.py`. | ||
| - Move all the `CHANGELOG.md` Unreleased items that have at least one non empty item the into a new section with the new version number and date, and then create new empty sub-sections (Added, Changed and Fixed) for the Unreleased section with a single empty item. | ||
| - Create a commit with the following message `version: $VERSION_NUMBER`. | ||
| - Push the commit. | ||
| - Create a new tag with the value fo the new version number `$VERSION_NUMBER`. | ||
| - Create a new release on the GitHub repo using the Markdown from the corresponding version entry in `CHANGELOG.md` as the description of the release and the version number as the title. Do not include the title of the release (version and date) in the description. | ||
|
|
||
| ## License | ||
|
|
||
| Dropbox Notifier is licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # CLAUDE.md | ||
|
|
||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
|
|
||
| **Instructions for AI Agents can be found in the [AGENTS.md](AGENTS.md) file.** |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ | |
| import appier | ||
| import appier_extras | ||
|
|
||
| from typing import cast | ||
|
|
||
| from .root import RootController | ||
|
|
||
|
|
||
|
|
@@ -27,3 +29,120 @@ def email_test(self, owner=None): | |
| ], | ||
| ) | ||
| return dict(email=email) | ||
|
|
||
| @appier.route("/admin/resend", "GET", json=True) | ||
| @appier.ensure(token="admin", context="admin") | ||
| def resend(self, owner=None): | ||
| owner = owner or appier.get_app() | ||
| owner = cast(appier.App, owner) | ||
|
|
||
| since = self.field("since", None) | ||
| if not since: | ||
| raise appier.OperationalError(message="No since timestamp defined") | ||
|
|
||
| email = cast(str, appier.conf("NOTIFIER_EMAIL", None)) | ||
| receivers = cast(list, appier.conf("NOTIFIER_RECEIVERS", [], cast=list)) | ||
| cc = cast(list, appier.conf("NOTIFIER_CC", [], cast=list)) | ||
| bcc = cast(list, appier.conf("NOTIFIER_BCC", [], cast=list)) | ||
| reply_to = cast(list, appier.conf("NOTIFIER_REPLY_TO", [], cast=list)) | ||
| folder_path = cast(str, appier.conf("NOTIFIER_FOLDER", None)) | ||
|
|
||
| if not folder_path: | ||
| raise appier.OperationalError(message="No notifier folder defined") | ||
| if not email and not receivers: | ||
| raise appier.OperationalError(message="No email or receivers defined") | ||
|
|
||
| ( | ||
| added_entries, | ||
| added_files, | ||
| folder_path, | ||
| shared_base, | ||
| shared_query, | ||
| prefix_size, | ||
| ) = self._resend(folder_path, since=since) | ||
|
|
||
| appier_extras.admin.Base.send_email_g( | ||
| owner, | ||
| "email/updated.html.tpl", | ||
| receivers=receivers if receivers else [email], | ||
| cc=cc, | ||
| bcc=bcc, | ||
| reply_to=reply_to, | ||
| subject=owner.to_locale(f"Dropbox folder {folder_path} updated"), | ||
| attachments=added_files, | ||
| added_entries=added_entries, | ||
| removed_entries=[], | ||
| folder_path=folder_path, | ||
| folder_url=shared_base, | ||
| folder_query=shared_query, | ||
| prefix_size=prefix_size, | ||
| ) | ||
|
|
||
| return dict(since=since, resent=len(added_files)) | ||
|
|
||
| def _resend(self, folder_path, since=None): | ||
| api = self.get_api() | ||
|
|
||
| folder_meta = api.metadata_file(folder_path) | ||
| folder_path = folder_meta["path_display"] | ||
| prefix_size = len(folder_path) | ||
|
|
||
| share = api.list_shared_links(folder_path) | ||
| share_links = share.get("links", []) | ||
|
|
||
| # in case valid share links exist then we should re-use the | ||
| # best of them to create the appropriate links | ||
| if share_links: | ||
| shared = share_links[0] | ||
|
|
||
| # loops trying to find the best possible share link | ||
| # for the folder, keeping in mind that using extended sharing | ||
| # controls will allow deep shared folder | ||
| for share_link in share_links: | ||
| link_permissions = share_link.get("link_permissions", {}) | ||
| if not link_permissions.get("can_use_extended_sharing_controls", False): | ||
|
Comment on lines
+95
to
+103
|
||
| continue | ||
| shared = share_link | ||
|
|
||
| # creates a shared link to the folder so that it can be used | ||
| # for the URL creation in no shared link already exists | ||
| else: | ||
| shared = api.create_shared_link(folder_path) | ||
|
|
||
| shared_url = appier.legacy.urlparse(shared["url"]) | ||
| shared_base = f"{shared_url.scheme}://{shared_url.netloc}{shared_url.path}" | ||
| shared_query = shared_url.query | ||
|
|
||
|
Comment on lines
+105
to
+115
|
||
| contents = api.list_folder_file(folder_path, recursive=True) | ||
| entries = contents.get("entries", []) | ||
|
|
||
| # filters the entries to only include files that have been | ||
| # modified after the provided since timestamp value | ||
| added_entries = [ | ||
| entry | ||
| for entry in entries | ||
| if entry.get(".tag") == "file" | ||
| and entry.get("server_modified", "") >= since | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The filter compares Useful? React with 👍 / 👎. |
||
| and entry.get("path_display", None) | ||
| ] | ||
|
|
||
| added_files = [] | ||
|
|
||
| for added_entry in added_entries: | ||
| contents, result = api.download_file(added_entry["id"]) | ||
| content_type = appier.FileTuple.guess(result["name"]) | ||
| file_tuple = appier.FileTuple.from_data( | ||
| contents, | ||
| name=added_entry["path_display"][prefix_size:], | ||
| mime=content_type or "application/octet-stream", | ||
| ) | ||
| added_files.append(file_tuple) | ||
|
|
||
| return ( | ||
| added_entries, | ||
| added_files, | ||
| folder_path, | ||
| shared_base, | ||
| shared_query, | ||
| prefix_size, | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar in the comment: "controls will allows deep shared folder" should be "controls will allow deep shared folders".