Skip to content

Commit 313498d

Browse files
splashxjesselang
authored andcommitted
Support for N-depth in module structure, basic testing and a travis pipeline (#3)
With these changes, `cog-command` supports complex commands such as `widget-create` or `some-really-long-command`. See README.md for details. A basic test suite has been added, along with an accompanying travis config to run those tests. A *huge* thank you to @splashx for this great work!
1 parent c74029d commit 313498d

23 files changed

+339
-15
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
**/*.pyo
33
build
44
dist
5-
*.egg-info
5+
*.egg-info
6+
.idea

.travis.yml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# encrypt the travis password / token using:
2+
# travis encrypt --add deploy.password
3+
#deploy:
4+
# provider: pypi
5+
# user: "Your username"
6+
# password:
7+
# secure: "Your encrypted password"
8+
# on:
9+
# tags: true
10+
11+
language: python
12+
13+
python:
14+
- 3.5
15+
- 3.6
16+
- nightly
17+
18+
env:
19+
- PYTHONPATH=tests/testbundle
20+
21+
install:
22+
- pip3 install .
23+
24+
script:
25+
- python -m unittest discover -v tests/

README.md

+66-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
[![Build Status](https://travis-ci.org/operable/pycog3.svg?branch=master)](https://travis-ci.org/operable/pycog3)
2+
13
# pycog3
24

35
Simple, opinionated library for building Cog commands in Python3.
@@ -13,7 +15,7 @@ imports, instantiates, and runs Python command code based on the
1315
values of `$COG_BUNDLE` and `$COG_COMMAND`.
1416

1517
`cog-command`'s magic requires that Python projects follow a strict
16-
directory layout:
18+
directory layout (basic):
1719

1820
```
1921
<bundle_name>
@@ -27,16 +29,78 @@ directory layout:
2729
|-- <command2>.py
2830
2931
```
32+
33+
pycog3 also supports more advanced, multi-level structure when using the `-` field separator in the command name.
34+
For example, defining:
35+
* `commands\commanda.py` - maps to `!commanda`
36+
* `commands\level1\commandb.py` - maps to `!level1-commandb`
37+
* `commands\level1\level2a\commandc.py` - maps to `!level1-level2a-commandb`
38+
* `commands\level1\level2b\leveln\commandz.py` - maps to `!level1-level2b-leveln-commandz`
39+
40+
41+
```
42+
<bundle_directory>
43+
|
44+
|-- <bundle_name>
45+
|-- __init__.py
46+
|-- commands
47+
|
48+
|-- __init__.py
49+
|-- <commanda>.py
50+
|-- <commandb>.py
51+
.
52+
.
53+
|-- <commandz>.py
54+
|
55+
|-- <level1>
56+
|
57+
|-- __init__.py
58+
|-- <commanda>.py
59+
|-- <commandb>.py
60+
.
61+
.
62+
|-- <commandz>.py
63+
|
64+
|-- <level2a>
65+
| |
66+
| |-- <commanda>.py
67+
| |-- <commandb>.py
68+
| |-- <commandc>.py
69+
| .
70+
| .
71+
| |-- <commandz>.py
72+
|
73+
|-- <level2b>
74+
|
75+
|-- __init__.py
76+
|-- <...>
77+
|
78+
|-- __init__.py
79+
| -- <leveln>
80+
|
81+
|-- __init__.py
82+
|-- <commanda>.py
83+
|-- <commandb>.py
84+
.
85+
.
86+
|-- <commandz>.py
87+
88+
```
89+
90+
The only requirement is a class with the same name of the filename should exist (first letter capital).
91+
3092
## Examples
3193

3294
See the [cog-bundles/statuspage](https://github.com/cog-bundles/statuspage) repository for an example of this library in action.
3395

96+
If you're interested in the multi-level usage, check the cog-bundle [pi-bundle](https://github.com/pan-net-security/pi-bundle) or the test bundle in `test/`.
97+
3498
## Installation
3599

36100
Add this line to your application's setup.py or requirements.txt:
37101

38102
```
39-
pycog3>=0.1.25
103+
pycog3>=0.1.28
40104
```
41105

42106
## TODO

bin/cog-command

+27-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,36 @@
22

33
import importlib
44
import os
5+
import sys
6+
7+
8+
def fail(message):
9+
print("%s" % (message), file=sys.stderr)
10+
sys.stdout.flush()
11+
sys.stderr.flush()
12+
sys.exit(1)
513

614
bundle_name = os.getenv("COG_BUNDLE")
15+
716
command_name = os.getenv("COG_COMMAND")
8-
class_name = command_name.capitalize()
9-
full_path = "%s.commands.%s" % (bundle_name, command_name)
1017

11-
bundle_module = importlib.import_module(full_path)
18+
try:
19+
path = command_name.replace("-", ".")
20+
except AttributeError as e:
21+
fail("ERROR: COG_COMMAND env var is not set")
22+
23+
full_path = "%s.commands.%s" % (bundle_name, path)
24+
class_name = command_name.split("-")[-1].capitalize()
25+
26+
27+
try:
28+
bundle_module = importlib.import_module(full_path)
29+
except ImportError as e:
30+
if len(command_name)==0:
31+
fail("ERROR: COG_COMMAND env var is set but empty")
32+
else:
33+
fail('ERROR: Unable to import module "' + str(full_path) + '"')
34+
1235
klass = getattr(bundle_module, class_name)
1336
cmd = klass()
14-
cmd.execute()
37+
cmd.execute()

cog/request.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import json
22
import os
33
import re
4-
import string
54
import sys
65

76
class Request(object):

requirements.txt

-3
This file was deleted.

setup.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
# from setuptools import setup, find_packages
21
from distutils.core import setup
32

43
setup (
54
name = "pycog3",
6-
version = "0.1.27",
5+
version = "0.1.28",
76
scripts = ["bin/cog-command"],
87
description = "Command library for the Cog ChatOps platform for Python3",
98
author = "Kevin Smith",
109
author_email = "[email protected]",
1110
url = "https://github.com/cog-bundles/pycog3",
12-
download_url = "https://github.com/cog-bundles/pycog3/tarball/0.1.27",
11+
download_url = "https://github.com/cog-bundles/pycog3/tarball/0.1.28",
1312
packages = ["cog"],
14-
requires = ["requests (>=2.10)", "PyYAML (>=3.11)"],
1513
keywords = ["bot", "devops", "chatops", "automation"],
1614
classifiers = [
1715
"Programming Language :: Python :: 3",

tests/test_command.py

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import os
2+
import unittest
3+
import subprocess
4+
5+
6+
# test summary for command
7+
# !commanda
8+
# !level1 - commanda
9+
# !level1 - level2a - commandc
10+
# !level1 - level2b - leveln - commandz
11+
# !nonexistent
12+
# COG_COMMAND env var no set
13+
# COG_COMMAND env var set but empty
14+
15+
class TestCommand(unittest.TestCase):
16+
17+
fixed_output_prefix = "COG_TEMPLATE: template\nJSON\n"
18+
bundle_name = 'testbundle'
19+
20+
def setUp(self):
21+
os.environ['dyn_config_var1'] = '1'
22+
os.environ['COG_BUNDLE'] = self.bundle_name
23+
self.maxDiff=None
24+
pass
25+
26+
def test_level0_commanda(self):
27+
os.environ['COG_COMMAND'] = 'commanda'
28+
result = subprocess.check_output(["cog-command"])
29+
30+
self.assertEqual(result.decode("utf-8"), self.fixed_output_prefix + '"0a"\n')
31+
32+
def test_level1_commandb(self):
33+
os.environ['COG_COMMAND'] = 'level1-commandb'
34+
result = subprocess.check_output(["cog-command"])
35+
36+
self.assertEqual(result.decode("utf-8") , self.fixed_output_prefix+'"1b"\n')
37+
38+
def test_level1_level2a_commandc(self):
39+
os.environ['COG_COMMAND'] = 'level1-level2a-commandc'
40+
result = subprocess.check_output(["cog-command"])
41+
42+
self.assertEqual(result.decode("utf-8"), self.fixed_output_prefix + '"2ac"\n')
43+
44+
def test_level1_level2b_leveln_commandz(self):
45+
os.environ['COG_COMMAND'] = 'level1-level2b-leveln-commandz'
46+
result = subprocess.check_output(["cog-command"])
47+
48+
self.assertEqual(result.decode("utf-8"), self.fixed_output_prefix + '"nz"\n')
49+
50+
def test_missing_cog_command_env_var(self):
51+
os.environ.pop("COG_COMMAND")
52+
result=subprocess.run(["cog-command"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
53+
self.assertEqual(result.stderr.decode("utf-8"), "ERROR: COG_COMMAND env var is not set\n")
54+
55+
def test_empty_cog_command_env_var(self):
56+
os.environ['COG_COMMAND'] = ''
57+
result=subprocess.run(["cog-command"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
58+
self.assertEqual(result.stderr.decode("utf-8"), "ERROR: COG_COMMAND env var is set but empty\n")
59+
60+
def test_invalid_command(self):
61+
os.environ['COG_COMMAND'] = 'nonexisting'
62+
full_path = "%s.commands.%s" % (self.bundle_name, os.environ.get("COG_COMMAND"))
63+
result=subprocess.run(["cog-command"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
64+
self.assertEqual(result.stderr.decode("utf-8"), 'ERROR: Unable to import module "' + str(full_path) + '"\n')
65+
66+
if __name__ == '__main__':
67+
unittest.main()

tests/testbundle/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
testbundle
2+
=======================================
3+
4+
# Overview
5+
6+
This is a dummy cog bundle used to test pycog3 python module.
7+
8+
9+
# Configuring
10+
11+
There is one dynamic configuration variable `dyn_config_var1` which should be set in the environmental variables:
12+
13+
# Executing
14+
15+
Should be executed after `pycog3` is installed.
16+
If running outside cog's environment, at the root of the project `/`, do:
17+
18+
```bash
19+
$ export PYTHONPATH=tests/testbundle
20+
$ export COG_BUNDLE="testbundle"
21+
$ export COG_COMMAND="commanda"
22+
$ export dyn_config_var1="foo"
23+
$ cog-command
24+
COG_TEMPLATE: template
25+
JSON
26+
"0a"
27+
```
28+
29+
30+

tests/testbundle/testbundle/__init__.py

Whitespace-only changes.

tests/testbundle/testbundle/commands/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from cog.command import Command
2+
3+
4+
class Testbundle(Command):
5+
def run(self):
6+
pass
7+
8+
def __init__(self):
9+
super().__init__()
10+
self.dyn_config_var1 = None
11+
12+
def prepare(self):
13+
self.dyn_config_var1 = self.config("dyn_config_var1")
14+
if self.dyn_config_var1 is None:
15+
self.fail('Missing dyn_config_var1')
16+
17+
def level0_function(self):
18+
return "0" #string 0 representing level 0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from testbundle.commands.base import Testbundle
2+
3+
4+
class Commanda(Testbundle):
5+
def __init__(self):
6+
super().__init__()
7+
8+
def run(self):
9+
self.run_command()
10+
11+
def run_command(self):
12+
level = self.level0_function()
13+
level = level + "a"
14+
self.response.content(level, template="template").send()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from testbundle.commands.base import Testbundle
2+
3+
class Level1(Testbundle):
4+
def run(self):
5+
pass
6+
7+
def __init__(self):
8+
super().__init__()
9+
10+
def level1_function(self):
11+
return "1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from testbundle.commands.level1.base import Level1
2+
3+
class Commandb(Level1):
4+
def __init__(self):
5+
super().__init__()
6+
7+
def run(self):
8+
self.level1_function()
9+
self.run_command()
10+
11+
def run_command(self):
12+
level = self.level1_function()
13+
level = level + "b"
14+
self.response.content(level, template="template").send()

tests/testbundle/testbundle/commands/level1/level2a/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from testbundle.commands.level1.base import Level1
2+
3+
4+
class Level2a(Level1):
5+
def run(self):
6+
pass
7+
8+
def __init__(self):
9+
super().__init__()
10+
11+
def level2a_function(self):
12+
return "2a"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from testbundle.commands.level1.level2a.base import Level2a
2+
3+
class Commandc(Level2a):
4+
def __init__(self):
5+
super().__init__()
6+
7+
def run(self):
8+
self.run_command()
9+
10+
def run_command(self):
11+
level = self.level2a_function()
12+
level = level + "c"
13+
self.response.content(level, template="template").send()

tests/testbundle/testbundle/commands/level1/level2b/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)