Skip to content

Commit

Permalink
#59: Give ability to override worksection project hash
Browse files Browse the repository at this point in the history
  • Loading branch information
dgroup committed Jan 15, 2022
1 parent 4f33863 commit 076ab31
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 19 deletions.
8 changes: 5 additions & 3 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ jobs:
env:
WS_URL_ALL_USERS: ${{ secrets.WS_URL_ALL_USERS }}
WS_URL_POST_COMMENT: ${{ secrets.WS_URL_POST_COMMENT }}
WS_EMAIL: ${{ secrets.WS_EMAIL }}
WS_ADMIN_EMAIL: ${{ secrets.WS_EMAIL }}
WS_ADMIN_USER_ID: ${{ secrets.WS_ADMIN_USER_ID }}
WS_PRJ_223728_HASH: ${{ secrets.WS_PRJ_223728_HASH }}
WS_PRJ_1010_HASH: ${{ secrets.WS_PRJ_1010_HASH }}
- name: "Upload coverage to Codecov"
uses: codecov/[email protected]
# with:
Expand All @@ -85,7 +87,7 @@ jobs:
env:
WS_URL_ALL_USERS: ${{ secrets.WS_URL_ALL_USERS }}
WS_URL_POST_COMMENT: ${{ secrets.WS_URL_POST_COMMENT }}
WS_EMAIL: ${{ secrets.WS_EMAIL }}
WS_ADMIN_EMAIL: ${{ secrets.WS_EMAIL }}
WS_ADMIN_USER_ID: ${{ secrets.WS_ADMIN_USER_ID }}
WS_INT_TESTS_DISABLED: true

Expand All @@ -111,7 +113,7 @@ jobs:
env:
WS_URL_ALL_USERS: ${{ secrets.WS_URL_ALL_USERS }}
WS_URL_POST_COMMENT: ${{ secrets.WS_URL_POST_COMMENT }}
WS_EMAIL: ${{ secrets.WS_EMAIL }}
WS_ADMIN_EMAIL: ${{ secrets.WS_EMAIL }}
WS_ADMIN_USER_ID: ${{ secrets.WS_ADMIN_USER_ID }}
WS_INT_TESTS_DISABLED: true

Expand Down
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ services:
environment:
WS_URL_ALL_USERS: "https://xxx.worksection.com/xxxx"
WS_URL_POST_COMMENT: "https://xxx.worksection.com/xxxx"
WS_EMAIL: "[email protected]"
WS_ADMIN_EMAIL: "[email protected]"
WS_ADMIN_USER_ID: "370080"
WS_PRJ_223728_HASH: "xxx"
build:
dockerfile: Containerfile
context: .
entrypoint: g2w --log=DEBUG
ports:
- "8080:8080"
restart: always
3 changes: 2 additions & 1 deletion g2w/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .push import Push
from .ws import Ws
from .api import LoggableRoute

__all__ = ["Push", "Ws"]
__all__ = ["Push", "Ws", "LoggableRoute"]
9 changes: 7 additions & 2 deletions g2w/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@

import uvicorn # pragma: no cover
from fastapi import FastAPI
from fastapi.routing import APIRouter

from g2w import Push, Ws
from g2w import Push, Ws, LoggableRoute


# @todo #/DEV Add support of command line parser for program arguments
Expand All @@ -30,13 +31,15 @@ def main() -> None: # pragma: no cover

ws = Ws()
app = FastAPI()
router = APIRouter(route_class=LoggableRoute)


# @todo #/DEV add logging framework and remove `print` statement everywhere


@app.post("/gitlab/push/{project_id}")
@router.post("/gitlab/push/{project_id}")
def push(event: Push, project_id: int) -> dict:

author = ws.find_user(event.user_email)
msg = event.comment(author)
comments = []
Expand All @@ -46,5 +49,7 @@ def push(event: Push, project_id: int) -> dict:
return {"comments": comments}


app.include_router(router)

if __name__ == "__main__": # pragma: no cover
main()
35 changes: 35 additions & 0 deletions g2w/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import logging
import time
from typing import Callable

from fastapi import Request, Response
from fastapi.routing import APIRoute

# @todo #/DEV Extract timings to a separate route.
"""
Interceptor that logs all for incoming requests/responses.
"""


class LoggableRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()

async def custom_route_handler(req: Request) -> Response:
before = time.time()
resp: Response = await original_route_handler(req)
duration = time.time() - before
resp.headers["X-Response-Time"] = str(duration)
# @todo #58/DEV Ensure that logging is enabled for HTTP traffic and
# could be used.
logging.debug("req duration: {0}", duration)
logging.debug(
"req: %s, duration: %s, resp: %s, resp. headers: %s",
req,
duration,
resp,
resp.headers,
)
return resp

return custom_route_handler
61 changes: 53 additions & 8 deletions g2w/ws.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
import logging # pragma: no cover
import os # pragma: no cover
from typing import List

import requests # pragma: no cover

"""
Worksection client that allows manipulation with
"""

def get_hash(prj) -> str:
"""
Fetch project hash from environment variable.
The naming format is 'WS_PRJ_0000_HASH', where '0000' is project id:
WS_PRJ_0001_HASH: 12fjasdfsdfhk34hsdf
WS_PRJ_0002_HASH: asf324i324jdfi23hfd
...
"""
val = os.getenv(f"WS_PRJ_{prj}_HASH")
if val is None:
logging.error("g2w-003: No hash found for project id '%'", prj)
raise ValueError("g2w-003: No hash found for project id '%'", prj)
return val


def post(req) -> dict:
"""
Send POST request to Worksection API.
"""
resp = requests.post(req).json()
# @todo #58/DEV Ensure that logging is enabled for this method as well.
logging.debug("WS req: '%s', resp: '%s'", req, resp)
return resp


class Ws:
"""
Worksection client that allows manipulation with
"""
def __init__(
self,
email=os.getenv("WS_EMAIL"),
email=os.getenv("WS_ADMIN_EMAIL"),
system_user_id=os.getenv("WS_ADMIN_USER_ID"),
all_users=os.getenv("WS_URL_ALL_USERS"),
post_comment=os.getenv("WS_URL_POST_COMMENT"),
Expand All @@ -24,17 +49,37 @@ def __init__(
users: List[dict] = []

def all_users(self) -> List[dict]:
"""
Fetch all users from worksection space.
"""
if not self.users:
# @todo #/DEV use memorize feature/approach instead of own caching.
self.users.extend(requests.get(self.url_all_users).json()["data"])
return self.users

def add_comment(self, prj: int, task_id: int, body: str) -> dict:
return requests.post(
self.url_post_comment.format(prj, task_id, self.system_email, body)
).json()["data"]
def add_comment(self, prj: int, task: int, body: str) -> dict:
"""
Add a comment to a particular worksection task id.
"""
resp = post(self.post_comment_url(prj, task, body))
if resp["status"] == "ok":
return resp["data"]
else:
return resp

def post_comment_url(self, prj, task, body) -> str:
"""
Construct URL for posting comments.
"""
return self.url_post_comment.format(
prj, task, self.system_email, body, get_hash(prj)
)

def find_user(self, email: str) -> dict:
"""
Find user details in Worksection by email.
Return user or system account (if not found).
"""
user = next((u for u in self.all_users() if u["email"] == email), None)
if user is not None:
return user
Expand Down
8 changes: 6 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ Please note, that `GitLab` commit messages must have the following format: `#WS-
version: "3.9"
services:
g2ws:
image: dgroup/g2w:0.1.0
image: dgroup/g2w:0.2.0
container_name: g2w
environment:
# Mandatory environment variables (docker, podman, etc.)
WS_URL_ALL_USERS: "https://xxx.worksection.com/xxxx" # https://worksection.com/faq/api-user.html#q1572
WS_URL_POST_COMMENT: "https://xxx.worksection.com/xxxx" # https://worksection.com/faq/api-comments.html#q1575
WS_EMAIL: "[email protected]" # plain worksection user email
WS_ADMIN_EMAIL: "[email protected]" # plain worksection user email
WS_ADMIN_USER_ID: "370080" # plain worksection user id
WS_PRJ_223728_HASH: "xxx" # HASH generated for a particular Worksection project
build:
dockerfile: Containerfile
context: .
Expand Down Expand Up @@ -162,3 +163,6 @@ curl --request POST \
```bash
pip install g2w
```

## Materials & Links
* http://www.md5.cz - generate WS hash
2 changes: 1 addition & 1 deletion tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# - https://setuptools.pypa.io/en/latest/pkg_resources.html


@pytest.mark.skipif(os.getenv("WS_EMAIL") is None, reason="Email account for worksection is absent")
@pytest.mark.skipif(os.getenv("WS_ADMIN_EMAIL") is None, reason="Environment variable 'WS_ADMIN_EMAIL' is absent")
@pytest.mark.skipif(os.getenv("WS_URL_ALL_USERS") is None, reason="Environment variable 'WS_URL_ALL_USERS' is absent")
@pytest.mark.skipif(os.getenv("WS_URL_POST_COMMENT") is None, reason="Environment variable 'WS_URL_POST_COMMENT' is absent")
@pytest.mark.skipif(os.getenv("WS_INT_TESTS_DISABLED") is not None, reason="Integration tests are disabled")
Expand Down
12 changes: 11 additions & 1 deletion tests/test_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def test_users():


@pytest.mark.skipif(os.getenv("WS_INT_TESTS_DISABLED") is not None, reason="Integration tests are disabled")
@pytest.mark.skipif(os.getenv("WS_EMAIL") is None, reason="Email account for worksection is absent")
@pytest.mark.skipif(os.getenv("WS_ADMIN_EMAIL") is None, reason="WS_ADMIN_EMAIL variable 'WS_URL_POST_COMMENT' is absent")
@pytest.mark.skipif(os.getenv("WS_URL_POST_COMMENT") is None, reason="Environment variable 'WS_URL_POST_COMMENT' is absent")
def test_add_comment():
assert (
Expand All @@ -30,3 +30,13 @@ def test_add_comment():
)["id"]
is not None
)


@pytest.mark.skipif(os.getenv("WS_PRJ_1010_HASH") is None, reason="Environment variable 'WS_PRJ_1010_HASH' is absent")
def test_post_comment_url():
assert (
Ws(post_comment="https://test.ws.com/api/admin/v2/?action=post_comment&page=/project/{0}/{1}/&email_user_from={2}&text={3}&hash={4}", email="[email protected]").post_comment_url(
1010, 1234, "text"
)
== "https://test.ws.com/api/admin/v2/?action=post_comment&page=/project/1010/1234/&[email protected]&text=text&hash=1fsasdfj"
)

1 comment on commit 076ab31

@0pdd
Copy link
Collaborator

@0pdd 0pdd commented on 076ab31 Jan 15, 2022

Choose a reason for hiding this comment

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

I wasn't able to retrieve PDD puzzles from the code base and submit them to GitHub. If you think that it's a bug on our side, please submit it to yegor256/0pdd:

set -x && set -e && set -o pipefail && cd /tmp/0pdd20211201-12-1eyu7d6/dgroup/g2w && master=master && git config --local core.autocrlf false && git reset origin/${master} --hard --quiet && git clean --force -d && git fetch --quiet && git checkout origin/${master} && git rebase --abort || true &&...

Please, copy and paste this stack trace to GitHub:

Exec::Error
set -x && set -e && set -o pipefail && cd /tmp/0pdd20211201-12-1eyu7d6/dgroup/g2w && master=master && git config --local core.autocrlf false && git reset origin/${master} --hard --quiet && git clean --force -d && git fetch --quiet && git checkout origin/${master} && git rebase --abort || true && git rebase --autostash --strategy-option=theirs origin/${master} [1]:
+ set -e
+ set -o pipefail
+ cd /tmp/0pdd20211201-12-1eyu7d6/dgroup/g2w
+ master=master
+ git config --local core.autocrlf false
+ git reset origin/master --hard --quiet
fatal: ambiguous argument 'origin/master': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
+ true
+ git rebase --autostash --strategy-option=theirs origin/master
fatal: Needed a single revision
invalid upstream 'origin/master'


/app/objects/exec.rb:60:in `block (2 levels) in run'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/open3.rb:219:in `popen_run'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/open3.rb:101:in `popen3'
/app/objects/exec.rb:54:in `block in run'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/timeout.rb:93:in `block in timeout'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/timeout.rb:33:in `block in catch'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/timeout.rb:33:in `catch'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/timeout.rb:33:in `catch'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/timeout.rb:108:in `timeout'
/app/objects/exec.rb:53:in `run'
/app/objects/git_repo.rb:110:in `pull'
/app/objects/git_repo.rb:75:in `push'
/app/objects/job.rb:36:in `proceed'
/app/objects/job_starred.rb:33:in `proceed'
/app/objects/job_recorded.rb:32:in `proceed'
/app/objects/job_emailed.rb:35:in `proceed'
/app/objects/job_commiterrors.rb:36:in `proceed'
/app/objects/job_detached.rb:48:in `exclusive'
/app/objects/job_detached.rb:36:in `block in proceed'
/app/objects/job_detached.rb:36:in `fork'
/app/objects/job_detached.rb:36:in `proceed'
/app/0pdd.rb:357:in `block in <top (required)>'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1675:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1675:in `block in compile!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1013:in `block (3 levels) in route!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1032:in `route_eval'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1013:in `block (2 levels) in route!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1061:in `block in process_route'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1059:in `catch'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1059:in `process_route'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1011:in `block in route!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1008:in `each'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1008:in `route!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1129:in `block in dispatch!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1101:in `block in invoke'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1101:in `catch'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1101:in `invoke'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1124:in `dispatch!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:939:in `block in call!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1101:in `block in invoke'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1101:in `catch'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1101:in `invoke'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:939:in `call!'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:929:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-protection-2.1.0/lib/rack/protection/xss_header.rb:18:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-protection-2.1.0/lib/rack/protection/path_traversal.rb:16:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-protection-2.1.0/lib/rack/protection/json_csrf.rb:26:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-protection-2.1.0/lib/rack/protection/base.rb:50:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-protection-2.1.0/lib/rack/protection/base.rb:50:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-protection-2.1.0/lib/rack/protection/frame_options.rb:31:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-2.2.3/lib/rack/logger.rb:17:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-2.2.3/lib/rack/common_logger.rb:38:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:253:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:246:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-2.2.3/lib/rack/head.rb:12:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-2.2.3/lib/rack/method_override.rb:24:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:216:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1991:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1542:in `block in call'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1769:in `synchronize'
/app/vendor/bundle/ruby/2.6.0/gems/sinatra-2.1.0/lib/sinatra/base.rb:1542:in `call'
/app/vendor/bundle/ruby/2.6.0/gems/rack-2.2.3/lib/rack/handler/webrick.rb:95:in `service'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/webrick/httpserver.rb:140:in `service'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/webrick/httpserver.rb:96:in `run'
/app/vendor/ruby-2.6.0/lib/ruby/2.6.0/webrick/server.rb:307:in `block in start_thread'

Please sign in to comment.