Skip to content

Commit

Permalink
Merge pull request #116 from rveachkc/async-send
Browse files Browse the repository at this point in the history
Async send, thanks @nickolay-github!
  • Loading branch information
rveachkc committed Jan 18, 2022
2 parents 7aee332 + 24901b2 commit d311a27
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 35 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,18 @@ Install with pip:
pip install pymsteams
```

Install with async capabilities (python 3.6+):

```bash
pip install pymsteams[async]
```

## Usage

### Creating ConnectorCard Messages

This is the simplest implementation of pymsteams. It will send a message to the teams webhook url with plain text in the message.

```python
import pymsteams

Expand All @@ -35,6 +43,27 @@ myTeamsMessage.text("this is my text")
myTeamsMessage.send()
```

### Creating CreatorCard Messages to send via async loop

```python
import asyncio
import pymsteams

loop = asyncio.get_event_loop()

# the async_connectorcard object is used instead of the normal one.
myTeamsMessage = pymsteams.async_connectorcard("<Microsoft Webhook URL>")

# all formatting for the message should be the same
myTeamsMessage.text("This is my message")

# to send the message, pass to the event loop
loop.run_until_complete(myTeamsMessage.send())
```

Please visit the python asyncio documentation for more info on using asyncio and the event loop: https://docs.python.org/3/library/asyncio-eventloop.html


### Optional Formatting Methods for Cards

#### Add a title
Expand Down
9 changes: 8 additions & 1 deletion dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
anyio>=3.2.1
astroid>=2.4.2
atomicwrites>=1.3.0
attrs>=19.1.0
bandit>=1.6.2
bleach>=3.3.0
certifi>=2019.6.16
certifi>=2021.5.30
cffi>=1.14.0
chardet>=3.0.4
Click>=7.0
Expand All @@ -12,6 +13,9 @@ docutils>=0.16
dparse>=0.5.1
gitdb2>=2.0.5
GitPython>=2.1.11
h11>=0.12.0
httpcore>=0.13.6
httpx>=0.18.2
idna>=2.8
importlib-metadata>=0.18
isort>=4.3.20
Expand All @@ -34,15 +38,18 @@ PyYAML>=5.4
readme-renderer>=26.0
requests>=2.25.1
requests-toolbelt>=0.9.1
rfc3986>=1.5.0
safety>=1.9.0
SecretStorage>=3.1.2
six>=1.12.0
smmap2>=2.0.5
sniffio>=1.2.0
stevedore>=1.30.1
toml>=0.10.1
tqdm>=4.46.1
twine>=3.1.1
typed-ast>=1.4.0
typing-extensions>=3.10.0.0
urllib3>=1.26.5
wcwidth>=0.1.7
webencodings>=0.5.1
Expand Down
81 changes: 54 additions & 27 deletions pymsteams/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

# https://github.com/rveachkc/pymsteams/
# reference: https://dev.outlook.com/connectors/reference

import requests


class TeamsWebhookException(Exception):
"""custom exception for failed webhook call"""
pass


class cardsection:

def title(self, stitle):
Expand Down Expand Up @@ -42,8 +43,8 @@ def addFact(self, factname, factvalue):
self.payload["facts"] = []

newfact = {
"name" : factname,
"value" : factvalue
"name": factname,
"value": factvalue
}
self.payload["facts"].append(newfact)
return self
Expand All @@ -65,10 +66,10 @@ def text(self, stext):
def linkButton(self, buttontext, buttonurl):
self.payload["potentialAction"] = [
{
"@context" : "http://schema.org",
"@type" : "ViewAction",
"name" : buttontext,
"target" : [ buttonurl ]
"@context": "http://schema.org",
"@type": "ViewAction",
"name": buttontext,
"target": [buttonurl]
}
]
return self
Expand All @@ -89,30 +90,30 @@ def __init__(self):


class potentialaction:
def addInput(self,_type,_id,title, isMultiline = None):

def addInput(self, _type, _id, title, isMultiline=None):
if "inputs" not in self.payload.keys():
self.payload["inputs"] = []
if(self.choices.dumpChoices() == []):
if (self.choices.dumpChoices() == []):
input = {
"@type": _type,
"id": _id,
"isMultiline" :isMultiline,
"isMultiline": isMultiline,
"title": title
}
else:
input = {
"@type": _type,
"id": _id,
"isMultiline" :str(isMultiline).lower(),
"choices":self.choices.dumpChoices(),
"isMultiline": str(isMultiline).lower(),
"choices": self.choices.dumpChoices(),
"title": title
}

self.payload["inputs"].append(input)
return self

def addAction(self,_type,_name,_target,_body=None):
def addAction(self, _type, _name, _target,_body=None):
if "actions" not in self.payload.keys():
self.payload["actions"] = []
action = {
Expand Down Expand Up @@ -147,7 +148,7 @@ def addOpenURI(self, _name, _targets):
def dumpPotentialAction(self):
return self.payload

def __init__(self, _name, _type = "ActionCard"):
def __init__(self, _name, _type="ActionCard"):
self.payload = {}
self.payload["@type"] = _type
self.payload["name"] = _name
Expand All @@ -157,15 +158,17 @@ def __init__(self, _name, _type = "ActionCard"):
class choice:
def __init__(self):
self.choices = []
def addChoices(self,_display,_value):

def addChoices(self, _display, _value):
self.choices.append({
"display": _display,
"value": _value
})
"display": _display,
"value": _value
})

def dumpChoices(self):
return self.choices


class connectorcard:

def text(self, mtext):
Expand All @@ -192,10 +195,10 @@ def addLinkButton(self, buttontext, buttonurl):
self.payload["potentialAction"] = []

thisbutton = {
"@context" : "http://schema.org",
"@type" : "ViewAction",
"name" : buttontext,
"target" : [ buttonurl ]
"@context": "http://schema.org",
"@type": "ViewAction",
"name": buttontext,
"target": [buttonurl]
}

self.payload["potentialAction"].append(thisbutton)
Expand Down Expand Up @@ -226,7 +229,7 @@ def printme(self):
print("payload: %s" % self.payload)

def send(self):
headers = {"Content-Type":"application/json"}
headers = {"Content-Type": "application/json"}
r = requests.post(
self.hookurl,
json=self.payload,
Expand All @@ -235,9 +238,9 @@ def send(self):
timeout=self.http_timeout,
verify=self.verify,
)
self.last_http_status = r
self.last_http_response = r

if r.status_code == requests.codes.ok and r.text == '1': # pylint: disable=no-member
if r.status_code == requests.codes.ok and r.text == '1': # pylint: disable=no-member
return True
else:
raise TeamsWebhookException(r.text)
Expand All @@ -260,6 +263,30 @@ def __init__(self, hookurl, http_proxy=None, https_proxy=None, http_timeout=60,
self.proxies = None


class async_connectorcard(connectorcard):

async def send(self):
try:
import httpx
except ImportError as e:
print("For use the asynchronous connector card, "
"install the asynchronous version of the library via pip: pip install pymsteams[async]")
raise e

headers = {"Content-Type": "application/json"}

async with httpx.AsyncClient(proxies=self.proxies, verify=self.verify) as client:
resp = await client.post(self.hookurl,
json=self.payload,
headers=headers,
timeout=self.http_timeout)
self.last_http_response = resp
if resp.status_code == httpx.codes.OK and resp.text == '1':
return True
else:
raise TeamsWebhookException(resp.text)


def formaturl(display, url):
mdurl = "[%s](%s)" % (display, url)
return mdurl
11 changes: 7 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
from setuptools import setup
from setuptools.command.install import install

VERSION = "0.1.16"
VERSION = "0.2.0"


def readme():
""" print long description """
with open('README.md') as f:
long_descrip = f.read()
return long_descrip



class VerifyVersionCommand(install):
"""Custom command to verify that the git tag matches our version"""
description = 'verify that the git tag matches our version'
Expand Down Expand Up @@ -49,8 +51,6 @@ def run(self):
"Topic :: Internet",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Office/Business",
"Topic :: Office/Business :: Groupware",
],
Expand All @@ -59,6 +59,9 @@ def run(self):
install_requires=[
'requests>=2.20.0',
],
extras_require={
"async": ["httpx>=0.18.2"]
},
python_requires='>=2.7',
cmdclass={
'verify': VerifyVersionCommand,
Expand Down
25 changes: 22 additions & 3 deletions test/test_webhook.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import os
import sys
import pytest
Expand Down Expand Up @@ -33,7 +34,23 @@ def test_send_message():
teams_message.addLinkButton("Go to the Repo", "https://github.com/rveachkc/pymsteams")
# teams_message.send()

# assert isinstance(teams_message.last_http_status.status_code, int)
# assert isinstance(teams_message.last_http_response.status_code, int)


def test_async_send_message():
"""
This asynchronously send a simple text message with a title and link button.
"""

loop = asyncio.get_event_loop()

teams_message = pymsteams.async_connectorcard(os.getenv("MS_TEAMS_WEBHOOK"))
teams_message.text("This is a simple text message.")
teams_message.title("Simple Message Title")
teams_message.addLinkButton("Go to the Repo", "https://github.com/rveachkc/pymsteams")
# loop.run_until_complete(teams_message.send())

# assert isinstance(teams_message.last_http_response.status_code, int)


def test_send_sectioned_message():
Expand Down Expand Up @@ -67,7 +84,7 @@ def test_send_sectioned_message():

# send
# teams_message.send()
# assert isinstance(teams_message.last_http_status.status_code, int)
# assert isinstance(teams_message.last_http_response.status_code, int)


def test_send_potential_action():
Expand Down Expand Up @@ -107,7 +124,8 @@ def test_send_potential_action():
myTeamsMessage.summary("Message Summary")

# myTeamsMessage.send()
# assert isinstance(myTeamsMessage.last_http_status.status_code, int)
# assert isinstance(myTeamsMessage.last_http_response.status_code, int)


def test_http_500():
with pytest.raises(pymsteams.TeamsWebhookException):
Expand Down Expand Up @@ -153,6 +171,7 @@ def getMsg(card):
msg = getMsg(card2)
# assert msg.send()


def test_chaining():
card = pymsteams.cardsection()
card.title("Foo").activityTitle("Bar").activitySubtitle("Baz")
Expand Down

0 comments on commit d311a27

Please sign in to comment.