ciocore 5.1.1__py2.py3-none-any.whl → 10.0.0b3__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. ciocore/VERSION +1 -1
  2. ciocore/__init__.py +23 -1
  3. ciocore/api_client.py +655 -160
  4. ciocore/auth/__init__.py +5 -3
  5. ciocore/cli.py +501 -0
  6. ciocore/common.py +15 -13
  7. ciocore/conductor_submit.py +77 -60
  8. ciocore/config.py +127 -13
  9. ciocore/data.py +162 -77
  10. ciocore/docsite/404.html +746 -0
  11. ciocore/docsite/apidoc/api_client/index.html +3605 -0
  12. ciocore/docsite/apidoc/apidoc/index.html +909 -0
  13. ciocore/docsite/apidoc/config/index.html +1652 -0
  14. ciocore/docsite/apidoc/data/index.html +1553 -0
  15. ciocore/docsite/apidoc/hardware_set/index.html +2460 -0
  16. ciocore/docsite/apidoc/package_environment/index.html +1507 -0
  17. ciocore/docsite/apidoc/package_tree/index.html +2386 -0
  18. ciocore/docsite/assets/_mkdocstrings.css +16 -0
  19. ciocore/docsite/assets/images/favicon.png +0 -0
  20. ciocore/docsite/assets/javascripts/bundle.471ce7a9.min.js +29 -0
  21. ciocore/docsite/assets/javascripts/bundle.471ce7a9.min.js.map +7 -0
  22. ciocore/docsite/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  23. ciocore/docsite/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  24. ciocore/docsite/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  25. ciocore/docsite/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  26. ciocore/docsite/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  27. ciocore/docsite/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  28. ciocore/docsite/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  29. ciocore/docsite/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  30. ciocore/docsite/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  31. ciocore/docsite/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  32. ciocore/docsite/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  33. ciocore/docsite/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  34. ciocore/docsite/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  35. ciocore/docsite/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  36. ciocore/docsite/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  37. ciocore/docsite/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  38. ciocore/docsite/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  39. ciocore/docsite/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  40. ciocore/docsite/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  41. ciocore/docsite/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  42. ciocore/docsite/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  43. ciocore/docsite/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  44. ciocore/docsite/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  45. ciocore/docsite/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  46. ciocore/docsite/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  47. ciocore/docsite/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  48. ciocore/docsite/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  49. ciocore/docsite/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  50. ciocore/docsite/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  51. ciocore/docsite/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  52. ciocore/docsite/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  53. ciocore/docsite/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  54. ciocore/docsite/assets/javascripts/lunr/tinyseg.js +206 -0
  55. ciocore/docsite/assets/javascripts/lunr/wordcut.js +6708 -0
  56. ciocore/docsite/assets/javascripts/workers/search.b8dbb3d2.min.js +42 -0
  57. ciocore/docsite/assets/javascripts/workers/search.b8dbb3d2.min.js.map +7 -0
  58. ciocore/docsite/assets/stylesheets/main.3cba04c6.min.css +1 -0
  59. ciocore/docsite/assets/stylesheets/main.3cba04c6.min.css.map +1 -0
  60. ciocore/docsite/assets/stylesheets/palette.06af60db.min.css +1 -0
  61. ciocore/docsite/assets/stylesheets/palette.06af60db.min.css.map +1 -0
  62. ciocore/docsite/cmdline/docs/index.html +871 -0
  63. ciocore/docsite/cmdline/downloader/index.html +934 -0
  64. ciocore/docsite/cmdline/packages/index.html +878 -0
  65. ciocore/docsite/cmdline/uploader/index.html +995 -0
  66. ciocore/docsite/how-to-guides/index.html +869 -0
  67. ciocore/docsite/index.html +895 -0
  68. ciocore/docsite/logo.png +0 -0
  69. ciocore/docsite/objects.inv +0 -0
  70. ciocore/docsite/search/search_index.json +1 -0
  71. ciocore/docsite/sitemap.xml +3 -0
  72. ciocore/docsite/sitemap.xml.gz +0 -0
  73. ciocore/docsite/stylesheets/extra.css +26 -0
  74. ciocore/docsite/stylesheets/tables.css +167 -0
  75. ciocore/downloader/base_downloader.py +644 -0
  76. ciocore/downloader/download_runner_base.py +47 -0
  77. ciocore/downloader/job_downloader.py +119 -0
  78. ciocore/{downloader.py → downloader/legacy_downloader.py} +12 -9
  79. ciocore/downloader/log.py +73 -0
  80. ciocore/downloader/logging_download_runner.py +87 -0
  81. ciocore/downloader/perpetual_downloader.py +63 -0
  82. ciocore/downloader/registry.py +97 -0
  83. ciocore/downloader/reporter.py +135 -0
  84. ciocore/exceptions.py +8 -2
  85. ciocore/file_utils.py +51 -50
  86. ciocore/hardware_set.py +449 -0
  87. ciocore/loggeria.py +89 -20
  88. ciocore/package_environment.py +110 -48
  89. ciocore/package_query.py +182 -0
  90. ciocore/package_tree.py +319 -258
  91. ciocore/retry.py +0 -0
  92. ciocore/uploader/_uploader.py +547 -364
  93. ciocore/uploader/thread_queue_job.py +176 -0
  94. ciocore/uploader/upload_stats/__init__.py +3 -4
  95. ciocore/uploader/upload_stats/stats_formats.py +10 -4
  96. ciocore/validator.py +34 -2
  97. ciocore/worker.py +174 -151
  98. ciocore-10.0.0b3.dist-info/METADATA +928 -0
  99. ciocore-10.0.0b3.dist-info/RECORD +128 -0
  100. {ciocore-5.1.1.dist-info → ciocore-10.0.0b3.dist-info}/WHEEL +1 -1
  101. ciocore-10.0.0b3.dist-info/entry_points.txt +2 -0
  102. tests/instance_type_fixtures.py +175 -0
  103. tests/package_fixtures.py +205 -0
  104. tests/test_api_client.py +297 -12
  105. tests/test_base_downloader.py +104 -0
  106. tests/test_cli.py +149 -0
  107. tests/test_common.py +1 -7
  108. tests/test_config.py +40 -18
  109. tests/test_data.py +162 -173
  110. tests/test_downloader.py +118 -0
  111. tests/test_hardware_set.py +139 -0
  112. tests/test_job_downloader.py +213 -0
  113. tests/test_package_query.py +38 -0
  114. tests/test_package_tree.py +91 -291
  115. tests/test_submit.py +44 -18
  116. tests/test_uploader.py +1 -4
  117. ciocore/__about__.py +0 -10
  118. ciocore/cli/conductor.py +0 -191
  119. ciocore/compat.py +0 -15
  120. ciocore-5.1.1.data/scripts/conductor +0 -19
  121. ciocore-5.1.1.data/scripts/conductor.bat +0 -13
  122. ciocore-5.1.1.dist-info/METADATA +0 -408
  123. ciocore-5.1.1.dist-info/RECORD +0 -47
  124. tests/mocks/api_client_mock.py +0 -51
  125. /ciocore/{cli → downloader}/__init__.py +0 -0
  126. {ciocore-5.1.1.dist-info → ciocore-10.0.0b3.dist-info}/top_level.txt +0 -0
@@ -1,26 +1,111 @@
1
+ """
2
+ Manage environment variables for both Windows and Linux render nodes.
3
+ """
4
+
1
5
  class PackageEnvironment(object):
2
- """Encapsulate the submission environment vars.
3
6
 
4
- Handle building up the env with a single call to extend()
5
- """
6
7
 
7
8
  def __init__(self, env_list=None, platform=None):
8
- """Initialize, possibly with a package or list of variables.
9
-
10
- On the first add, if the package contains a platform field (which it will) or in the case of a list, it is
11
- accompanied by a platform, then we use it, otherwise we default to linux. It is an error to
12
- try to change platform after the first add.
9
+ """
10
+ Encapsulate a list of environment variables.
13
11
 
14
- The merge policy is effectively set the first time a variable is declared and used for all
15
- subsequent amendments. Policy is implied by the storage data type. String=exclusive,
16
- List=append. User cannot change the policy once a variable has been declared.
12
+ Typically, one would initialize a PackageEnvironment with a package, and then modify by adding more packages or lists of variables. Extra variables can be added by the customer, or programmatically such as during asset scraping.
17
13
 
14
+ Args:
15
+ env_list (object|list): An object that provides a list of dictionaries with properties: `name`, `value`, and `merge_policy`.
16
+ platform (str): If the env_list is a regular list, then this is required.
17
+
18
+ Args are delegated to [extend()](/package_environment/#ciocore.package_environment.PackageEnvironment.extend).
18
19
  """
19
20
  self.platform = None
20
21
  self._env = {}
21
- # delegete all initialization to extend()
22
+
22
23
  self.extend(env_list, platform)
23
24
 
25
+
26
+ def extend(self, env_list, platform=None):
27
+ """
28
+ Extend the Package environment with the given variable specifications.
29
+
30
+ Args:
31
+ env_list (object|list): Either:
32
+ * A list of dictionaries with properties: `name`, `value`, and `merge_policy`.
33
+ * An object with an `environment` key that contains a list of the dictionaries described above. The latter is the structure of a package. Therefore we can initialize or extend a PackageEnvironment with a package.
34
+ platform (str): Defaults to `None`. If env_list is a package, then the platform is taken from the package and the `platform` keyword is ignored. If env_list is a list, then if this is the first add, a platform should be specified, otherwise it will default to linux.
35
+
36
+ The first time data is added to a PackageEnvironment, the platform is set in stone. Subsequent `adds` that try to change the platform are considered an error.
37
+
38
+ Each variable to be added specifies a merge_policy: `append`, `prepend`, or `exclusive` `append` and `prepend` can be thought of as lists= types. Once an individual variable has been initialized as a list, it can't be changed to `exclusive`. This means:
39
+
40
+ 1. It's not possible to overwrite variables that have been added as `append` or `prepend`.
41
+ 2. Exclusive variables are always overwritten by subsequent adds.
42
+
43
+ Raises:
44
+ ValueError: Either an attempt to change the platform once initialized, or an invalid merge policy.
45
+
46
+
47
+ Example:
48
+ >>> from ciocore import api_client, package_tree, package_environment
49
+ >>> packages = api_client.request_software_packages()
50
+ >>> pt = package_tree.PackageTree(packages, product="cinema4d")
51
+ >>> one_dcc_name = pt.supported_host_names()[0]
52
+ cinema4d 21.209.RB305619 linux
53
+
54
+ >>> pkg = pt.find_by_name(one_dcc_name)
55
+ >>> pe = package_environment.PackageEnvironment(pkg)
56
+ >>> print(dict(pe))
57
+ {
58
+ "PATH": "/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin",
59
+ "g_licenseServerRLM": "conductor-rlm:6112",
60
+ "LD_LIBRARY_PATH": "/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/lib64:/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin/resource/modules/python/libs/linux64/python.linux64.framework/lib64:/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin/resource/modules/embree.module/libs/linux64",
61
+ "PYTHONPATH": "/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin/resource/modules/python/libs/linux64/python.linux64.framework/lib64/python2.7/lib-dynload",
62
+ }
63
+
64
+ >>> extra_env = [
65
+ {"name":"PATH", "value": "/my/custom/scripts", "merge_policy":"append"},
66
+ {"name":"DEBUG_MODE", "value": "1", "merge_policy":"exclusive"}
67
+ ]
68
+ >>> pe.extend(extra_env)
69
+ >>> print(dict(pe))
70
+ {
71
+ "PATH": "/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin:/my/custom/scripts",
72
+ "g_licenseServerRLM": "conductor-rlm:6112",
73
+ "LD_LIBRARY_PATH": "/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/lib64:/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin/resource/modules/python/libs/linux64/python.linux64.framework/lib64:/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin/resource/modules/embree.module/libs/linux64",
74
+ "PYTHONPATH": "/opt/maxon/cinema4d/21/cinema4d21.209vRB305619/bin/resource/modules/python/libs/linux64/python.linux64.framework/lib64/python2.7/lib-dynload",
75
+ "DEBUG_MODE": "1",
76
+ }
77
+ """
78
+
79
+ if not env_list:
80
+ return
81
+
82
+ try:
83
+ others = env_list["environment"]
84
+ requested_platform = env_list.get("platform")
85
+ except TypeError:
86
+ others = env_list
87
+ requested_platform = platform
88
+
89
+ if not self.platform:
90
+ self.platform = requested_platform or "linux"
91
+ elif requested_platform and requested_platform != self.platform:
92
+ raise ValueError("Can't change platform once initialized.")
93
+
94
+ for var in others:
95
+ name = var["name"]
96
+ value = var["value"]
97
+ policy = var["merge_policy"]
98
+ if policy not in ["append", "prepend", "exclusive"]:
99
+ raise ValueError("Unexpected merge policy: %s" % policy)
100
+
101
+ if policy == "append":
102
+ self._append(name, value)
103
+ elif policy == "prepend":
104
+ self._prepend(name, value)
105
+ else:
106
+ self._set(name, value)
107
+
108
+
24
109
  def _set(self, name, value):
25
110
  """Set the value of an exclusive variable.
26
111
 
@@ -50,45 +135,22 @@ class PackageEnvironment(object):
50
135
  self._env[name] = []
51
136
  self._env[name].append(value)
52
137
 
53
- def extend(self, env_list, platform=None):
54
- """Extend with the given variable specifications.
138
+ def _prepend(self, name, value):
139
+ """Set the value of an append/prepend variable.
55
140
 
56
- env_list is either:
57
- A list of objects OR an object with an "environment" key that contains a
58
- list of objects. The latter is the structure of a package. Therefore we can initialize or extend
59
- a PackageEnvironment with a package.
141
+ Can be appended to with subsequent adds.
60
142
 
61
- Each of these objects in the list has a name, a value, and a merge_policy. One by one
62
- they are added according to their merge policy. See _set and _append above.
143
+ It is an error if the variable has already been declared with policy=exclusive.
63
144
  """
64
-
65
- if not env_list:
66
- return
67
-
68
- try:
69
- others = env_list["environment"]
70
- requested_platform = env_list.get("platform")
71
- except TypeError:
72
- others = env_list
73
- requested_platform = platform
74
-
75
- if not self.platform:
76
- self.platform = requested_platform or "linux"
77
- elif requested_platform and requested_platform != self.platform:
78
- raise ValueError("Can't change platform once initialized.")
79
-
80
- for var in others:
81
- name = var["name"]
82
- value = var["value"]
83
- policy = var["merge_policy"]
84
- if policy not in ["append", "exclusive"]:
85
- raise ValueError("Unexpected merge policy: %s" % policy)
86
-
87
- if policy == "append":
88
- self._append(name, value)
89
- else:
90
- self._set(name, value)
91
-
145
+ if self._env.get(name):
146
+ if not isinstance(self._env[name], list):
147
+ raise ValueError(
148
+ "Can't change merge policy for '{}' from 'exclusive' to 'prepend'.".format(name)
149
+ )
150
+ else:
151
+ self._env[name] = []
152
+ self._env[name].insert(0, value)
153
+
92
154
  def __iter__(self):
93
155
  """Cast the object as a dict."""
94
156
  sep = ";" if self.platform == "windows" else ":"
@@ -0,0 +1,182 @@
1
+ """
2
+ Generate markdown from the software packages list.
3
+ """
4
+ import os
5
+ import sys
6
+ import json
7
+ from ciocore.package_tree import PackageTree
8
+ from ciocore import api_client
9
+ import markdown
10
+ import io
11
+ import tempfile
12
+ import webbrowser
13
+
14
+ PURE = """
15
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css" integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ """
18
+
19
+ def green(rhs):
20
+ return "\033[92m{}\033[0m".format(rhs)
21
+
22
+
23
+ def red(rhs):
24
+ return "\033[91m{}\033[0m".format(rhs)
25
+
26
+
27
+ def blue(rhs):
28
+ return "\033[94m{}\033[0m".format(rhs)
29
+
30
+
31
+ def magenta(rhs):
32
+ return "\033[95m{}\033[0m".format(rhs)
33
+
34
+
35
+ def raw(rhs, stream):
36
+ stream.write("{}\n\n".format(rhs))
37
+
38
+
39
+ def d(n, rhs, stream):
40
+ """Indent with dashes"""
41
+ stream.write("{} {}\n".format("-" * n, rhs))
42
+
43
+
44
+ def hr(stream):
45
+ stream.write("---\n\n")
46
+
47
+
48
+ def h(n, rhs, stream):
49
+ stream.write("{} {}\n\n".format("#" * n, rhs))
50
+
51
+
52
+ def plugin_table_header(stream):
53
+ stream.write(
54
+ '|<div style="width:150px">Plugin</div> |<div style="min-width:400px">Versions</div>|\n|:------------|:-------------|\n'
55
+ )
56
+
57
+
58
+ def plugin_table_row(plugin, versions, stream):
59
+ stream.write("|{}|{}|\n".format(plugin, versions))
60
+
61
+
62
+ def write_markdown(hostnames, tree_data, platform, stream):
63
+ """
64
+ Write the tree of packages in Markdown.
65
+
66
+ Use this to generate docs for the Conductor mkdocs site.
67
+ """
68
+ if not hostnames:
69
+ return
70
+ h(2, "{} Software".format(platform.capitalize()), stream)
71
+ last_hostgroup = None
72
+ for hostname in hostnames:
73
+ display_hostname = " ".join(hostname.split()[:2])
74
+ hostgroup = hostname.split(" ")[0]
75
+ stream.write("\n")
76
+ if not hostgroup == last_hostgroup:
77
+ hr(stream)
78
+ h(3, hostgroup, stream)
79
+ h(4, display_hostname, stream)
80
+ last_hostgroup = hostgroup
81
+ plugins = tree_data.supported_plugins(hostname)
82
+ if plugins:
83
+ plugin_table_header(stream)
84
+ for plugin in plugins:
85
+ plugin_table_row(
86
+ plugin["plugin"], ", ".join(plugin["versions"]), stream
87
+ )
88
+
89
+
90
+ def write_text(hostnames, tree_data, platform, color_func, stream):
91
+ """
92
+ Write the tree of packages as text.
93
+
94
+ Products are indented with one dash.
95
+ Host packages are indented with two dashes.
96
+ Plugin packages are indented with three dashes.
97
+ """
98
+ if not hostnames:
99
+ d(0, red("There are no '{}' host packages".format(platform)), stream)
100
+ return
101
+ d(0, "{} Software".format(platform).upper(), stream)
102
+ last_hostgroup = None
103
+ for hostname in hostnames:
104
+ display_hostname = " ".join(hostname.split()[:2])
105
+ hostgroup = hostname.split(" ")[0]
106
+ if not hostgroup == last_hostgroup:
107
+ d(0, green("-" * 30), stream)
108
+ d(1, color_func(hostgroup), stream)
109
+ d(2, color_func(display_hostname), stream)
110
+ last_hostgroup = hostgroup
111
+ plugins = tree_data.supported_plugins(hostname)
112
+ if plugins:
113
+ for plugin in plugins:
114
+ d(
115
+ 3,
116
+ color_func(
117
+ "{} [{}]".format(
118
+ plugin["plugin"], ", ".join(plugin["versions"])
119
+ )
120
+ ),
121
+ stream,
122
+ )
123
+
124
+ def sort_hostnames_by_version(hostnames):
125
+ def sort_version(pkg):
126
+ hostname, product, version, platform = pkg
127
+ return f"{version} {product} {platform}"
128
+ def sort_product(pkg):
129
+ hostname, product, version, platform = pkg
130
+ return f"{product} {platform}"
131
+ pkg_sortable = [(hostname, hostname.split()[0], hostname.split()[1], hostname.split()[2]) for hostname in hostnames]
132
+ _psorted = sorted(pkg_sortable, key=sort_version, reverse=True)
133
+ _sorted = sorted(_psorted, key=sort_product)
134
+ return [pkg[0] for pkg in _sorted]
135
+
136
+ def pq(format="text"):
137
+ packages = api_client.request_software_packages()
138
+
139
+ tree_data = PackageTree(packages)
140
+
141
+ hostnames = sort_hostnames_by_version(tree_data.supported_host_names())
142
+ linux_hostnames = [h for h in hostnames if h.endswith("linux")]
143
+ windows_hostnames = [h for h in hostnames if h.endswith("windows")]
144
+
145
+ if format == "markdown":
146
+ stream = sys.stdout
147
+ raw(
148
+ "This page contains the complete list of software available at Conductor. If you require applications or plugins that are not in the list, please [create a support ticket](https://support.conductortech.com/hc/en-us/requests/new) and let us know.",
149
+ stream,
150
+ )
151
+ write_markdown(linux_hostnames, tree_data, "linux", stream)
152
+ write_markdown(windows_hostnames, tree_data, "windows", stream)
153
+ elif format == "text":
154
+ stream = sys.stdout
155
+ write_text(linux_hostnames, tree_data, "linux", magenta, stream)
156
+ d(0, "", stream)
157
+ write_text(windows_hostnames, tree_data, "windows", blue, stream)
158
+ elif format == "html":
159
+ stream = io.StringIO()
160
+ raw(
161
+ "This page contains the complete list of software available at Conductor. If you require applications or plugins that are not in the list, please [create a support ticket](https://support.conductortech.com/hc/en-us/requests/new) and let us know.",
162
+ stream,
163
+ )
164
+
165
+ write_markdown(linux_hostnames, tree_data, "linux", stream)
166
+ write_markdown(windows_hostnames, tree_data, "windows", stream)
167
+
168
+ html = markdown.markdown(
169
+ stream.getvalue(), extensions=["markdown.extensions.tables"]
170
+ )
171
+
172
+ html = decorate(html)
173
+
174
+ stream.close()
175
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False) as f:
176
+ f.write(html)
177
+ webbrowser.open("file://" + f.name, new=2)
178
+
179
+ def decorate(html):
180
+ html = html.replace("<table>", '<table class="pure-table pure-table-bordered">')
181
+ html = '<html><head>{}</head><body style="margin: 2em;">{}</body></html>'.format(PURE, html)
182
+ return html