Repository: ohsu-comp-bio/py-tes (language: python)

Issue:
feat: pass through json serialization options
Pass through arbitrary keyword arguments to `json.dumps()` in the [`Base.as_json()`](https://github.com/ohsu-comp-bio/py-tes/blob/a9ac2959fdb38bd31433d358724e20c2c544c6a1/tes/models.py#L97-L101) method.

Among other things, this can be used to "pretty print" a model (see #21), like so:

```python
import tes

task = tes.Task(
    executors=[
        tes.Executor(
            image="alpine",
            command=["echo", "hello"]
        )
    ]
)

print(task.as_json(indent=3))
```

Output:

```console
{
   "executors": [
      {
         "image": "alpine",
         "command": [
            "echo",
            "hello"
         ]
      }
   ]
}
```

Resolves #21
Easily print task or other model
Models are not easily printable. as_dict and as_json methods exist, but don't dump an indented/pretty form that's readable.


Discussion / hints:
I think this is producing a new unit test error. But I don't see how it's connected to the PR. Is it because the test is running in python 3.10 and a new error occurred?
Yes, looks like. I could try to fix this, but it will be hard to make this work for the wide range of versions specified in `setup.py` (`2.7`, `3.4`, `3.5` and `3.6`) and including `3.7` through to `3.10`.

Actually, all of the currently supported versions have been deprecated/unsupported for more than a year:
![image](https://user-images.githubusercontent.com/10855418/217663062-5a73a52c-9a1c-4573-91db-8034a5e36f48.png)

Personally, I think that whoever uses `py-tes` and was happy with it up until now (not many people asking for updates, features etc.) can likely go on using `py-tes` up to `0.4.2`, and I think we can safely break backwards compatibility from at least `3.5` and backwards (better `3.6`).

I could:
- Update dependency ranges to satisfy recent Python `3.7` through `3.10` patch versions
- Update the code, where necessary
- Update the GH Actions workflow to run tests with all these versions

What do you think @kellrott?
It's probably worth the time to get it back in sync with the current Python release. And you are right, if someone needs an older version of Python, the 0.4.2 release of py-tes seems to have worked find so far.
Cool, I'll do that then.
Currently, this is what a printed task looks like:
```
Task(id='b7pacb33qt7nh789q47g', state=u'EXECUTOR_ERROR', name='5641737/P7-noXY/task-2', description=None, inputs=[Input(url='swift://smc-het/working_dir/5641737/tasks/tumors/P7-noXY/3/output_cnvs', path='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/3/output_cnvs', type=u'FILE', name=None, description=None, content=None), Input(url='swift://smc-het/working_dir/5641737/tasks/tumors/P7-noXY/3/output_variants', path='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/3/output_variants', type=u'FILE', name=None, description=None, content=None), Input(url=None, path='/opt/gwftool/script.sh', type=u'FILE', name=None, description=None, content=None), Input(url='swift://smc-het/working_dir/5641737/tools/PhyloWGS', path='/mnt/nfs1/smc-het/working_dir/5641737/tools/PhyloWGS', type=u'DIRECTORY', name=None, description=None, content=None)], outputs=[Output(url='swift://smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/OUTPUT_CLONAL_FREQS', path='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/OUTPUT_CLONAL_FREQS', type=u'FILE', name=None, description=None), Output(url='swift://smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/OUTPUT_TOP_K_TREES', path='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/OUTPUT_TOP_K_TREES', type=u'FILE', name=None, description=None), Output(url='swift://smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/TREES', path='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/trees.zip', type=u'FILE', name=None, description=None), Output(url='swift://smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4', path='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4', type=u'DIRECTORY', name=None, description=None)], resources=Resources(cpu_cores=2, ram_gb=None, disk_gb=None, preemptible=None, zones=None), executors=[Executor(image='docker.compbio.ohsu.edu/smc-het/5641737/phylowgs:smchet1', command=['bash', '/opt/gwftool/script.sh'], workdir='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4', stdin=None, stdout='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/stdout', stderr='/mnt/nfs1/smc-het/working_dir/5641737/tasks/tumors/P7-noXY/4/stderr', env=None)], volumes=None, tags={u'tumor': u'P7-noXY', u'entry': u'5641737'}, logs=[TaskLog(start_time=datetime.datetime(2017, 10, 27, 4, 0, 18, 969520, tzinfo=tzutc()), end_time=datetime.datetime(2017, 10, 27, 3, 59, 35, 469078, tzinfo=tzutc()), metadata=None, logs=[ExecutorLog(start_time=datetime.datetime(2017, 10, 27, 4, 1, 0, 291021, tzinfo=tzutc()), end_time=datetime.datetime(2017, 10, 27, 4, 1, 28, 5058, tzinfo=tzutc()), stdout=None, stderr=None, exit_code=1)], outputs=None, system_logs=None)], creation_time=None)
```
Looks more like this in a terminal:

![screen shot 2018-02-01 at 10 25 52 am](https://user-images.githubusercontent.com/262647/35695821-7c9c1ff6-073a-11e8-8955-32c6451fe5a5.png)


Fix patch:
diff --git a/.coverage b/.coverage
deleted file mode 100644
index 39575c3..0000000
--- a/.coverage
+++ /dev/null
@@ -1,1 +0,0 @@
-!coverage.py: This is a private format, don't read it directly!{"lines":{"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/utils.py":[9,11,12,13,14,15,16,17,18,19,20,21,534,23,24,26,27,28,32,33,34,37,39,42,561,521,572,583,524,695,90,607,784,99,100,101,103,104,615,620,622,157,616,629,532,635,533,640,641,130,619,644,154,538,160,674,163,164,166,168,169,170,683,686,642,177,692,182,183,698,546,211,724,730,219,733,737,738,739,740,741,745,244,680,639,259,262,265,266,779,780,781,272,816,304,833,839,841,842,843,844,846,850,851,854,861,863,864,867,868,875,364,268,379,892,390,410,417,419,420,431,449,459,675,676,496,497,339,500,506,507],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/sessions.py":[517,9,10,11,12,13,14,16,17,18,20,21,22,23,24,27,28,30,35,38,551,41,47,562,56,57,59,60,573,64,65,69,70,583,74,75,78,591,592,81,597,87,88,601,602,603,606,96,528,98,612,615,616,619,622,111,113,114,628,117,631,120,634,637,645,651,652,654,656,663,665,666,667,672,673,674,539,677,678,679,680,682,683,685,699,701,702,704,709,710,712,715,719,724,221,245,286,50,309,326,329,330,331,334,339,343,348,351,356,359,362,366,372,609,376,382,385,386,387,389,390,392,393,395,405,408,409,412,413,416,417,418,420,421,422,423,424,425,426,427,428,429,430,431,433,436,437,438,100,590,476,477,478,479,480,481,482,483,484,485,486,593,488,490,492,493,497,498,499,501,502,504,506,119],"/Users/strucka/Projects/tes-python-packages/py-tes/tes/__init__.py":[1,3,4,8,9,10,11,12,13,14,15,16,17,18],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/fields.py":[1,2,3,5,71,8,105,138,50,116,158,22,157,62,63],"/Users/strucka/Projects/tes-python-packages/py-tes/tests/test_client.py":[1,2,3,4,7,8,11,12,14,15,16,20,21,22,24,25,26,27,28,29,31,32,33,35,36,37,39,40,42,43,44,45,46,47,48,49,52,53,54,55,57,59,60,61,63,64,66,67,68,69,70,71,72,75,76,77,78,80,82,83,84,86,87,89,90,91,92,93,94,96,97,98,99,101,103,104,105,107,108,110,111,112,113,114,115,117,118,119,120,122,124,125,126,128,129],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/adapters.py":[9,11,12,14,15,16,17,18,19,20,21,22,23,24,25,26,27,29,30,31,34,35,36,38,40,41,42,43,46,47,48,49,52,53,55,56,58,59,76,81,106,107,108,110,111,112,113,114,117,118,120,122,123,124,126,128,132,144,157,158,159,161,162,164,201,253,263,266,269,272,273,274,276,277,282,285,286,288,290,313,319,320,323,352,366,388],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/sbcsgroupprober.py":[34,35,37,38,39,40,43,44,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/events.py":[4,5,6,7,8,15,16,21,22,23,24,25,26,27,28,29,31,32,36,37,38,39,40,42,43,45,46,47,48,49,50,51,52,54,55,56,57,58,59,61,62,64,66,67,68,69,70,71,72,73,75,76,78,79,81,82,84,85],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/skip.py":[57,59,60,61],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/proxy.py":[30,163,164,165,168,169,170,43,45,46,47,176,177,178,57,58,59,60,62,64,78,80,81,82,83,102,103,104,110,111,112,116,117,118],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/langturkishmodel.py":[192,52,183,186,187,188,189,190,191],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/version.py":[8,9,6],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/__init__.py":[3,5,6,8,14,15,16,17,18,19,20,21,25,26,27,33,34,35,51,54,57,76,83,85,87,88,90,93],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/six.py":[1,23,25,26,27,28,29,31,32,36,37,38,40,49,50,51,52,53,55,60,62,63,64,65,71,72,75,77,80,82,83,86,88,89,91,92,93,94,97,100,103,105,106,107,112,114,115,117,124,126,127,128,130,136,139,141,142,143,154,155,156,157,159,160,161,164,171,173,174,175,177,178,179,181,182,184,185,186,187,189,190,191,195,196,198,199,200,201,202,205,206,207,209,218,224,226,229,231,232,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,303,308,309,310,311,312,314,316,317,320,322,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,350,351,352,354,356,357,360,362,366,367,368,370,371,372,374,376,377,380,382,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,420,421,422,424,426,427,430,432,436,437,438,439,441,442,443,445,447,448,451,453,457,459,460,461,463,465,466,469,471,472,473,474,475,476,477,479,482,483,486,491,502,511,512,514,515,516,517,520,521,525,528,529,535,546,549,552,555,557,560,561,562,565,566,567,568,569,570,573,592,595,598,599,601,604,606,608,610,611,612,613,614,615,618,642,643,646,648,649,651,654,656,657,658,659,660,661,662,663,666,670,674,678,689,691,692,693,694,695,696,699,701,703,706,712,717,721,722,776,777,779,786,788,789,790,800,812,828,849,850,851,856,857,862,863,866,868],"/Users/strucka/Projects/tes-python-packages/py-tes/tes/models.py":[1,3,5,6,9,10,11,13,17,18,19,21,22,23,24,25,28,35,36,39,40,41,42,43,44,45,48,51,52,54,55,56,57,60,61,62,66,67,68,69,71,72,74,75,77,78,80,81,83,84,88,89,90,91,93,94,96,97,99,100,102,103,107,108,109,110,113,114,115,116,118,119,121,122,124,125,127,128,130,131,133,134,136,137,141,142,143,144,146,147,149,150,152,153,155,156,158,159,161,162,166,167,168,169,171,172,174,175,179,180,181,182,184,185,187,188,190,191,193,194,198,199,200,201,203,204,205,206,207,210,211,213,214,216,217,219,220,222,223,225,226,228,229,231,232,234,235,237,238,241,242,245,246,253,254,255,256,257,268,278,281,282,283,284,286,287,291,292,293,294,298,299,300,303,304,305,306,308,309,311,312,316,317,318,319,323,324,325,328,329,330,331,333,334,336,337,339,340,342,343,347,348,349,350,352,353],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/error.py":[2,4,37,6,7,8,9,10,11,12,45,14,48,50,51,46,58],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/capture.py":[64,96,98,69,102,97,58,59,101],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/validators.py":[128,130,3,5,134,7,137,138,11,12,13,14,15,144,146,19,20,21,23,27,133,36,166,39,43,151,56,59,60,61,63,147,75,82,135,98,99,100,38,102,103,104,106,108,110,111,115],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/suite.py":[539,540,543,544,545,546,547,548,551,552,554,563,52,53,567,68,72,73,75,76,79,80,81,94,95,96,97,98,99,100,103,104,105,106,107,113,114,148,149,150,151,153,154,155,156,157,158,173,177,201,204,205,208,209,216,217,218,224,226,227,228,269,270,274,277,278,279,282,283,285,286,287,288,289,290,291,292,293,297,298,301,302,303,304,308,309,310,312,564,315,323,324,325,326,327,328,329,330,331,337,338,339,340,341,342,313,345,346,347,348,349,350,351,356,357,358,360,361,362,364,365,367,368,372,373,374,394,396,397,401,402,403,404,405,406,407,418,419,420,421,422,423,424,427,435,436,441,443,445,446,447,448,451,452,453,454,457,459,460,462,463,464,465,466,467,471,474,475,476,477,478,479,480,481,482,483,484,485,486],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/testid.py":[137,138,142,143,144,145,148,149,150,151,154,155],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/packages/ssl_match_hostname/_implementation.py":[96,1,6,7,14,15,16,17,19,84,22,23,26,79],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/utf8prober.py":[35,36,38,76,44,49,53,57,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/resolver.py":[2,4,5,7,9,10,23,12,14,15,16,164,18,19,21,22,217,25,26,27,28,154,30,31,160,33,34,35,162,37,167,168,170,171,172,174,175,177,178,179,219,183,184,116,186,187,188,192,193,195,196,197,198,161,200,201,202,143,205,207,208,209,163,213,214,216,89,218,91,92,93,223,224,225,226,165,144,114,115,204,120],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/gb2312prober.py":[33,34,40,44,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/idna/__init__.py":[1],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/packages.py":[1,6,7,10,11,12],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/timeout.py":[1,99,4,5,7,103,11,140,15,18,195,171,182,88,91,156,93,213],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/enums.py":[5,8,11,12,13,14,17,21,22,23,24,25,26,27,28,29,32,35,36,37,38,41,44,45,46,47,50,53,54,55,56,57,59,65,71,72,73,74,75,76],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/xunit.py":[192,193,191],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/sjisprober.py":[32,33,36,37,44,48,52,56,89,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/api.py":[129,11,101,71,72,112,75,13,143,16,115,88,57,58,61],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/mbcsgroupprober.py":[32,33,34,35,36,37,38,41,42,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/_config.py":[1,19,4,6,9],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/__init__.py":[41,43,44,45,46,49,50,51,54,58,59,61,62,63,66,67,69,70,71,75,76,83,84,86,87,90,91,93,94,95,97,98,99,100,101,102,103,110,111,112,118,121],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/contrib/pyopenssl.py":[43,44,46],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/poolmanager.py":[1,2,3,4,6,7,8,9,10,11,12,13,16,402,19,149,22,151,152,281,410,155,154,159,160,162,165,425,170,301,49,54,439,57,266,147,197,204,206,377,352,229,110,111,112,242,115,116,117,153,121,379,380],"/Users/strucka/Projects/tes-python-packages/py-tes/tests/test_utils.py":[1,2,4,5,8,10,11,12,13,14,15,16,18,19,20,21,22,24,25,26,27,28,29,30,31,33,34,36,37,38,39,43,44,45,49,50,51,52,53,54,55,56],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/pkg_resources/_vendor/six.py":[185,187],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/hebrewprober.py":[128,130,131,132,133,134,135,136,137,138,139,144,149,151,152,154,28,29,286,164,174,178,182,196,282,255],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/exceptions.py":[14,15,18,19,21,24,29,30],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/representer.py":[2,3,5,6,8,10,12,13,15,17,18,20,27,34,40,74,75,76,77,78,80,81,82,83,84,86,94,112,136,139,141,149,153,169,172,179,182,185,186,187,189,209,224,227,233,237,241,248,251,252,254,255,257,258,260,261,263,264,266,267,269,270,272,273,275,276,278,279,281,282,284,285,287,288,290,291,293,295,311,320,326,337,340,344,348,389,451,452,454,455,457,458,460,461,463,464,466,467,469,470,472,473,475,476,478,479,481,482,484,485],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/compat.py":[32,34,60,42,44,13,47,16,17,20,23,25,26,31,28,29,30,52],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/idna/core.py":[1,2,3,4,5,6,8,9,10,12,258,16,17,18,131,21,22,23,26,27,28,286,31,32,33,36,39,42,45,49,307,56,190,63,140,335,231,364,146,124],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/constructor.py":[2,3,516,5,6,8,10,12,13,15,17,18,20,21,22,535,24,26,30,35,37,38,551,42,43,44,45,46,47,48,50,51,52,565,55,56,58,61,574,64,65,66,67,68,633,524,106,98,87,88,100,91,92,93,94,101,609,668,99,612,613,614,103,616,105,618,620,621,622,112,625,626,628,629,630,617,120,532,634,636,637,638,127,640,641,642,644,645,646,135,648,137,650,23,652,653,654,656,657,658,660,149,150,151,152,153,666,155,156,157,158,159,672,161,674,163,164,113,168,170,171,172,173,174,175,117,197,201,202,205,206,207,208,632,210,214,215,216,217,218,219,220,223,227,39,110,125,126,255,256,257,258,260,129,128,121,670,284,133,292,134,302,304,649,624,53,334,356,377,383,384,385,386,662,390,391,392,393,395,396,397,398,399,661,401,665,411,416,417,418,420,421,422,424,425,426,428,429,430,669,432,433,434,436,437,438,440,441,442,444,445,446,448,449,450,452,453,454,673,456,457,458,460,461,462,464,465,467,469,472,475,118,478,481,484,664,495],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/response.py":[13,15,16,17,18,19,20,22,23,25,26,28,29,32,34,60,62,64,68,70,75,76,78,83,87,96,97,98,102,106,112,114,115,119,122,139,141,143,144,145,146,147,148,150,152,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,171,172,173,175,177,180,181,183,184,185,186,187,190,192,193,198,201,202,207,208,210,214,218,220,225,226,229,230,231,232,235,236,238,239,240,241,242,243,244,245,246,247],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/idna/intranges.py":[34,6,38,8,10,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/failuredetail.py":[33,35,36],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/logcapture.py":[34,38,39,40,41,44,178,179,193,194,195,196,198,199,76,204,77,78,207,80,209,86,217,79,222,208],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/packages/__init__.py":[1,3,5],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/compat.py":[22,25,26,27,28,29],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/pyversion.py":[70,136,49,50,51,52,53,54,56,58],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/packages/ssl_match_hostname/__init__.py":[1,3,6,7,10,11,13,14,16,19],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/request.py":[1,2,4,5,7,8,11,12,77,95],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/__init__.py":[128,129,2,4,5,6,8,9,11,13,14,15,19,276,279,280,281,282,156,30,69,295,290,163,164,165,166,167,168,41,298,299,301,93,306,52,286,308,314,296,64,267,197,70,71,73,258,75,204,212,87,292,220,221,95,293,103,104,105,231,241,249,125,126,127],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/models.py":[513,515,516,517,8,521,10,11,12,526,17,530,19,20,21,22,849,25,26,27,29,30,31,34,35,548,39,42,43,559,48,49,50,51,52,565,55,56,57,60,61,574,575,578,581,609,584,585,588,589,591,592,593,82,596,601,91,92,93,606,95,96,97,98,99,612,101,102,746,104,105,103,620,109,623,531,617,532,635,637,640,643,534,654,877,662,665,675,170,171,685,174,689,179,180,182,704,194,709,711,177,631,716,721,726,216,219,220,223,224,225,226,227,229,230,743,233,234,235,236,237,238,239,240,241,243,894,246,763,765,767,770,772,774,264,300,779,781,776,280,282,284,286,520,288,560,291,293,295,297,812,301,402,304,305,306,307,308,309,822,564,315,317,830,832,331,333,334,335,568,850,853,825,857,858,347,354,355,868,870,360,745,365,573,370,371,375,381,524,917,896,320,747,388,393,397,398,400,401,914,405,918,408,409,410,411,412,413,414,415,928,417,930,931,420,933,934,423,424,937,939,428,430,431,433,436,437,438,440,441,442,444,816,100,451,452,748,454,462,463,464,467,468,846,472,847,337,827,495,936,498,499,500,501,425,505,818,508,511],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/request.py":[1,3,4,37,7,72,41,10,39,44,45,50,89,90,42],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/cyaml.py":[2,3,5,7,9,10,12,14,16,21,23,28,30,35,38,39,40,41,42,52,55,56,57,58,59,69,72,73,74,75,76],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/composer.py":[129,2,4,5,6,135,8,9,138,11,13,14,16,24,29,31,133,34,35,36,39,46,48,136,50,52,55,58,60,61,63,64,71,72,73,78,79,80,81,82,83,84,85,86,137,88,89,90,91,92,93,94,95,97,99,100,101,102,103,104,105,106,107,109,110,111,112,113,114,115,117,118,119,120,121,122,123,124,125,127],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/adapter.py":[13,15,16,17,19,20,21,23,26,28,29,31,32,34,37,38,42,46,50,55,62,65,66,68,76,78,79,80,81,82,83,84,87,88,89,90,91,92,94,95,96,104,105,108,109,113,114,118,122,126,129,133,134,136,143,148,150,151,171,173,174,177,180,182,183,184,185,186,188,189,194,197,200,202,203,206,209,210,211,212,213,215,216,217,218,219,221,222,223,228,229,230,231,235,238,244,245,246,247,249,252,255,256,264,265,266,267,268,269,270,271,272,273,274,276,284,287],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/charsetgroupprober.py":[32,33,65,39,49,85,57,28,29],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/charsetprober.py":[32,66,35,37,39,103,44,61,47,51,54,58,29,30],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/contrib/socks.py":[32,33,37,39,23,24,26,27,28,29,30],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/case.py":[128,129,130,131,132,133,140,147,148,149,151,29,33,34,36,37,38,39,40,41,42,45,59,60,64,69,70,74,99,100,101,102,103,104],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/auth.py":[8,10,11,12,13,14,15,17,19,20,21,22,24,25,28,286,292,266,72,73,75,79,80,82,86,217,92,222,95,100,101,103,108,109,111,117,127],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/langhebrewmodel.py":[193,194,195,196,197,198,199,53,190],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/packages/six.py":[1,2,4,6,8,10,12,13,20,23,24,25,26,27,28,29,31,32,35,36,37,38,39,40,41,45,47,49,50,51,52,53,55,57,59,60,61,62,63,64,65,67,69,71,72,73,75,77,79,80,81,82,83,85,86,87,88,89,91,92,93,94,97,100,103,105,106,107,112,114,115,117,124,126,127,128,130,136,139,141,142,143,154,155,156,157,159,160,161,164,171,173,174,175,177,178,179,181,182,184,185,186,187,189,190,191,195,196,198,199,200,201,202,203,205,206,207,209,218,224,226,229,231,232,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,303,308,309,310,311,312,314,316,317,320,322,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,350,351,352,354,356,357,360,362,366,367,368,370,371,372,374,376,377,380,382,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,420,421,422,424,426,427,430,432,436,437,438,439,441,442,443,445,447,448,451,453,457,459,460,461,463,465,466,469,471,472,473,474,475,476,477,479,482,483,486,491,502,511,512,514,515,516,517,520,521,525,528,529,535,546,549,552,555,557,560,561,562,565,566,567,568,569,570,573,592,595,596,598,601,604,606,608,610,611,612,613,614,615,618,642,646,648,649,651,654,656,657,658,659,660,661,662,663,666,670,674,678,689,691,692,693,694,695,696,699,701,703,706,712,717,721,722,776,777,779,786,788,789,790,800,812,828,849,850,851,856,857,862,863,866,868],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/_make.py":[512,1,514,3,4,1029,6,519,8,9,10,523,524,526,941,344,18,19,20,21,22,345,536,25,540,541,30,31,545,34,37,40,553,554,43,44,557,46,349,560,50,563,513,565,566,1033,56,57,58,571,572,1034,693,352,579,326,694,1036,586,695,588,590,591,569,594,568,799,699,1041,617,716,530,570,531,717,532,635,637,126,533,130,131,132,645,134,135,136,137,138,142,365,803,707,789,662,755,152,154,155,157,158,159,160,161,164,165,166,167,170,199,177,178,179,180,542,182,183,696,185,186,543,671,714,373,193,195,708,197,516,712,201,202,715,204,546,718,207,208,209,210,211,724,377,217,218,719,220,638,549,720,587,379,230,564,237,238,239,754,723,756,757,758,761,765,766,575,790,642,558,333,794,559,796,797,798,133,800,801,802,219,804,805,806,812,813,814,815,818,825,223,828,906,832,181,834,323,836,837,838,839,840,841,842,843,844,845,334,847,336,337,850,1030,340,853,854,855,856,857,346,347,860,861,350,864,356,358,359,872,361,363,364,402,573,371,372,885,374,887,376,889,890,891,380,381,895,384,388,389,902,391,904,905,394,395,908,909,911,912,401,914,403,916,918,919,920,580,922,925,926,415,928,929,930,931,933,422,426,427,428,429,942,431,945,947,437,438,585,441,927,445,446,448,672,331,452,965,454,968,969,458,459,322,463,419,982,983,984,473,986,482,995,491,500,509,510,511],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/packages/ordered_dict.py":[128,257,132,5,6,137,10,11,142,16,17,151,154,156,28,218,158,159,160,161,34,36,37,38,39,40,41,42,44,173,175,48,177,50,51,52,54,169,244,190,63,65,66,67,68,197,71,79,69,210,212,213,214,215,216,164,49,91,220,224,225,235,168,116,170,120,249,122,124,253,126],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/serializer.py":[2,4,5,6,8,9,74,11,13,46,15,16,78,36,27,60],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/contrib/__init__.py":[1],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/euctwfreq.py":[385,44,47],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/core.py":[65,34,36,37,41,42,43,44,50,51,55,56,59,188,61,62,193,66,199,200,201,202,203,204,205,207,187,60],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/mbcharsetprober.py":[34,37,39,45,53,57,90,61,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/__init__.py":[1,3,10,20,24,25,26,27,30,32,33,34,35,37,38,40,41,44,45,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/euckrprober.py":[34,35,41,45,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/loader.py":[522,523,540,541,542,547,550,551,559,564,566,567,569,570,79,81,82,83,84,85,86,89,90,91,92,93,94,95,97,99,105,108,109,110,112,113,114,116,119,121,122,123,128,131,134,135,143,144,145,146,147,149,150,151,154,156,157,158,159,160,161,163,164,167,168,170,177,178,179,180,181,182,183,186,196,197,200,201,209,210,211,212,314,315,316,317,321,322,323,325,326,327,328,330,331,332,333,338,340,341,343,356,359,369,371,374,375,378,379,404,405,406,409,410,416,417,418,420,421,428,431,432,433,434,435,436,447,454,455,473,474,475,476,481,486,487,488,493,494],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/langgreekmodel.py":[224,69,50,206,209,210,211,212,213,214,215,218,219,220,221,222,223],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/cover.py":[164,263,173,271,182,183],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/certifi/__init__.py":[1,3],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/cp949prober.py":[34,35,43,47,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/_collections.py":[1,2,3,4,133,262,135,136,265,138,139,268,14,15,18,276,21,150,151,24,281,154,27,157,261,160,237,294,39,168,41,43,172,173,47,175,48,177,50,180,182,137,57,87,288,224,72,202,247,45,79,208,83,142,86,185,89,90,143,92,93,229,96,263,101,232,171,44,146,238,297,250,234],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/converters.py":[8,3,5],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/selector.py":[149,129,130,131,134,135,140,141,144,148,171,153,152,68,154,156,157,69,162,35,37,167,40,41,42,43,44,45,174,175,176,178,179,53,54,57,187,188,191,193,194,196,197,72,73,74,76,77,80,81,163,169,222,224,225,226,227,228,229,232,233,234,235,236,237,238,239,240,241,116,117,118,119,123,170,126,127],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/universaldetector.py":[36,39,40,41,43,44,45,46,47,48,51,66,68,69,70,71,72,73,74,75,76,77,78,79,81,220,94,111],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/retry.py":[1,2,3,4,5,6,7,9,394,279,273,142,144,145,147,20,150,23,24,153,154,155,285,158,159,160,162,27,294,167,168,169,170,171,257,173,175,172,310,189,190,319,320,203,401,217,152,166,17,233,157,243,251],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/latin1prober.py":[130,29,30,32,34,35,36,37,38,39,40,41,42,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,92,96,97,103,108,112,116],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/result.py":[38,39,40,41,106,43,44,109,110,104,105,103],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/escprober.py":[35,69,40,73,42,77,83,58,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/jisfreq.py":[322,44,47],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/codingstatemachine.py":[33,66,86,80,83,54,55,28,30,63],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/__init__.py":[1,3,4,5,6,16,21,22,28,53],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/scanner.py":[514,375,1149,1197,1177,120,27,29,542,1055,32,33,35,548,549,38,39,40,41,42,43,44,46,48,561,121,1204,60,181,64,67,70,1095,465,73,76,79,593,594,595,596,1109,598,478,609,1124,101,620,109,1137,115,116,117,118,631,1144,1145,634,191,1148,637,1150,1151,1152,311,130,132,1157,134,1159,648,1161,1162,651,1164,1165,1166,1167,1168,1169,1170,1171,1172,1173,1174,1175,1176,665,1178,156,1181,1182,1183,1184,1187,1189,166,113,680,169,1194,1195,1196,173,1198,687,177,773,30,695,654,703,192,195,708,199,200,204,782,1228,718,207,208,721,722,211,1230,365,728,219,220,122,223,227,550,1254,231,1201,235,749,125,979,247,1272,127,769,771,772,261,774,775,778,215,269,270,271,784,273,274,276,196,657,1156,283,284,285,286,555,803,292,163,146,1319,470,298,302,303,304,136,306,307,309,137,824,314,138,318,123,322,1163,1153,652,843,1356,551,337,338,142,857,346,143,144,867,356,1382,145,361,364,877,368,371,374,887,376,660,379,382,149,1408,385,387,901,422,151,312,399,402,1427,405,153,239,1436,1437,150,1443,421,1446,424,937,427,430,433,243,436,439,440,441,442,159,444,445,447,448,450,453,456,129,459,663,305,462,463,464,248,467,203,473,476,477,1158,479,481,1190,1191,272,1192,545,425,1193,170,341],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/sbcharsetprober.py":[33,34,35,36,37,70,39,77,53,124,29,30,63],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/deprecated.py":[40,42,43,44],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/_compat.py":[1,3,4,7,10,11,15,16,19,21,24,28,31,33,38,43,48,53,58,63,68,70,72,73,74,75],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/big5prober.py":[34,35,41,45,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/compat.py":[9,11,13,20,23,26,28,29,30,33,39,40,43,44,45,46,47,49,51,52,53,54,55,56],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/gb2312freq.py":[281,42,44],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/structures.py":[8,10,12,15,40,42,43,44,45,46,48,51,53,54,56,59,60,62,63,65,73,82,85,89,90,92,93,94,96,99,104],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/nodes.py":[2,3,8,25,26,28,29,30,31,32,33,35,37,38,39,40,41,42,44,45,47,48],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/_funcs.py":[1,3,5,6,7,10,11,142,151,154,39,40,41,42,43,45,46,49,50,51,52,55,57,187,64,67,70,71],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/debug.py":[40,41,42,43],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/langcyrillicmodel.py":[321,141,302,278,281,282,283,284,285,286,287,290,291,292,293,294,295,296,299,300,301,46,303,304,305,308,309,310,311,312,313,314,317,318,319,320,65,322,323,326,327,328,329,330,331,332,84,103,122],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/__init__.py":[24,19,20,21],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/jpcntx.py":[31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,116,117,118,119,120,121,123,131,143,170,173,180,183,184,188,192,212,213],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/mbcssm.py":[534,538,539,28,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,566,568,569,570,571,572,64,68,69,70,73,75,76,77,78,79,99,104,105,106,107,108,109,110,113,115,116,117,118,119,155,159,160,161,162,163,166,168,169,170,171,172,208,212,213,216,218,219,220,221,222,258,262,263,264,265,266,267,270,272,273,274,275,276,312,316,317,318,319,320,321,329,331,332,333,334,335,540,373,377,378,379,382,384,385,386,387,388,424,428,429,430,431,432,433,434,437,439,440,441,442,443,479,483,484,485,486,487,488,489,492,494,495,496,497,498],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/euckrfreq.py":[41,43,193],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/errorclass.py":[148,150,151,152,153,154],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/euctwprober.py":[33,34,40,44,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/connectionpool.py":[1,2,3,4,5,7,8,777,11,155,448,282,409,26,27,28,29,158,159,214,161,162,35,36,38,39,40,41,42,43,812,794,46,157,48,50,883,52,54,58,757,62,406,64,65,67,836,292,201,74,759,78,288,81,163,86,855,164,731,94,97,321,449,747,304,749,750,752,753,754,755,756,446,758,425,447,252],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/base.py":[98,100,101,102],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/certifi/core.py":[35,9,10,11,14,18,21,22,24,27],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/prof.py":[71,74,75,76,80,81,82,83,84,57],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/hooks.py":[34,13,14,17,18,23,25,26,27],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/exceptions.py":[1,2,8,9,10,13,14,15,18,19,20,24,29,30,31,35,40,41,42,45,46,47,50,51,52,55,56,57,61,66,74,76,85,86,88,94,95,96,99,104,105,108,109,110,115,116,117,120,121,122,125,126,127,130,131,132,135,136,137,140,141,143,150,151,152,153,156,157,158,161,162,163,166,167,168,171,172,173,176,177,178,181,182,183,186,190,191,194,195,196,199,203,204,207,214,215,218,223,224,225,228,229,232,237,238,239,244,245,246],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/selectors.py":[256,130,131,133,8,9,10,11,12,13,14,16,17,18,19,21,22,150,24,25,26,539,29,30,287,288,289,34,164,37,294,389,41,534,278,172,302,136,308,286,312,58,543,266,565,192,139,71,456,457,458,459,206,463,336,466,270,165,226,484,146,245,505,250,281,127],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/emitter.py":[516,9,11,12,14,15,17,18,1047,540,31,33,34,35,38,39,922,559,1082,582,106,619,111,629,120,133,146,160,170,175,178,546,215,227,233,234,261,267,275,790,281,794,799,803,293,470,816,311,317,840,334,847,856,355,360,829,369,374,377,389,393,396,910,911,912,913,914,915,916,917,918,919,920,921,410,923,924,925,415,928,423,427,431,438,460,982,993,495],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/polling.py":[1,3,5,6,7,44,39,12,13,14,45,19,20,23,24,33,27],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/response.py":[1,2,3,4,5,6,7,9,10,14,15,16,17,19,22,24,29,32,545,513,55,57,60,522,63,69,76,108,110,111,113,114,531,116,118,121,122,123,124,125,126,127,128,130,131,132,133,134,136,139,140,142,143,146,147,148,150,151,155,158,161,174,115,181,190,194,202,206,208,219,239,240,245,248,250,256,257,260,264,265,273,276,278,289,298,300,301,302,324,328,567,343,346,367,368,371,374,375,377,378,383,384,385,393,394,395,403,404,405,408,410,413,415,431,435,436,438,439,441,471,474,478,485,487,489,491,492,496,505,509],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/ssl_.py":[1,2,3,4,6,7,9,215,12,13,14,15,18,19,20,21,278,279,280,25,38,39,42,43,44,45,176,50,51,70,71,72,73,74,75,76,77,78,79,80,81,82,83,86,87,216,199,281,149],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/wait.py":[1,36,29,9],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/exceptions.py":[8,9,12,15,17,19,20,21,22,23,24,25,28,29,32,33,36,37,40,41,44,50,53,57,60,61,64,65,68,69,72,73,76,77,80,81,84,85,88,89,92,93,96,97,100,101,104,105,110,111,112,115,116,117,120,121,122],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/exceptions.py":[1,4,33,39,12,13,14,17,22,25,30],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/connection.py":[1,2,3,4,5,6,7,8,9,10,11,13,14,15,282,259,23,25,26,27,28,154,31,37,39,169,257,48,50,52,54,55,56,62,65,66,67,70,258,260,208,209,211,213,214,215,95,97,354,228,101,165,104,106,255,368,370,371,246,250,251,252,253,254,127],"/Users/strucka/Projects/tes-python-packages/py-tes/tes/utils.py":[1,3,4,5,6,7,8,11,12,13,15,16,17,18,20,21,22,23,24,25,28,29,30,31,32,35,36,37,38,39,40,41,42,43,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,87,88,89,90,91,92,93],"/Users/strucka/Projects/tes-python-packages/py-tes/tes/client.py":[1,3,4,6,7,8,10,11,14,15,16,17,19,21,22,27,28,29,30,31,32,33,34,35,36,37,38,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/langbulgarianmodel.py":[224,225,226,227,72,209,212,53,214,215,216,217,218,223,221,222,213],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/manager.py":[128,262,263,264,265,272,273,274,149,167,166,295,168,301,302,177,178,184,249,88,89,93,94,95,96,99,252,106,107,111,114,123,118,105,120,121,250,251,124,253,254],"/Users/strucka/Projects/tes-python-packages/py-tes/tests/test_models.py":[1,2,4,7,8,10,11,12,17,19,20,21,26,27,29,30,32,33,35,36,37,39,41,43],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/__init__.py":[32,33,34,35,36,13,14,15,16,17,20,21,22,23,24,25,26,27,28,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/cookies.py":[512,514,515,516,520,10,523,12,13,14,15,17,18,20,21,535,536,537,26,542,36,38,39,40,41,43,46,49,52,66,69,72,75,79,82,83,85,89,93,98,529,103,105,110,112,113,115,119,532,126,127,128,136,142,143,144,147,166,169,172,188,190,202,219,228,236,245,253,262,271,279,287,300,316,322,331,338,344,349,351,352,357,377,402,409,415,422,437,472,503,511],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/util.py":[320,446,278,263,520,521,266,267,140,397,270,271,272,273,403,660,405,662,663,408,409,410,411,312,393,163,164,406,470,321,264,306,307,308,309,310,265,184,313,187,188,189,190,319,192,279,195,407,471,311,337,338,339,340,398,342,343,399,479,323,481,483,484,485,486,449,404,448,318,502,503,504,505,506,447,276],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/isolate.py":[61,62],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/langthaimodel.py":[192,193,194,195,196,197,198,52,189],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/multiprocess.py":[224,225,226,227,231,233,234,235,238,223],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/importer.py":[143,144,146,147,148,149,151,152,153,154,155,156,157,158,32,161,165,166,167,40,41,42,44,45,47,30,54,59,62,53,65,66,67,68,70,71,72,75,76,77,78,79,80,81,94,96,97,98,99,101,103,104,63],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/reader.py":[129,133,134,135,137,138,139,140,175,173,152,146,147,20,149,22,151,24,26,155,156,154,150,33,169,170,171,172,45,174,157,177,59,60,61,62,63,64,65,66,67,68,69,70,71,72,76,77,78,79,87,88,89,94,95,97,99,100,101,102,103,104,105,106,107,18,110,111,112,114,115,116,117,148,122,123,125,126],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/idna/idnadata.py":[3,37,54,65,71,81,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,590,1571,1574,1582],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/pkg_resources/extern/__init__.py":[28,29,30],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/eucjpprober.py":[32,33,36,37,44,48,52,56,89,28,29,30,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/chardistribution.py":[171,132,133,139,151,152,28,30,32,34,36,40,41,42,43,44,46,177,158,61,192,193,70,199,84,217,218,224,100,105,113,114,120,170],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/dumper.py":[2,4,5,6,7,9,12,13,14,15,16,27,30,31,32,33,34,45,48,49,50,51,52],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/certs.py":[17,14,15],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/url.py":[1,2,201,4,7,11,14,19,20,22,23,24,132,154,27,28,26,158,159,160,33,162,163,164,38,167,168,176,172,174,29,48,179,30,55,184,31,189,190,192,193,195,198,161,200,129,211,215,219,222,95,225,99,115,116,117,118,119,120,122,123,124,126],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/connection.py":[96,1,2,3,4,37,7,107,130,110,109,112,120,121,118,119,88,36,125,126,127],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/__version__.py":[5,6,7,8,9,10,11,12,13,14],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/util/response.py":[1,2,4,69,38,7,15,18,19,20,22,24],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/_internal_utils.py":[37,38,39,40,9,11,14,19,20,27,30],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/tokens.py":[2,3,4,5,6,17,18,19,25,26,28,29,31,32,33,34,35,36,37,39,40,42,43,45,46,48,49,51,52,54,55,57,58,60,61,63,64,66,67,69,70,72,73,75,76,77,82,83,84,89,90,91,96,97,98,99,100,101,102,103],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/attr/filters.py":[3,5,38,7,8,11,21],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/big5freq.py":[384,43,46],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/loader.py":[33,2,4,5,6,7,8,9,11,13,21,23,24,25,26,27,28,29,31],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/yaml/parser.py":[512,526,537,538,539,540,542,543,544,545,546,551,552,553,554,555,556,563,564,565,566,567,569,570,571,572,573,62,64,65,66,67,69,70,583,72,587,76,77,78,81,82,83,84,85,86,87,89,91,92,94,96,97,98,99,100,102,103,104,105,107,109,112,114,116,117,118,119,120,121,127,130,131,132,135,137,139,142,143,144,145,146,147,148,151,152,154,159,162,166,183,184,185,186,187,188,190,193,194,195,196,200,201,204,206,208,217,264,265,267,268,270,273,274,279,280,281,282,292,301,315,316,317,318,319,325,326,327,328,330,331,334,335,336,337,338,339,340,341,342,343,344,345,346,372,574,376,381,402,422,427,446,471,472,473,474,476,477,478,479,480,486,493,494,495,496,497,498,499,500,502],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/request.py":[130,134,136,138,139,13,14,143,16,17,18,21,26,28,29,30,31,32,36,37,38,39,40,44,46,47,49,51,52,54,55,57,59,61,63,65,67,69,76,141,95,97,99,101,103,146,110,112,114,118,122,126],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/urllib3/filepost.py":[1,2,4,5,7,8,9,11,14,21,41,59],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/nose/plugins/doctests.py":[192,193,194,195,188,189,190,191],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests/status_codes.py":[3,5,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,25,26,27,28,29,30,31,32,34,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,72,73,74,75,76,77,78,79,80,81,82,85,87,88,89,90,91],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/requests_mock/mocker.py":[68,128,134,137,138,87,140,13,15,144,17,18,20,21,22,23,24,25,26,155,28,158,31,161,36,165,38,39,40,41,42,43,172,45,180,181,183,184,244,187,188,190,63,64,65,67,196,70,73,205,78,141,81,83,84,86,185,88,152,143,223,123,100,101,167,146,110,114,116,170,153,121,122,127,125,126,149],"/Users/strucka/Projects/tes-python-packages/venv/lib/python2.7/site-packages/chardet/escsm.py":[129,131,132,133,134,135,136,28,243,170,174,175,176,177,178,179,180,181,182,185,187,188,189,62,191,192,66,67,68,69,70,71,74,76,77,78,79,80,81,226,230,231,232,233,234,237,239,240,241,242,115,244,190,119,120,121,122,123,124,125,126],"/home/kellrott/workspaces/py-tes/tes/__init__.py":[1,3,4,5,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,22,41],"/home/kellrott/workspaces/py-tes/tes/client.py":[1,3,4,5,7,8,9,10,12,15,18,22,23,24,25,26,27,28,29,30,31,32,33,34,36,37,45,53,67,77,86,103,123,19,38,39,78,79,124,126,128,130,134,135,137,80,81,82,83,84,40,41,42,54,55,59,127,60,61,62,64,65,46,47,48,49,50,51,68,69,70,129,71,72,73,74,75,87,88,89,90,91,92,94,96,97,98,99,100,101,104,107,110,111,115,116,105,119,121,120,117],"/home/kellrott/workspaces/py-tes/tes/models.py":[1,3,4,5,6,8,9,10,11,14,15,16,18,33,40,44,56,70,76,82,88,89,91,97,104,105,106,107,109,110,112,113,115,116,118,119,121,122,126,127,128,129,131,132,134,135,137,138,140,141,145,146,147,148,150,151,153,154,156,157,159,160,41,164,165,166,167,169,170,172,173,175,176,178,179,181,182,184,185,189,190,191,192,193,194,196,197,198,199,201,202,204,205,207,208,212,213,214,215,217,218,220,221,225,226,227,228,229,230,232,233,234,235,237,238,240,241,243,244,246,247,251,252,253,254,256,257,258,259,260,263,264,266,267,269,270,272,273,275,276,278,279,281,282,284,285,287,288,290,291,292,293,296,361,362,363,364,366,367,371,372,373,374,378,379,380,383,384,385,386,388,389,391,392,396,397,398,399,403,404,405,408,409,410,411,413,414,416,417,419,420,422,423,427,428,429,430,432,433,57,62,63,58,59,65,22,77,79,98,99,92,93,94,45,47,48,50,49,53,46,100,297,298,301,302,304,306,309,312,315,322,333,342,348,355,358,323,324,325,326,328,330,356,334,335,337,339,23,24,26,27,28,29,30,78,71,72],"/home/kellrott/workspaces/py-tes/tes/utils.py":[1,3,4,6,7,11,12,15,20,21,25,26,30,90,91,92,93,94,95,96,31,33,34,38,39,40,41,16,17,46,47,48,49,50,51,45,54,63,64,65,66,77,79,80,87,67,75,76,55,56,57,61,27,81,82,83,84,85,22,32,58,68,69,70,71,72,73]}}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index d22d113..18dd7bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,5 +16,180 @@ eggs/
 \#*\#
 .desktop
 
+# Misc
 test_tmp
-*venv*
\ No newline at end of file
+*venv*
+
+# Created by https://www.toptal.com/developers/gitignore/api/python
+# Edit at https://www.toptal.com/developers/gitignore?templates=python
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+# ruff
+.ruff_cache/
+
+# End of https://www.toptal.com/developers/gitignore/api/python
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9623fa9..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-language: python
-python:
-  - 2.7
-  - 3.5
-  - 3.6
-
-install:
-  - pip install pip --upgrade
-  - pip install setuptools --upgrade
-  - pip install -r tests/requirements.txt
-  - pip install -r requirements.txt
-  - python setup.py install
-
-script: 
-  - flake8 .
-  - python -m nose tests --with-coverage --cover-package tes --cover-min-percentage 80
-
-after_success:
-  - coveralls
-
-notifications:
-  email: false
diff --git a/README.md b/README.md
index 62110a7..356cb5c 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,10 @@ pip install py-tes
 
 ### Example
 
-```
+```python
 import tes
 
+# define task
 task = tes.Task(
     executors=[
         tes.Executor(
@@ -30,8 +31,123 @@ task = tes.Task(
     ]
 )
 
-cli = tes.HTTPClient("http://funnel.example.com", timeout=5)
+# create client
+cli = tes.HTTPClient("https://funnel.example.com", timeout=5)
+
+# access endpoints
+service_info = cli.get_service_info()
 task_id = cli.create_task(task)
-res = cli.get_task(task_id)
+task_info = cli.get_task(task_id, view="BASIC")
 cli.cancel_task(task_id)
+tasks_list = cli.list_tasks(view="MINIMAL")  # default view
+```
+
+### How to...
+
+> Makes use of the objects above...
+
+#### ...export a model to a dictionary
+
+```python
+task_dict = task.as_dict(drop_empty=False)
+```
+
+`task_dict` contents:
+
+```console
+{'id': None, 'state': None, 'name': None, 'description': None, 'inputs': None, 'outputs': None, 'resources': None, 'executors': [{'image': 'alpine', 'command': ['echo', 'hello'], 'workdir': None, 'stdin': None, 'stdout': None, 'stderr': None, 'env': None}], 'volumes': None, 'tags': None, 'logs': None, 'creation_time': None}
+```
+
+#### ...export a model to JSON
+
+```python
+task_json = task.as_json()  # also accepts `drop_empty` arg
+```
+
+`task_json` contents:
+
+```console
+{"executors": [{"image": "alpine", "command": ["echo", "hello"]}]}
+```
+
+#### ...pretty print a model
+
+```python
+print(task.as_json(indent=3))  # keyword args are passed to `json.dumps()`
+```
+
+Output:
+
+```json
+{
+   "executors": [
+      {
+         "image": "alpine",
+         "command": [
+            "echo",
+            "hello"
+         ]
+      }
+   ]
+}
+```
+
+#### ...access a specific task from the task list
+
+```python
+specific_task = tasks_list.tasks[5]
+```
+
+`specific_task` contents:
+
+```console
+Task(id='393K43', state='COMPLETE', name=None, description=None, inputs=None, outputs=None, resources=None, executors=None, volumes=None, tags=None, logs=None, creation_time=None)
+```
+
+#### ...iterate over task list items
+
+```python
+for t in tasks_list[:3]:
+    print(t.as_json(indent=3))
+```
+
+Output:
+
+```console
+{
+   "id": "task_A2GFS4",
+   "state": "RUNNING"
+}
+{
+   "id": "task_O8G1PZ",
+   "state": "CANCELED"
+}
+{
+   "id": "task_W246I6",
+   "state": "COMPLETE"
+}
+```
+
+#### ...instantiate a model from a JSON representation
+
+```python
+task_from_json = tes.client.unmarshal(task_json, tes.Task)
+```
+
+`task_from_json` contents:
+
+```console
+Task(id=None, state=None, name=None, description=None, inputs=None, outputs=None, resources=None, executors=[Executor(image='alpine', command=['echo', 'hello'], workdir=None, stdin=None, stdout=None, stderr=None, env=None)], volumes=None, tags=None, logs=None, creation_time=None)
+```
+
+Which is equivalent to `task`:
+
+```python
+print(task_from_json == task)
+```
+
+Output:
+
+```console
+True
 ```
diff --git a/requirements.txt b/requirements.txt
index 631e096..907fd76 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,3 @@
-attrs>=17.4.0
-future>=0.16.0
-python-dateutil>=2.6.1
-requests>=2.18.2
+attrs>=22.2.0
+python-dateutil>=2.8.2
+requests>=2.28.2
diff --git a/setup.py b/setup.py
index b75e855..6a8542a 100644
--- a/setup.py
+++ b/setup.py
@@ -37,25 +37,20 @@ def find_version(*file_paths):
     url="https://github.com/ohsu-comp-bio/py-tes",
     license="MIT",
     packages=find_packages(),
-    python_requires=">=2.7, <4",
-    install_requires=[
-        "attrs>=17.4.0",
-        "future>=0.16.0",
-        "python-dateutil>=2.6.1",
-        "requests>=2.18.1"
-    ],
+    python_requires=">=3.7, <4",
+    install_requires=read("requirements.txt").splitlines(),
     zip_safe=True,
     classifiers=[
-        "Development Status :: 3 - Alpha",
+        "Development Status :: 4 - Beta",
         "Intended Audience :: Developers",
         "Natural Language :: English",
         "License :: OSI Approved :: MIT License",
         "Topic :: Software Development :: Libraries",
-        "Programming Language :: Python :: 2",
-        "Programming Language :: Python :: 2.7",
         "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.4",
-        "Programming Language :: Python :: 3.5",
-        "Programming Language :: Python :: 3.6"
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9"
+        "Programming Language :: Python :: 3.10"
+        "Programming Language :: Python :: 3.11"
     ],
 )
diff --git a/tes/__init__.py b/tes/__init__.py
index 95b0822..d48b5ce 100644
--- a/tes/__init__.py
+++ b/tes/__init__.py
@@ -1,5 +1,3 @@
-from __future__ import absolute_import
-
 from tes.client import HTTPClient
 from tes.utils import unmarshal
 from tes.models import (
@@ -20,22 +18,22 @@
 )
 
 __all__ = [
-    HTTPClient,
-    unmarshal,
-    Input,
-    Output,
-    Resources,
-    Executor,
-    Task,
-    ExecutorLog,
-    TaskLog,
-    OutputFileLog,
-    CreateTaskResponse,
-    GetTaskRequest,
-    ListTasksRequest,
-    ListTasksResponse,
-    ServiceInfoRequest,
-    ServiceInfo
+    "HTTPClient",
+    "unmarshal",
+    "Input",
+    "Output",
+    "Resources",
+    "Executor",
+    "Task",
+    "ExecutorLog",
+    "TaskLog",
+    "OutputFileLog",
+    "CreateTaskResponse",
+    "GetTaskRequest",
+    "ListTasksRequest",
+    "ListTasksResponse",
+    "ServiceInfoRequest",
+    "ServiceInfo"
 ]
 
 __version__ = "0.4.2"
diff --git a/tes/client.py b/tes/client.py
index 6c2fdf7..2cf8d81 100644
--- a/tes/client.py
+++ b/tes/client.py
@@ -1,13 +1,11 @@
-from __future__ import absolute_import, print_function, unicode_literals
-
 import re
 import requests
 import time
 
 from attr import attrs, attrib
 from attr.validators import instance_of, optional
-from builtins import str
-from requests.utils import urlparse
+from urllib.parse import urlparse
+from typing import Any, Dict, Optional
 
 from tes.models import (Task, ListTasksRequest, ListTasksResponse, ServiceInfo,
                         GetTaskRequest, CancelTaskRequest, CreateTaskResponse,
@@ -21,19 +19,16 @@ def process_url(value):
 
 @attrs
 class HTTPClient(object):
-    url = attrib(converter=process_url)
-    timeout = attrib(default=10, validator=instance_of(int))
-    user = attrib(default=None,
-                  converter=strconv,
-                  validator=optional(instance_of(str)))
-    password = attrib(default=None,
-                      converter=strconv,
-                      validator=optional(instance_of(str)))
-    token = attrib(default=None,
-                   converter=strconv,
-                   validator=optional(instance_of(str)))
-
-    @url.validator
+    url: str = attrib(converter=process_url)
+    timeout: int = attrib(default=10, validator=instance_of(int))
+    user: Optional[str] = attrib(
+        default=None, converter=strconv, validator=optional(instance_of(str)))
+    password: Optional[str] = attrib(
+        default=None, converter=strconv, validator=optional(instance_of(str)))
+    token: Optional[str] = attrib(
+        default=None, converter=strconv, validator=optional(instance_of(str)))
+
+    @url.validator  # type: ignore
     def __check_url(self, attribute, value):
         u = urlparse(value)
         if u.scheme not in ["http", "https"]:
@@ -42,48 +37,51 @@ def __check_url(self, attribute, value):
                 % ("http", "https")
             )
 
-    def get_service_info(self):
-        kwargs = self._request_params()
-        response = requests.get(
-            "%s/v1/tasks/service-info" % (self.url),
+    def get_service_info(self) -> ServiceInfo:
+        kwargs: Dict[str, Any] = self._request_params()
+        response: requests.Response = requests.get(
+            f"{self.url}/v1/tasks/service-info",
             **kwargs)
         response.raise_for_status()
         return unmarshal(response.json(), ServiceInfo)
 
-    def create_task(self, task):
+    def create_task(self, task: Task) -> CreateTaskResponse:
         if isinstance(task, Task):
             msg = task.as_json()
         else:
             raise TypeError("Expected Task instance")
 
-        kwargs = self._request_params(data=msg)
-        response = requests.post(
-            "%s/v1/tasks" % (self.url),
+        kwargs: Dict[str, Any] = self._request_params(data=msg)
+        response: requests.Response = requests.post(
+            f"{self.url}/v1/tasks",
             **kwargs
         )
         response.raise_for_status()
         return unmarshal(response.json(), CreateTaskResponse).id
 
-    def get_task(self, task_id, view="BASIC"):
-        req = GetTaskRequest(task_id, view)
-        payload = {"view": req.view}
-        kwargs = self._request_params(params=payload)
-        response = requests.get(
-            "%s/v1/tasks/%s" % (self.url, req.id),
+    def get_task(self, task_id: str, view: str = "BASIC") -> Task:
+        req: GetTaskRequest = GetTaskRequest(task_id, view)
+        payload: Dict[str, Optional[str]] = {"view": req.view}
+        kwargs: Dict[str, Any] = self._request_params(params=payload)
+        response: requests.Response = requests.get(
+            f"{self.url}/v1/tasks/{req.id}",
             **kwargs)
         response.raise_for_status()
         return unmarshal(response.json(), Task)
 
-    def cancel_task(self, task_id):
-        req = CancelTaskRequest(task_id)
-        kwargs = self._request_params()
-        response = requests.post(
-            "%s/v1/tasks/%s:cancel" % (self.url, req.id),
+    def cancel_task(self, task_id: str) -> None:
+        req: CancelTaskRequest = CancelTaskRequest(task_id)
+        kwargs: Dict[str, Any] = self._request_params()
+        response: requests.Response = requests.post(
+            f"{self.url}/v1/tasks/{req.id}:cancel",
             **kwargs)
         response.raise_for_status()
-        return
+        return None
 
-    def list_tasks(self, view="MINIMAL", page_size=None, page_token=None):
+    def list_tasks(
+        self, view: str = "MINIMAL", page_size: Optional[int] = None,
+        page_token: Optional[str] = None
+    ) -> ListTasksResponse:
         req = ListTasksRequest(
             view=view,
             page_size=page_size,
@@ -91,47 +89,49 @@ def list_tasks(self, view="MINIMAL", page_size=None, page_token=None):
             name_prefix=None,
             project=None
         )
-        msg = req.as_dict()
+        msg: Dict = req.as_dict()
 
-        kwargs = self._request_params(params=msg)
-        response = requests.get(
-            "%s/v1/tasks" % (self.url),
+        kwargs: Dict[str, Any] = self._request_params(params=msg)
+        response: requests.Response = requests.get(
+            f"{self.url}/v1/tasks",
             **kwargs)
         response.raise_for_status()
         return unmarshal(response.json(), ListTasksResponse)
 
-    def wait(self, task_id, timeout=None):
-        def check_success(data):
+    def wait(self, task_id: str, timeout=None) -> Task:
+        def check_success(data: Task) -> bool:
             return data.state not in ["QUEUED", "RUNNING", "INITIALIZING"]
 
         max_time = time.time() + timeout if timeout else None
 
+        response: Optional[Task] = None
         while True:
             try:
                 response = self.get_task(task_id, "MINIMAL")
             except Exception:
-                response = None
+                pass
 
             if response is not None:
                 if check_success(response):
                     return response
 
-            if max_time is not None and time.time() >= max_time:
-                raise TimeoutError("last_response: %s" % (response.as_dict()))
+                if max_time is not None and time.time() >= max_time:
+                    raise TimeoutError("last_response: {response.as_dict()}")
             time.sleep(0.5)
 
-    def _request_params(self, data=None, params=None):
-        kwargs = {'timeout': self.timeout}
-
+    def _request_params(
+        self, data: Optional[str] = None,
+        params: Optional[Dict] = None
+    ) -> Dict[str, Any]:
+        kwargs: Dict[str, Any] = {}
+        kwargs['timeout'] = self.timeout
+        kwargs['headers'] = {}
+        kwargs['headers']['Content-type'] = 'application/json'
+        kwargs['auth'] = (self.user, self.password)
         if data:
             kwargs['data'] = data
         if params:
             kwargs['params'] = params
         if self.token:
-            kwargs['headers'] = {'Content-type': 'application/json',
-                                 'Authorization': 'Bearer ' + self.token}
-        else:
-            kwargs['headers'] = {'Content-type': 'application/json'}
-            kwargs['auth'] = (self.user, self.password)
-
+            kwargs['headers']['Authorization'] = f"Bearer {self.token}"
         return kwargs
diff --git a/tes/models.py b/tes/models.py
index f0f50c2..8550291 100644
--- a/tes/models.py
+++ b/tes/models.py
@@ -3,17 +3,16 @@
 import dateutil.parser
 import json
 import os
-import six
 
 from attr import asdict, attrs, attrib
 from attr.validators import instance_of, optional, in_
-from builtins import str
 from datetime import datetime
+from typing import Any, Dict, List, Optional, Tuple, Type, Union
 
 
 @attrs
 class _ListOfValidator(object):
-    type = attrib()
+    type: Type = attrib()
 
     def __call__(self, inst, attr, value):
         """
@@ -21,27 +20,20 @@ def __call__(self, inst, attr, value):
         """
         if not all([isinstance(n, self.type) for n in value]):
             raise TypeError(
-                "'{name}' must be a list of {type!r} (got {value!r} that is a "
-                "list of {actual!r})."
-                .format(name=attr.name,
-                        type=self.type,
-                        actual=value[0].__class__,
-                        value=value),
+                "'{attr.name}' must be a list of {self.type!r} (got {value!r} "
+                "that is a list of {values[0].__class__!r}).",
                 attr, self.type, value,
             )
 
-    def __repr__(self):
-        return (
-            "<instance_of validator for type {type!r}>"
-            .format(type=self.type)
-        )
+    def __repr__(self) -> str:
+        return f"<instance_of validator for type {self.type!r}>"
 
 
-def list_of(type):
-    return _ListOfValidator(type)
+def list_of(_type: Any) -> _ListOfValidator:
+    return _ListOfValidator(_type)
 
 
-def _drop_none(obj):
+def _drop_none(obj: Any) -> Any:
     if isinstance(obj, (list, tuple, set)):
         return type(obj)(_drop_none(x) for x in obj if x is not None)
     elif isinstance(obj, dict):
@@ -53,13 +45,13 @@ def _drop_none(obj):
         return obj
 
 
-def strconv(value):
+def strconv(value: Any) -> Any:
     if isinstance(value, (tuple, list)):
-        if all([isinstance(n, six.string_types) for n in value]):
+        if all([isinstance(n, str) for n in value]):
             return [str(n) for n in value]
         else:
             return value
-    elif isinstance(value, six.string_types):
+    elif isinstance(value, str):
         return str(value)
     else:
         return value
@@ -67,19 +59,19 @@ def strconv(value):
 
 # since an int64 value is encoded as a string in json we need to handle
 # conversion
-def int64conv(value):
+def int64conv(value: Optional[str]) -> Optional[int]:
     if value is not None:
         return int(value)
     return value
 
 
-def timestampconv(value):
-    if isinstance(value, six.string_types):
+def timestampconv(value: Optional[str]) -> Optional[datetime]:
+    if value is not None:
         return dateutil.parser.parse(value)
     return value
 
 
-def datetime_json_handler(x):
+def datetime_json_handler(x: Any) -> str:
     if isinstance(x, datetime):
         return x.isoformat()
     raise TypeError("Unknown type")
@@ -88,212 +80,213 @@ def datetime_json_handler(x):
 @attrs
 class Base(object):
 
-    def as_dict(self, drop_empty=True):
+    def as_dict(self, drop_empty: bool = True) -> Dict[str, Any]:
         obj = asdict(self)
         if drop_empty:
             return _drop_none(obj)
         return obj
 
-    def as_json(self, drop_empty=True):
+    def as_json(self, drop_empty: bool = True, **kwargs) -> str:
         return json.dumps(
             self.as_dict(drop_empty),
-            default=datetime_json_handler
+            default=datetime_json_handler,
+            **kwargs
         )
 
 
 @attrs
 class Input(Base):
-    url = attrib(
+    url: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    path = attrib(
+    path: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    type = attrib(
+    type: str = attrib(
         default="FILE", validator=in_(["FILE", "DIRECTORY"])
     )
-    name = attrib(
+    name: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    description = attrib(
+    description: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    content = attrib(
+    content: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
 
 
 @attrs
 class Output(Base):
-    url = attrib(
+    url: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    path = attrib(
+    path: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    type = attrib(
+    type: str = attrib(
         default="FILE", validator=in_(["FILE", "DIRECTORY"])
     )
-    name = attrib(
+    name: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    description = attrib(
+    description: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
 
 
 @attrs
 class Resources(Base):
-    cpu_cores = attrib(
+    cpu_cores: Optional[int] = attrib(
         default=None, validator=optional(instance_of(int))
     )
-    ram_gb = attrib(
+    ram_gb: Optional[Union[float, int]] = attrib(
         default=None, validator=optional(instance_of((float, int)))
     )
-    disk_gb = attrib(
+    disk_gb: Optional[Union[float, int]] = attrib(
         default=None, validator=optional(instance_of((float, int)))
     )
-    preemptible = attrib(
+    preemptible: Optional[bool] = attrib(
         default=None, validator=optional(instance_of(bool))
     )
-    zones = attrib(
+    zones: Optional[List[str]] = attrib(
         default=None, converter=strconv, validator=optional(list_of(str))
     )
 
 
 @attrs
 class Executor(Base):
-    image = attrib(
+    image: str = attrib(
         converter=strconv, validator=instance_of(str)
     )
-    command = attrib(
+    command: List[str] = attrib(
         converter=strconv, validator=list_of(str)
     )
-    workdir = attrib(
+    workdir: str = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    stdin = attrib(
+    stdin: str = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    stdout = attrib(
+    stdout: str = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    stderr = attrib(
+    stderr: str = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    env = attrib(
+    env: Optional[Dict] = attrib(
         default=None, validator=optional(instance_of(dict))
     )
 
 
 @attrs
 class ExecutorLog(Base):
-    start_time = attrib(
+    start_time: datetime = attrib(
         default=None,
         converter=timestampconv,
         validator=optional(instance_of(datetime))
     )
-    end_time = attrib(
+    end_time: datetime = attrib(
         default=None,
         converter=timestampconv,
         validator=optional(instance_of(datetime))
     )
-    stdout = attrib(
+    stdout: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    stderr = attrib(
+    stderr: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    exit_code = attrib(
+    exit_code: Optional[int] = attrib(
         default=None, validator=optional(instance_of(int))
     )
 
 
 @attrs
 class OutputFileLog(Base):
-    url = attrib(
+    url: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    path = attrib(
+    path: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    size_bytes = attrib(
+    size_bytes: Optional[int] = attrib(
         default=None, converter=int64conv, validator=optional(instance_of(int))
     )
 
 
 @attrs
 class TaskLog(Base):
-    start_time = attrib(
+    start_time: datetime = attrib(
         default=None,
         converter=timestampconv,
         validator=optional(instance_of(datetime))
     )
-    end_time = attrib(
+    end_time: datetime = attrib(
         default=None,
         converter=timestampconv,
         validator=optional(instance_of(datetime))
     )
-    metadata = attrib(
+    metadata: Optional[Dict] = attrib(
         default=None, validator=optional(instance_of(dict))
     )
-    logs = attrib(
+    logs: Optional[List[ExecutorLog]] = attrib(
         default=None, validator=optional(list_of(ExecutorLog))
     )
-    outputs = attrib(
+    outputs: Optional[List[OutputFileLog]] = attrib(
         default=None, validator=optional(list_of(OutputFileLog))
     )
-    system_logs = attrib(
+    system_logs: Optional[List[str]] = attrib(
         default=None, validator=optional(list_of(str))
     )
 
 
 @attrs
 class Task(Base):
-    id = attrib(
+    id: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    state = attrib(
+    state: Optional[str] = attrib(
         default=None,
         validator=optional(in_(
             ["UNKNOWN", "QUEUED", "INITIALIZING", "RUNNING", "COMPLETE",
              "CANCELED", "EXECUTOR_ERROR", "SYSTEM_ERROR"]
         ))
     )
-    name = attrib(
+    name: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    description = attrib(
+    description: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    inputs = attrib(
+    inputs: Optional[List[Input]] = attrib(
         default=None, validator=optional(list_of(Input))
     )
-    outputs = attrib(
+    outputs: Optional[List[Output]] = attrib(
         default=None, validator=optional(list_of(Output))
     )
-    resources = attrib(
+    resources: Optional[Resources] = attrib(
         default=None, validator=optional(instance_of(Resources))
     )
-    executors = attrib(
+    executors: Optional[List[Executor]] = attrib(
         default=None, validator=optional(list_of(Executor))
     )
-    volumes = attrib(
+    volumes: Optional[List[str]] = attrib(
         default=None, validator=optional(list_of(str))
     )
-    tags = attrib(
+    tags: Optional[Dict] = attrib(
         default=None, validator=optional(instance_of(dict))
     )
-    logs = attrib(
+    logs: Optional[List[TaskLog]] = attrib(
         default=None, validator=optional(list_of(TaskLog))
     )
-    creation_time = attrib(
+    creation_time: datetime = attrib(
         default=None,
         converter=timestampconv,
         validator=optional(instance_of(datetime))
     )
 
-    def is_valid(self):
+    def is_valid(self) -> Tuple[bool, Union[None, TypeError]]:
         errs = []
         if self.executors is None or len(self.executors) == 0:
             errs.append("Must provide one or more Executors")
@@ -313,7 +306,7 @@ def is_valid(self):
                     if not os.path.isabs(e.stderr):
                         errs.append("Executor stderr must be an absolute path")
                 if e.env is not None:
-                    for k, v in self.executors.env:
+                    for k, v in e.env:
                         if not isinstance(k, str) and not isinstance(k, str):
                             errs.append(
                                 "Executor env keys and values must be StrType"
@@ -336,7 +329,7 @@ def is_valid(self):
                     errs.append("Output url must be provided")
                 if o.path is None:
                     errs.append("Output path must be provided")
-                elif not os.path.isabs(i.path):
+                elif not os.path.isabs(o.path):
                     errs.append("Output path must be absolute")
 
         if self.volumes is not None:
@@ -360,17 +353,17 @@ def is_valid(self):
 
 @attrs
 class GetTaskRequest(Base):
-    id = attrib(
+    id: str = attrib(
         converter=strconv, validator=instance_of(str)
     )
-    view = attrib(
+    view: Optional[str] = attrib(
         default=None, validator=optional(in_(["MINIMAL", "BASIC", "FULL"]))
     )
 
 
 @attrs
 class CreateTaskResponse(Base):
-    id = attrib(
+    id: str = attrib(
         converter=strconv, validator=instance_of(str)
     )
 
@@ -382,20 +375,20 @@ class ServiceInfoRequest(Base):
 
 @attrs
 class ServiceInfo(Base):
-    name = attrib(
+    name: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    doc = attrib(
+    doc: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    storage = attrib(
+    storage: Optional[List[str]] = attrib(
         default=None, converter=strconv, validator=optional(list_of(str))
     )
 
 
 @attrs
 class CancelTaskRequest(Base):
-    id = attrib(
+    id: str = attrib(
         converter=strconv, validator=instance_of(str)
     )
 
@@ -407,28 +400,28 @@ class CancelTaskResponse(Base):
 
 @attrs
 class ListTasksRequest(Base):
-    project = attrib(
+    project: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    name_prefix = attrib(
+    name_prefix: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    page_size = attrib(
+    page_size: Optional[int] = attrib(
         default=None, validator=optional(instance_of(int))
     )
-    page_token = attrib(
+    page_token: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
-    view = attrib(
+    view: Optional[str] = attrib(
         default=None, validator=optional(in_(["MINIMAL", "BASIC", "FULL"]))
     )
 
 
 @attrs
 class ListTasksResponse(Base):
-    tasks = attrib(
+    tasks: Optional[List[Task]] = attrib(
         default=None, validator=optional(list_of(Task))
     )
-    next_page_token = attrib(
+    next_page_token: Optional[str] = attrib(
         default=None, converter=strconv, validator=optional(instance_of(str))
     )
diff --git a/tes/utils.py b/tes/utils.py
index dfec1ac..199543b 100644
--- a/tes/utils.py
+++ b/tes/utils.py
@@ -1,8 +1,8 @@
-from __future__ import absolute_import, print_function, unicode_literals
-
 import json
 import re
 
+from typing import Any, Dict, Type
+
 from tes.models import (Task, Input, Output, Resources, Executor,
                         TaskLog, ExecutorLog, OutputFileLog)
 
@@ -11,7 +11,7 @@
 all_cap_re = re.compile('([a-z0-9])([A-Z])')
 
 
-def camel_to_snake(name):
+def camel_to_snake(name: str) -> str:
     s1 = first_cap_re.sub(r'\1_\2', name)
     return all_cap_re.sub(r'\1_\2', s1).lower()
 
@@ -26,15 +26,17 @@ def __init__(self, *args, **kwargs):
         Exception.__init__(self, *args, **kwargs)
 
 
-def unmarshal(j, o, convert_camel_case=True):
+def unmarshal(j: Any, o: Type, convert_camel_case=True) -> Any:
     if isinstance(j, str):
         m = json.loads(j)
     elif isinstance(j, dict):
         m = j
+    elif j is None:
+        return None
     else:
-        raise TypeError("j must be a str or dict")
+        raise TypeError("j must be a str, a dict or None")
 
-    d = {}
+    d: Dict[str, Any] = {}
     if convert_camel_case:
         for k, v in m.items():
             d[camel_to_snake(k)] = v
@@ -61,7 +63,7 @@ def unmarshal(j, o, convert_camel_case=True):
         }
     }
 
-    def _unmarshal(v, obj):
+    def _unmarshal(v: Any, obj: Type) -> Any:
         if isinstance(v, list):
             field = []
             for item in v:


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 68873ce..e01138f 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,26 +1,35 @@
 name: py-test_file
 
-on: [ pull_request ]
+on: [pull_request]
 
 jobs:
-
   test:
     runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+      matrix:
+        version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
+
     steps:
-    - name: Check out code
-      uses: actions/checkout@v2
+      - name: Check out code
+        uses: actions/checkout@v3
 
-    - name: Requirements
-      run: pip install -r requirements.txt
+      - name: Set up Python
+        uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.version }}
 
-    - name: Test Requirements
-      run: pip install -r tests/requirements.txt
+      - name: Install requirements
+        run: |
+          pip install -r requirements.txt
+          pip install -r tests/requirements.txt
 
-    - name: Install
-      run: python setup.py install --user
+      - name: Install app
+        run: pip install .
 
-    - name: Flake
-      run: flake8 .
+      - name: Lint with Flake8
+        run: flake8 .
 
-    - name: Test
-      run: python -m nose tests --with-coverage --cover-package tes --cover-min-percentage 80
+      - name: Run unit tests
+        run: coverage run --source tes -m pytest -W ignore::DeprecationWarning
diff --git a/tests/requirements.txt b/tests/requirements.txt
index b361eb6..45208ca 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,5 +1,5 @@
-coverage==4.5.4
-coveralls>=1.1
-flake8>=3.3.0
-nose>=1.3.7
-requests_mock>=1.3.0
+coverage>=6.5.0
+coveralls>=3.3.1
+flake8>=5.0.4
+pytest>=7.2.1
+requests_mock>=1.10.0