fabfile.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. #!/usr/bin/env python
  2. from __future__ import print_function
  3. ##### Configuration ##############################
  4. SHORT_PROJECT_NAME = 'python'
  5. FULL_PROJECT_NAME = 'byte_of_{}'.format(SHORT_PROJECT_NAME)
  6. # NOTE Slugs MUST be lower-case
  7. MARKDOWN_FILES = [
  8. {
  9. 'file': '01-frontpage.md',
  10. 'slug': "python",
  11. 'title': "Python",
  12. },
  13. {
  14. 'file': '02-preface.md',
  15. 'slug': "python_en-preface",
  16. 'title': "Python : Preface",
  17. },
  18. {
  19. 'file': '03-intro.md',
  20. 'slug': "python_en-introduction",
  21. 'title': "Python : Introduction",
  22. },
  23. {
  24. 'file': '04-installation.md',
  25. 'slug': "python_en-installation",
  26. 'title': "Python : Installation",
  27. },
  28. {
  29. 'file': '05-first-steps.md',
  30. 'slug': "python_en-first_steps",
  31. 'title': "Python : First Steps",
  32. },
  33. {
  34. 'file': '06-basics.md',
  35. 'slug': "python_en-basics",
  36. 'title': "Python : Basics",
  37. },
  38. {
  39. 'file': '07-operators-expressions.md',
  40. 'slug': "python_en-operators_and_expressions",
  41. 'title': "Python : Operators and Expressions",
  42. },
  43. ]
  44. ## NOTES
  45. ## 1. This assumes that you have already created the S3 bucket whose name
  46. ## is stored in AWS_S3_BUCKET_NAME environment variable.
  47. ## 2. Under that S3 bucket, you have created a folder whose name is stored
  48. ## above as SHORT_PROJECT_NAME.
  49. ## 3. Under that S3 bucket, you have created a folder whose name is stored as
  50. ## SHORT_PROJECT_NAME/assets.
  51. ##### Imports ####################################
  52. import os
  53. import glob
  54. import subprocess
  55. try:
  56. from xmlrpc.client import ServerProxy
  57. except ImportError:
  58. from xmlrpclib import ServerProxy
  59. from pprint import pprint
  60. import boto
  61. import boto.s3.bucket
  62. import boto.s3.key
  63. from bs4 import BeautifulSoup
  64. from fabric.api import task, local
  65. ##### Start with checks ##########################
  66. for chapter in MARKDOWN_FILES:
  67. assert (chapter['slug'].lower() == chapter['slug']), \
  68. "Slug must be lower case : {}".format(chapter['slug'])
  69. if str(os.environ.get('AWS_ENABLED')).lower() == 'false':
  70. AWS_ENABLED = False
  71. elif os.environ.get('AWS_ACCESS_KEY_ID') is not None \
  72. and len(os.environ['AWS_ACCESS_KEY_ID']) > 0 \
  73. and os.environ.get('AWS_SECRET_ACCESS_KEY') is not None \
  74. and len(os.environ['AWS_SECRET_ACCESS_KEY']) > 0 \
  75. and os.environ.get('AWS_S3_BUCKET_NAME') is not None \
  76. and len(os.environ['AWS_S3_BUCKET_NAME']) > 0:
  77. AWS_ENABLED = True
  78. else:
  79. AWS_ENABLED = False
  80. print("NOTE: S3 uploading is disabled because of missing " +
  81. "AWS key environment variables.")
  82. # In my case, they are the same - 'files.swaroopch.com'
  83. # http://docs.amazonwebservices.com/AmazonS3/latest/dev/VirtualHosting.html#VirtualHostingCustomURLs
  84. S3_PUBLIC_URL = os.environ['AWS_S3_BUCKET_NAME']
  85. # else
  86. #S3_PUBLIC_URL = 's3.amazonaws.com/{}'.format(os.environ['AWS_S3_BUCKET_NAME'])
  87. if os.environ.get('WORDPRESS_RPC_URL') is not None \
  88. and len(os.environ['WORDPRESS_RPC_URL']) > 0 \
  89. and os.environ.get('WORDPRESS_BASE_URL') is not None \
  90. and len(os.environ['WORDPRESS_BASE_URL']) > 0 \
  91. and os.environ.get('WORDPRESS_BLOG_ID') is not None \
  92. and len(os.environ['WORDPRESS_BLOG_ID']) > 0 \
  93. and os.environ.get('WORDPRESS_USERNAME') is not None \
  94. and len(os.environ['WORDPRESS_USERNAME']) > 0 \
  95. and os.environ.get('WORDPRESS_PASSWORD') is not None \
  96. and len(os.environ['WORDPRESS_PASSWORD']) > 0 \
  97. and os.environ.get('WORDPRESS_PARENT_PAGE_ID') is not None \
  98. and len(os.environ['WORDPRESS_PARENT_PAGE_ID']) > 0 \
  99. and os.environ.get('WORDPRESS_PARENT_PAGE_SLUG') is not None \
  100. and len(os.environ['WORDPRESS_PARENT_PAGE_SLUG']) > 0:
  101. WORDPRESS_ENABLED = True
  102. else:
  103. WORDPRESS_ENABLED = False
  104. print("NOTE: Wordpress uploading is disabled because of " +
  105. "missing environment variables.")
  106. ##### Helper methods #############################
  107. def _upload_to_s3(filename, key):
  108. """http://docs.pythonboto.org/en/latest/s3_tut.html#storing-data"""
  109. conn = boto.connect_s3()
  110. b = boto.s3.bucket.Bucket(conn, os.environ['AWS_S3_BUCKET_NAME'])
  111. k = boto.s3.key.Key(b)
  112. k.key = key
  113. k.set_contents_from_filename(filename)
  114. k.set_acl('public-read')
  115. url = 'http://{}/{}'.format(S3_PUBLIC_URL, key)
  116. print("Uploaded to S3 : {}".format(url))
  117. return url
  118. def upload_output_to_s3(filename):
  119. key = "{}/{}".format(SHORT_PROJECT_NAME, filename.split('/')[-1])
  120. return _upload_to_s3(filename, key)
  121. def upload_asset_to_s3(filename):
  122. key = "{}/assets/{}".format(SHORT_PROJECT_NAME, filename.split('/')[-1])
  123. return _upload_to_s3(filename, key)
  124. def replace_images_with_s3_urls(text):
  125. """http://www.crummy.com/software/BeautifulSoup/bs4/doc/"""
  126. soup = BeautifulSoup(text)
  127. for image in soup.find_all('img'):
  128. image['src'] = upload_asset_to_s3(image['src'])
  129. return soup.prettify()
  130. def markdown_to_html(source_text, upload_assets_to_s3=False):
  131. """Convert from Markdown to HTML; optional: upload images, etc. to S3."""
  132. args = ['pandoc',
  133. '-f', 'markdown',
  134. '-t', 'html5',
  135. '-S']
  136. p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  137. output = p.communicate(source_text)[0]
  138. # http://wordpress.org/extend/plugins/raw-html/
  139. output = '<!--raw-->\n' + output + '\n<!--/raw-->'
  140. if upload_assets_to_s3:
  141. output = replace_images_with_s3_urls(output)
  142. return output
  143. def _wordpress_get_pages():
  144. server = ServerProxy(os.environ['WORDPRESS_RPC_URL'])
  145. print("(Fetching list of pages from WP)")
  146. return server.wp.getPosts(os.environ['WORDPRESS_BLOG_ID'],
  147. os.environ['WORDPRESS_USERNAME'],
  148. os.environ['WORDPRESS_PASSWORD'],
  149. {
  150. 'post_type': 'page',
  151. 'number': pow(10, 5),
  152. })
  153. def wordpress_new_page(slug, title, content):
  154. """Create a new Wordpress page.
  155. https://codex.wordpress.org/XML-RPC_WordPress_API/Posts#wp.newPost
  156. https://codex.wordpress.org/Function_Reference/wp_insert_post
  157. http://docs.python.org/library/xmlrpclib.html
  158. """
  159. server = ServerProxy(os.environ['WORDPRESS_RPC_URL'])
  160. return server.wp.newPost(os.environ['WORDPRESS_BLOG_ID'],
  161. os.environ['WORDPRESS_USERNAME'],
  162. os.environ['WORDPRESS_PASSWORD'],
  163. {
  164. 'post_name': slug,
  165. 'post_content': content,
  166. 'post_title': title,
  167. 'post_parent':
  168. os.environ['WORDPRESS_PARENT_PAGE_ID'],
  169. 'post_type': 'page',
  170. 'post_status': 'publish',
  171. 'comment_status': 'closed',
  172. 'ping_status': 'closed',
  173. })
  174. def wordpress_edit_page(post_id, title, content):
  175. """Edit a Wordpress page.
  176. https://codex.wordpress.org/XML-RPC_WordPress_API/Posts#wp.editPost
  177. https://codex.wordpress.org/Function_Reference/wp_insert_post
  178. http://docs.python.org/library/xmlrpclib.html
  179. """
  180. server = ServerProxy(os.environ['WORDPRESS_RPC_URL'])
  181. return server.wp.editPost(os.environ['WORDPRESS_BLOG_ID'],
  182. os.environ['WORDPRESS_USERNAME'],
  183. os.environ['WORDPRESS_PASSWORD'],
  184. post_id,
  185. {
  186. 'post_content': content,
  187. 'post_title': title,
  188. })
  189. ##### Tasks ######################################
  190. @task
  191. def wp():
  192. """https://codex.wordpress.org/XML-RPC_WordPress_API/Posts"""
  193. if WORDPRESS_ENABLED:
  194. existing_pages = _wordpress_get_pages()
  195. existing_page_slugs = [i.get('post_name') for i in existing_pages]
  196. def page_slug_to_id(slug):
  197. pages = [i for i in existing_pages if i.get('post_name') == slug]
  198. page = pages[0]
  199. return page['post_id']
  200. for chapter in MARKDOWN_FILES:
  201. html = markdown_to_html(open(chapter['file']).read(),
  202. upload_assets_to_s3=True)
  203. # TODO Add previous and next links at end of html
  204. if chapter['slug'] in existing_page_slugs:
  205. page_id = page_slug_to_id(chapter['slug'])
  206. print("Existing page to be updated: {} : {}".format(
  207. chapter['slug'],
  208. page_id))
  209. result = wordpress_edit_page(page_id,
  210. chapter['title'],
  211. html)
  212. print("Result: {}".format(result))
  213. else:
  214. print("New page to be created: {}".format(chapter['slug']))
  215. result = wordpress_new_page(chapter['slug'],
  216. chapter['title'],
  217. html)
  218. print("Result: {}".format(result))
  219. page_url = "{}/{}/{}".format(os.environ['WORDPRESS_BASE_URL'],
  220. os.environ['WORDPRESS_PARENT_PAGE_SLUG'],
  221. chapter['slug'])
  222. print(page_url)
  223. print()
  224. @task
  225. def html():
  226. """HTML5 output."""
  227. args = ['pandoc',
  228. '-f', 'markdown',
  229. '-t', 'html5',
  230. '-o', '{}.html'.format(FULL_PROJECT_NAME),
  231. '-S',
  232. '-s',
  233. '--toc'] + [i['file'] for i in MARKDOWN_FILES]
  234. local(' '.join(args))
  235. local('open {}.html'.format(FULL_PROJECT_NAME))
  236. @task
  237. def epub():
  238. """http://johnmacfarlane.net/pandoc/epub.html"""
  239. args = ['pandoc',
  240. '-f', 'markdown',
  241. '-t', 'epub',
  242. '-o', '{}.epub'.format(FULL_PROJECT_NAME),
  243. '-S'] + [i['file'] for i in MARKDOWN_FILES]
  244. # TODO --epub-cover-image
  245. # TODO --epub-metadata
  246. # TODO --epub-stylesheet
  247. local(' '.join(args))
  248. if AWS_ENABLED:
  249. upload_output_to_s3('{}.epub'.format(FULL_PROJECT_NAME))
  250. @task
  251. def pdf():
  252. """http://johnmacfarlane.net/pandoc/README.html#creating-a-pdf"""
  253. args = ['pandoc',
  254. '-f', 'markdown',
  255. # https://github.com/jgm/pandoc/issues/571
  256. #'-t', 'pdf',
  257. '-o', '{}.pdf'.format(FULL_PROJECT_NAME),
  258. '-S'] + [i['file'] for i in MARKDOWN_FILES]
  259. local(' '.join(args))
  260. if AWS_ENABLED:
  261. upload_output_to_s3('{}.pdf'.format(FULL_PROJECT_NAME))
  262. @task
  263. def docx():
  264. """OOXML document format."""
  265. args = ['pandoc',
  266. '-f', 'markdown',
  267. '-t', 'docx',
  268. '-o', '{}.docx'.format(FULL_PROJECT_NAME),
  269. '-S'] + [i['file'] for i in MARKDOWN_FILES]
  270. local(' '.join(args))
  271. if AWS_ENABLED:
  272. upload_output_to_s3('{}.docx'.format(FULL_PROJECT_NAME))
  273. @task
  274. def odt():
  275. """OpenDocument document format."""
  276. args = ['pandoc',
  277. '-f', 'markdown',
  278. '-t', 'odt',
  279. '-o', '{}.odt'.format(FULL_PROJECT_NAME),
  280. '-S'] + [i['file'] for i in MARKDOWN_FILES]
  281. local(' '.join(args))
  282. if AWS_ENABLED:
  283. upload_output_to_s3('{}.odt'.format(FULL_PROJECT_NAME))
  284. @task
  285. def clean():
  286. """Remove generated output files"""
  287. possible_outputs = (
  288. '{}.html'.format(FULL_PROJECT_NAME),
  289. '{}.epub'.format(FULL_PROJECT_NAME),
  290. '{}.pdf'.format(FULL_PROJECT_NAME),
  291. '{}.docx'.format(FULL_PROJECT_NAME),
  292. '{}.odt'.format(FULL_PROJECT_NAME),
  293. )
  294. for filename in possible_outputs:
  295. if os.path.exists(filename):
  296. os.remove(filename)
  297. print("Removed {}".format(filename))