fabfile.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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.pd',
  10. 'slug': "python",
  11. 'title': "Python",
  12. },
  13. {
  14. 'file': '02-preface.pd',
  15. 'slug': "python_en-preface",
  16. 'title': "Python : Preface",
  17. },
  18. {
  19. 'file': '03-intro.pd',
  20. 'slug': "python_en-introduction",
  21. 'title': "Python : Introduction",
  22. },
  23. {
  24. 'file': '04-installation.pd',
  25. 'slug': "python_en-installation",
  26. 'title': "Python : Installation",
  27. },
  28. {
  29. 'file': '05-first-steps.pd',
  30. 'slug': "python_en-first_steps",
  31. 'title': "Python : First Steps",
  32. },
  33. {
  34. 'file': '06-basics.pd',
  35. 'slug': "python_en-basics",
  36. 'title': "Python : Basics",
  37. },
  38. {
  39. 'file': '07-operators-expressions.pd',
  40. 'slug': "python_en-operators_and_expressions",
  41. 'title': "Python : Operators and Expressions",
  42. },
  43. {
  44. 'file': '08-control-flow.pd',
  45. 'slug': "python_en-control_flow",
  46. 'title': "Python : Control Flow",
  47. },
  48. {
  49. 'file': '09-functions.pd',
  50. 'slug': "python_en-functions",
  51. 'title': "Python : Functions",
  52. },
  53. {
  54. 'file': '10-modules.pd',
  55. 'slug': "python_en-modules",
  56. 'title': "Python : Modules",
  57. },
  58. {
  59. 'file': '11-data-structures.pd',
  60. 'slug': "python_en-data_structures",
  61. 'title': "Python : Data Structures",
  62. },
  63. {
  64. 'file': '12-problem-solving.pd',
  65. 'slug': "python_en-problem_solving",
  66. 'title': "Python : Problem Solving",
  67. },
  68. {
  69. 'file': '13-oop.pd',
  70. 'slug': "python_en-object_oriented_programming",
  71. 'title': "Python : Object Oriented Programming",
  72. },
  73. {
  74. 'file': '14-io.pd',
  75. 'slug': "python_en-input_output",
  76. 'title': "Python : Input Output",
  77. },
  78. {
  79. 'file': '15-exceptions.pd',
  80. 'slug': "python_en-exceptions",
  81. 'title': "Python : Exceptions",
  82. },
  83. {
  84. 'file': '16-standard-library.pd',
  85. 'slug': "python_en-standard_library",
  86. 'title': "Python : Standard Library",
  87. },
  88. {
  89. 'file': '17-more.pd',
  90. 'slug': "python_en-more",
  91. 'title': "Python : More",
  92. },
  93. {
  94. 'file': '18-what-next.pd',
  95. 'slug': "python_en-what_next",
  96. 'title': "Python : What Next",
  97. },
  98. {
  99. 'file': '19-appendix-floss.pd',
  100. 'slug': "python_en-appendix_floss",
  101. 'title': "Python : Appendix : FLOSS",
  102. },
  103. {
  104. 'file': '20-appendix-about.pd',
  105. 'slug': "python_en-appendix_about",
  106. 'title': "Python : Appendix : About",
  107. },
  108. {
  109. 'file': '21-revision-history.pd',
  110. 'slug': "python_en-appendix_revision_history",
  111. 'title': "Python : Appendix : Revision History",
  112. },
  113. ]
  114. ## NOTES
  115. ## 1. This assumes that you have already created the S3 bucket whose name
  116. ## is stored in AWS_S3_BUCKET_NAME environment variable.
  117. ## 2. Under that S3 bucket, you have created a folder whose name is stored
  118. ## above as SHORT_PROJECT_NAME.
  119. ## 3. Under that S3 bucket, you have created a folder whose name is stored as
  120. ## SHORT_PROJECT_NAME/assets.
  121. ##### Imports ####################################
  122. import os
  123. import glob
  124. import subprocess
  125. try:
  126. from xmlrpc.client import ServerProxy
  127. except ImportError:
  128. from xmlrpclib import ServerProxy
  129. from pprint import pprint
  130. import boto
  131. import boto.s3.bucket
  132. import boto.s3.key
  133. from bs4 import BeautifulSoup
  134. from fabric.api import task, local
  135. ##### Start with checks ##########################
  136. for chapter in MARKDOWN_FILES:
  137. assert (chapter['slug'].lower() == chapter['slug']), \
  138. "Slug must be lower case : {}".format(chapter['slug'])
  139. if str(os.environ.get('AWS_ENABLED')).lower() == 'false':
  140. AWS_ENABLED = False
  141. elif os.environ.get('AWS_ACCESS_KEY_ID') is not None \
  142. and len(os.environ['AWS_ACCESS_KEY_ID']) > 0 \
  143. and os.environ.get('AWS_SECRET_ACCESS_KEY') is not None \
  144. and len(os.environ['AWS_SECRET_ACCESS_KEY']) > 0 \
  145. and os.environ.get('AWS_S3_BUCKET_NAME') is not None \
  146. and len(os.environ['AWS_S3_BUCKET_NAME']) > 0:
  147. AWS_ENABLED = True
  148. else:
  149. AWS_ENABLED = False
  150. print("NOTE: S3 uploading is disabled because of missing " +
  151. "AWS key environment variables.")
  152. # In my case, they are the same - 'files.swaroopch.com'
  153. # http://docs.amazonwebservices.com/AmazonS3/latest/dev/VirtualHosting.html#VirtualHostingCustomURLs
  154. S3_PUBLIC_URL = os.environ['AWS_S3_BUCKET_NAME']
  155. # else
  156. #S3_PUBLIC_URL = 's3.amazonaws.com/{}'.format(os.environ['AWS_S3_BUCKET_NAME'])
  157. if os.environ.get('WORDPRESS_RPC_URL') is not None \
  158. and len(os.environ['WORDPRESS_RPC_URL']) > 0 \
  159. and os.environ.get('WORDPRESS_BASE_URL') is not None \
  160. and len(os.environ['WORDPRESS_BASE_URL']) > 0 \
  161. and os.environ.get('WORDPRESS_BLOG_ID') is not None \
  162. and len(os.environ['WORDPRESS_BLOG_ID']) > 0 \
  163. and os.environ.get('WORDPRESS_USERNAME') is not None \
  164. and len(os.environ['WORDPRESS_USERNAME']) > 0 \
  165. and os.environ.get('WORDPRESS_PASSWORD') is not None \
  166. and len(os.environ['WORDPRESS_PASSWORD']) > 0 \
  167. and os.environ.get('WORDPRESS_PARENT_PAGE_ID') is not None \
  168. and len(os.environ['WORDPRESS_PARENT_PAGE_ID']) > 0 \
  169. and os.environ.get('WORDPRESS_PARENT_PAGE_SLUG') is not None \
  170. and len(os.environ['WORDPRESS_PARENT_PAGE_SLUG']) > 0:
  171. WORDPRESS_ENABLED = True
  172. else:
  173. WORDPRESS_ENABLED = False
  174. print("NOTE: Wordpress uploading is disabled because of " +
  175. "missing environment variables.")
  176. ##### Helper methods #############################
  177. def _upload_to_s3(filename, key):
  178. """http://docs.pythonboto.org/en/latest/s3_tut.html#storing-data"""
  179. conn = boto.connect_s3()
  180. b = boto.s3.bucket.Bucket(conn, os.environ['AWS_S3_BUCKET_NAME'])
  181. k = boto.s3.key.Key(b)
  182. k.key = key
  183. k.set_contents_from_filename(filename)
  184. k.set_acl('public-read')
  185. url = 'http://{}/{}'.format(S3_PUBLIC_URL, key)
  186. print("Uploaded to S3 : {}".format(url))
  187. return url
  188. def upload_output_to_s3(filename):
  189. key = "{}/{}".format(SHORT_PROJECT_NAME, filename.split('/')[-1])
  190. return _upload_to_s3(filename, key)
  191. def upload_asset_to_s3(filename):
  192. key = "{}/assets/{}".format(SHORT_PROJECT_NAME, filename.split('/')[-1])
  193. return _upload_to_s3(filename, key)
  194. def replace_images_with_s3_urls(text):
  195. """http://www.crummy.com/software/BeautifulSoup/bs4/doc/"""
  196. soup = BeautifulSoup(text)
  197. for image in soup.find_all('img'):
  198. image['src'] = upload_asset_to_s3(image['src'])
  199. return unicode(soup)
  200. def markdown_to_html(source_text, upload_assets_to_s3=False):
  201. """Convert from Markdown to HTML; optional: upload images, etc. to S3."""
  202. args = ['pandoc',
  203. '-f', 'markdown',
  204. '-t', 'html5']
  205. p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
  206. output = p.communicate(source_text)[0]
  207. # http://wordpress.org/extend/plugins/raw-html/
  208. output = '<!--raw-->\n' + output + '\n<!--/raw-->'
  209. # NOTE: Also assumes that you have added the CSS from
  210. # `pandoc -S -t html5` to the `style.css` of your active Wordpress theme.
  211. if upload_assets_to_s3:
  212. output = replace_images_with_s3_urls(output)
  213. return output
  214. def _wordpress_get_pages():
  215. server = ServerProxy(os.environ['WORDPRESS_RPC_URL'])
  216. print("(Fetching list of pages from WP)")
  217. return server.wp.getPosts(os.environ['WORDPRESS_BLOG_ID'],
  218. os.environ['WORDPRESS_USERNAME'],
  219. os.environ['WORDPRESS_PASSWORD'],
  220. {
  221. 'post_type': 'page',
  222. 'number': pow(10, 5),
  223. })
  224. def wordpress_new_page(slug, title, content):
  225. """Create a new Wordpress page.
  226. https://codex.wordpress.org/XML-RPC_WordPress_API/Posts#wp.newPost
  227. https://codex.wordpress.org/Function_Reference/wp_insert_post
  228. http://docs.python.org/library/xmlrpclib.html
  229. """
  230. server = ServerProxy(os.environ['WORDPRESS_RPC_URL'])
  231. return server.wp.newPost(os.environ['WORDPRESS_BLOG_ID'],
  232. os.environ['WORDPRESS_USERNAME'],
  233. os.environ['WORDPRESS_PASSWORD'],
  234. {
  235. 'post_name': slug,
  236. 'post_content': content,
  237. 'post_title': title,
  238. 'post_parent':
  239. os.environ['WORDPRESS_PARENT_PAGE_ID'],
  240. 'post_type': 'page',
  241. 'post_status': 'publish',
  242. 'comment_status': 'closed',
  243. 'ping_status': 'closed',
  244. })
  245. def wordpress_edit_page(post_id, title, content):
  246. """Edit a Wordpress page.
  247. https://codex.wordpress.org/XML-RPC_WordPress_API/Posts#wp.editPost
  248. https://codex.wordpress.org/Function_Reference/wp_insert_post
  249. http://docs.python.org/library/xmlrpclib.html
  250. """
  251. server = ServerProxy(os.environ['WORDPRESS_RPC_URL'])
  252. return server.wp.editPost(os.environ['WORDPRESS_BLOG_ID'],
  253. os.environ['WORDPRESS_USERNAME'],
  254. os.environ['WORDPRESS_PASSWORD'],
  255. post_id,
  256. {
  257. 'post_content': content,
  258. 'post_title': title,
  259. })
  260. ##### Tasks ######################################
  261. @task
  262. def wp():
  263. """https://codex.wordpress.org/XML-RPC_WordPress_API/Posts"""
  264. if WORDPRESS_ENABLED:
  265. existing_pages = _wordpress_get_pages()
  266. existing_page_slugs = [i.get('post_name') for i in existing_pages]
  267. def page_slug_to_id(slug):
  268. pages = [i for i in existing_pages if i.get('post_name') == slug]
  269. page = pages[0]
  270. return page['post_id']
  271. for chapter in MARKDOWN_FILES:
  272. html = markdown_to_html(open(chapter['file']).read(),
  273. upload_assets_to_s3=True)
  274. # TODO Add previous and next links at end of html
  275. if chapter['slug'] in existing_page_slugs:
  276. page_id = page_slug_to_id(chapter['slug'])
  277. print("Existing page to be updated: {} : {}".format(
  278. chapter['slug'],
  279. page_id))
  280. result = wordpress_edit_page(page_id,
  281. chapter['title'],
  282. html)
  283. print("Result: {}".format(result))
  284. else:
  285. print("New page to be created: {}".format(chapter['slug']))
  286. result = wordpress_new_page(chapter['slug'],
  287. chapter['title'],
  288. html)
  289. print("Result: {}".format(result))
  290. page_url = "{}/{}/{}".format(os.environ['WORDPRESS_BASE_URL'],
  291. os.environ['WORDPRESS_PARENT_PAGE_SLUG'],
  292. chapter['slug'])
  293. print(page_url)
  294. print()
  295. @task
  296. def html():
  297. """HTML5 output."""
  298. args = ['pandoc',
  299. '-f', 'markdown',
  300. '-t', 'html5',
  301. '-o', '{}.html'.format(FULL_PROJECT_NAME),
  302. '-s',
  303. '--toc'] + [i['file'] for i in MARKDOWN_FILES]
  304. local(' '.join(args))
  305. local('open {}.html'.format(FULL_PROJECT_NAME))
  306. @task
  307. def epub():
  308. """http://johnmacfarlane.net/pandoc/epub.html"""
  309. args = ['pandoc',
  310. '-f', 'markdown',
  311. '-t', 'epub',
  312. '-o', '{}.epub'.format(FULL_PROJECT_NAME)] + \
  313. [i['file'] for i in MARKDOWN_FILES]
  314. # TODO --epub-cover-image
  315. # TODO --epub-metadata
  316. # TODO --epub-stylesheet
  317. local(' '.join(args))
  318. if AWS_ENABLED:
  319. upload_output_to_s3('{}.epub'.format(FULL_PROJECT_NAME))
  320. @task
  321. def pdf():
  322. """http://johnmacfarlane.net/pandoc/README.html#creating-a-pdf"""
  323. args = ['pandoc',
  324. '-f', 'markdown',
  325. # https://github.com/jgm/pandoc/issues/571
  326. #'-t', 'pdf',
  327. '-o', '{}.pdf'.format(FULL_PROJECT_NAME)] + \
  328. [i['file'] for i in MARKDOWN_FILES]
  329. local(' '.join(args))
  330. if AWS_ENABLED:
  331. upload_output_to_s3('{}.pdf'.format(FULL_PROJECT_NAME))
  332. @task
  333. def docx():
  334. """OOXML document format."""
  335. args = ['pandoc',
  336. '-f', 'markdown',
  337. '-t', 'docx',
  338. '-o', '{}.docx'.format(FULL_PROJECT_NAME)] + \
  339. [i['file'] for i in MARKDOWN_FILES]
  340. local(' '.join(args))
  341. if AWS_ENABLED:
  342. upload_output_to_s3('{}.docx'.format(FULL_PROJECT_NAME))
  343. @task
  344. def odt():
  345. """OpenDocument document format."""
  346. args = ['pandoc',
  347. '-f', 'markdown',
  348. '-t', 'odt',
  349. '-o', '{}.odt'.format(FULL_PROJECT_NAME)] + \
  350. [i['file'] for i in MARKDOWN_FILES]
  351. local(' '.join(args))
  352. if AWS_ENABLED:
  353. upload_output_to_s3('{}.odt'.format(FULL_PROJECT_NAME))
  354. @task
  355. def clean():
  356. """Remove generated output files"""
  357. possible_outputs = (
  358. '{}.html'.format(FULL_PROJECT_NAME),
  359. '{}.epub'.format(FULL_PROJECT_NAME),
  360. '{}.pdf'.format(FULL_PROJECT_NAME),
  361. '{}.docx'.format(FULL_PROJECT_NAME),
  362. '{}.odt'.format(FULL_PROJECT_NAME),
  363. )
  364. for filename in possible_outputs:
  365. if os.path.exists(filename):
  366. os.remove(filename)
  367. print("Removed {}".format(filename))