Skip to content

Commit

Permalink
WIP!
Browse files Browse the repository at this point in the history
  • Loading branch information
webknjaz committed Sep 30, 2023
1 parent b260fb0 commit dba599a
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 71 deletions.
21 changes: 21 additions & 0 deletions create_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#! /usr/bin/env python

import asyncio
import os
from octomachinery.github.api.tokens import GitHubOAuthToken
from octomachinery.github.api.raw_client import RawGitHubAPI


async def main():
access_token = GitHubOAuthToken(os.environ['GITHUB_TOKEN'])
gh = RawGitHubAPI(access_token, user_agent='webknjaz')
await gh.post(
'/repos/mariatta/strange-relationship/issues',
data={
'title': 'We got a problem',
'body': 'Use more emoji!',
},
)


asyncio.run(main())
1 change: 1 addition & 0 deletions octomachinery/app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""GitHub App infra."""

#from .github import Application # noqa: F401
from .server.runner import run # noqa: F401
106 changes: 106 additions & 0 deletions octomachinery/app/github.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# FIXME: is this even needed?
from __future__ import annotations

import logging
from typing import Iterable, Optional, TYPE_CHECKING

from aiohttp.web_runner import GracefulExit
from anyio import run as run_until_complete
import attr

# pylint: disable=relative-beyond-top-level
from ..app.server.machinery import run_forever

# pylint: disable=relative-beyond-top-level
from ..app.config import BotAppConfig

# pylint: disable=relative-beyond-top-level
from ..routing.default_router import WEBHOOK_EVENTS_ROUTER

# pylint: disable=relative-beyond-top-level
from ..utils.asynctools import auto_cleanup_aio_tasks

if TYPE_CHECKING:
# pylint: disable=relative-beyond-top-level
from ..routing.abc import OctomachineryRouterBase


logger = logging.getLogger(__name__)


# server entity vs client-holding object?
@attr.s
class GitHubApplication: # server-only?
# GitHubEventsReceiver?
# GitHubAppsContainer?
# OctomachineryApplication?
_event_routers: Iterable[OctomachineryRouterBase] = attr.ib(
default={WEBHOOK_EVENTS_ROUTER},
converter=frozenset,
)
_config: Optional[BotAppConfig] = attr.ib(
default=None,
)

@auto_cleanup_aio_tasks
async def serve_forever(self):
"""Spawn an HTTP server in an async context."""
return await run_forever(self._config, self._event_routers)
accept_webhooks = serve_forever

@classmethod
def run_simple( # FIXME:
cls,
*,
name: Optional[str] = None,
version: Optional[str] = None,
url: Optional[str] = None,
config: Optional[BotAppConfig] = None,
event_routers: Optional[Iterable[OctomachineryRouterBase]] = None,
):
"""Start up a server."""
if (
config is not None and
(name is not None or version is not None or url is not None)
):
raise TypeError(
f'{cls.__name__}.run_simple() takes name, '
'version and url arguments only if '
'the config is not provided.',
)
if config is None:
config = BotAppConfig.from_dotenv(
app_name=name,
app_version=version,
app_url=url,
)

logging.basicConfig(
level=logging.DEBUG
if config.runtime.debug # pylint: disable=no-member
else logging.INFO,
)
if config.runtime.debug: # pylint: disable=no-member
logger.debug(
' App version: %s '.center(50, '='),
config.github.app_version,
)

cls_kwargs = {'config': config}
if event_routers is not None:
cls_kwargs['event_routers'] = event_routers

cls(**cls_kwargs).start()

def start(self):
try:
run_until_complete(self.serve_forever)
except (GracefulExit, KeyboardInterrupt):
logger.info(' Exiting the app '.center(50, '='))

block = start

#@classmethod
#run??serve_forever w/ anyio? like app.server.runner

#also need an instance method too
71 changes: 2 additions & 69 deletions octomachinery/app/server/runner.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,6 @@
"""Octomachinery CLI runner."""

import logging
import sys
from typing import Iterable, Optional
from ..github import GitHubApplication as _GitHubApplication

from aiohttp.web_runner import GracefulExit
from anyio import run as run_until_complete

import attr

# pylint: disable=relative-beyond-top-level
from ..config import BotAppConfig
# pylint: disable=relative-beyond-top-level
from ..routing import WEBHOOK_EVENTS_ROUTER
# pylint: disable=relative-beyond-top-level
from ..routing.abc import OctomachineryRouterBase
# pylint: disable=relative-beyond-top-level
from .config import WebServerConfig
# pylint: disable=relative-beyond-top-level
from .machinery import run_forever as run_server_forever


logger = logging.getLogger(__name__)


def run(
*,
name: Optional[str] = None,
version: Optional[str] = None,
url: Optional[str] = None,
config: Optional[BotAppConfig] = None,
event_routers: Optional[Iterable[OctomachineryRouterBase]] = None,
):
"""Start up a server using CLI args for host and port."""
if event_routers is None:
event_routers = {WEBHOOK_EVENTS_ROUTER}

if (
config is not None and
(name is not None or version is not None or url is not None)
):
raise TypeError(
'run() takes either a BotAppConfig instance as a config argument '
'or name, version and url arguments.',
)
if config is None:
config = BotAppConfig.from_dotenv(
app_name=name,
app_version=version,
app_url=url,
)
if len(sys.argv) > 2:
config = attr.evolve( # type: ignore[misc]
config,
server=WebServerConfig(*sys.argv[1:3]),
)

logging.basicConfig(
level=logging.DEBUG
if config.runtime.debug # pylint: disable=no-member
else logging.INFO,
)
if config.runtime.debug: # pylint: disable=no-member
logger.debug(
' App version: %s '.center(50, '='),
config.github.app_version,
)

try:
run_until_complete(run_server_forever, config, event_routers)
except (GracefulExit, KeyboardInterrupt):
logger.info(' Exiting the app '.center(50, '='))
run = _GitHubApplication.run_simple
21 changes: 19 additions & 2 deletions octomachinery/github/api/app_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
# pylint: disable=relative-beyond-top-level
from ..entities.app_installation import GitHubAppInstallation
# pylint: disable=relative-beyond-top-level
from ..models import GitHubAppInstallation as GitHubAppInstallationModel
from ..models import (
GitHubAppInstallation as GitHubAppInstallationModel,
GitHubInstallationAccessToken,
)
# pylint: disable=relative-beyond-top-level
from ..models.events import GitHubEvent
from .raw_client import RawGitHubAPI
Expand All @@ -40,7 +43,7 @@


@attr.dataclass
class GitHubApp:
class GitHubApp: # TODO: have ctx here?
"""GitHub API wrapper."""

_config: GitHubAppIntegrationConfig
Expand Down Expand Up @@ -104,6 +107,20 @@ def api_client(self): # noqa: D401
user_agent=self._config.user_agent,
)

async def get_token_for(
self,
installation_id: int,
) -> GitHubInstallationAccessToken:
"""Return an access token for the given installation."""
return GitHubInstallationAccessToken(**(
await self.api_client.post(
'/app/installations/{installation_id}/access_tokens',
url_vars={'installation_id': installation_id},
data=b'',
preview_api_version='machine-man',
)
))

async def get_installation(self, event):
"""Retrieve an installation creds from store."""
if 'installation' not in event.payload:
Expand Down
2 changes: 2 additions & 0 deletions octomachinery/routing/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)


# FIXME: add register sugar methods
class GidgetHubRouterBase(_GidgetHubRouter, OctomachineryRouterBase):
"""GidgetHub-based router exposing callback matching separately."""

Expand Down Expand Up @@ -65,6 +66,7 @@ async def dispatch(
await coro


# TODO: new methods?
class ConcurrentRouter(GidgetHubRouterBase):
"""GitHub event router invoking event handlers simultaneously."""

Expand Down
13 changes: 13 additions & 0 deletions ping_event.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
# This needs to be a sequence of mappings;
# it cannot be just a mapping because headers
# may occur multiple times
- Content-Type: application/json
- X-GitHub-Delivery: 2791443c-641a-40fa-836d-031a26f0d45f
- X-GitHub-Event: ping
---
{
"hook": {"app_id": 0},
"hook_id": 0,
"zen": "Hey zen!"
}
35 changes: 35 additions & 0 deletions ping_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Ping event handler."""
print(f'{__file__} imported')
import logging

from octomachinery.app.routing import process_event
from octomachinery.app.routing import WEBHOOK_EVENTS_ROUTER
from octomachinery.app.routing.decorators import process_webhook_payload
from octomachinery.app.runtime.context import RUNTIME_CONTEXT


logging.basicConfig()
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)


@process_event('ping')
@process_webhook_payload
async def on_ping(*, hook, hook_id, zen):
"""React to ping webhook event."""
print('got ping')
app_id = hook['app_id']

logger.info(
'Processing ping for App ID %s '
'with Hook ID %s '
'sharing Zen: %s',
app_id,
hook_id,
zen,
)

logger.info(
'Github App Wrapper from context in ping handler: %s',
RUNTIME_CONTEXT.github_app,
)
53 changes: 53 additions & 0 deletions req_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import asyncio
import pathlib

from aiohttp.client import ClientSession

from octomachinery.github.api.app_client import GitHubApp
from octomachinery.github.config.app import GitHubAppIntegrationConfig


target_github_account_or_org = 'sanitizers' # where the app is installed to

github_app_id = 28012
github_app_private_key_path = pathlib.Path(
'~/Downloads/diactoros.2019-03-30.private-key.pem',
).expanduser().resolve()

github_app_config = GitHubAppIntegrationConfig(
app_id=github_app_id,
private_key=github_app_private_key_path.read_text(),

app_name='MyGitHubClient',
app_version='1.0',
app_url='https://awesome-app.dev',
)


async def get_github_client(github_app, account):
github_app_installations = await github_app.get_installations()
target_github_app_installation = next( # find the one
(
i for n, i in github_app_installations.items()
if i._metadata.account['login'] == account
),
None,
)
return target_github_app_installation.api_client


async def main():
async with ClientSession() as http_session:
github_app = GitHubApp(github_app_config, http_session)
github_api = await get_github_client(
github_app, target_github_account_or_org,
)
org = await github_api.getitem(
'/orgs/{account_name}',
url_vars={'account_name': target_github_account_or_org},
)
print(f'Org found: {org["login"]}')
print(f'Rate limit stats: {github_api.rate_limit!s}')


asyncio.run(main())
25 changes: 25 additions & 0 deletions usage_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from octomachinery.app.config import BotAppConfig
from octomachinery.app.github import GitHubApplication
from octomachinery.routing.routers import ConcurrentRouter


router1 = ConcurrentRouter() # .on and other helpers?

app1 = GitHubApplication(
event_routers={router1},
config=BotAppConfig.from_dotenv(),
)


# process many events arriving over HTTP:
__name__ == '__main__' and app1.start()
# FIXME: use variants?
# await app1.serve_forever() # ? == app1.start()? <- Probot

# NOTE: Depending on whether the API is for external or internal use,
# NOTE: its semantics may feel different. What for external caller is
# NOTE: "send event into the system", for internals would be "dispatch/
# NOTE: handle the received event".
# process a single event:
# await app1.dispatch_event(event) # ? == await app1.receive(event) <- Probot
# [BAD] await app1.simulate_event(event)? <-- tests? FIXME: have a pytest fixture called `simulate_event`?

0 comments on commit dba599a

Please sign in to comment.