├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── dev │ ├── changelog.rst │ └── testing.rst ├── examples.rst ├── examples │ ├── custom-methods.rst │ ├── media.rst │ ├── posts.rst │ └── taxonomies.rst ├── index.rst ├── make.bat ├── overview.rst └── ref │ ├── client.rst │ ├── methods.rst │ └── wordpress.rst ├── setup.py ├── tests ├── __init__.py ├── files │ └── wordpress_logo.png ├── test_categories.py ├── test_comments.py ├── test_demo.py ├── test_fieldmaps.py ├── test_media.py ├── test_options.py ├── test_pages.py ├── test_posts.py ├── test_taxonomies.py └── test_users.py ├── wordpress_xmlrpc ├── __init__.py ├── base.py ├── compat.py ├── exceptions.py ├── fieldmaps.py ├── methods │ ├── __init__.py │ ├── comments.py │ ├── demo.py │ ├── media.py │ ├── options.py │ ├── pages.py │ ├── posts.py │ ├── taxonomies.py │ └── users.py └── wordpress.py └── wp-config-sample.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | *.egg-info 4 | .DS_Store 5 | dist/* 6 | wp-config.cfg 7 | 8 | /docs/_build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Travis CI Configuration File 2 | 3 | # Use Travis CI's beta Trusty environments 4 | sudo: required 5 | dist: trusty 6 | 7 | language: python 8 | 9 | python: 10 | - "2.7" 11 | - "3.2" 12 | - "3.5" 13 | 14 | addons: 15 | apt: 16 | packages: 17 | - php5-common 18 | - php5-cli 19 | - mysql-server 20 | - php5-mysql 21 | 22 | notifications: 23 | email: false 24 | 25 | env: 26 | global: 27 | - WP_VERSION=4.4 28 | - WP_INSTALL_PATH=/tmp/wordpress 29 | - WP_URL=http://127.0.0.1:8080 30 | 31 | # Clones WordPress and configures our testing environment. 32 | before_script: 33 | - mysql -e "CREATE DATABASE wordpress;" -uroot 34 | - curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 35 | - chmod +x wp-cli.phar 36 | - ./wp-cli.phar core download --path=$WP_INSTALL_PATH --version=$WP_VERSION 37 | - ./wp-cli.phar core config --dbname=wordpress --dbuser=root --dbhost=localhost --path=$WP_INSTALL_PATH 38 | - ./wp-cli.phar core install --admin_name=admin --admin_password=admin --admin_email=admin@example.com --url=$WP_URL --title=WordPress --path=$WP_INSTALL_PATH 39 | - ./wp-cli.phar server --host=0.0.0.0 --path=$WP_INSTALL_PATH & 40 | - touch wp-config.cfg 41 | - echo "[wordpress]" >> wp-config.cfg 42 | - echo "url = $WP_URL/xmlrpc.php" >> wp-config.cfg 43 | - echo "username = admin" >> wp-config.cfg 44 | - echo "password = admin" >> wp-config.cfg 45 | - echo "userid = 1" >> wp-config.cfg 46 | - cat wp-config.cfg 47 | 48 | install: 49 | - pip install nose 50 | 51 | script: nosetests 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Max Cutler 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE README.rst wp-config-sample.cfg 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://travis-ci.org/maxcutler/python-wordpress-xmlrpc.svg?branch=master 2 | :target: https://travis-ci.org/maxcutler/python-wordpress-xmlrpc 3 | 4 | Overview 5 | ======== 6 | 7 | Python library to interface with a WordPress blog's `XML-RPC API`__. 8 | 9 | __ http://codex.wordpress.org/XML-RPC_Support 10 | 11 | An implementation of the standard WordPress API methods is provided, 12 | but the library is designed for easy integration with custom 13 | XML-RPC API methods provided by plugins. 14 | 15 | This library was developed against and tested on WordPress 3.5. 16 | This library is compatible with Python 2.6+ and 3.2+. 17 | 18 | Please see docs for more information: http://python-wordpress-xmlrpc.rtfd.org -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-wordpress-xmlrpc.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-wordpress-xmlrpc.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-wordpress-xmlrpc" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-wordpress-xmlrpc" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-wordpress-xmlrpc documentation build configuration file, created by 4 | # sphinx-quickstart on Sun Apr 15 15:18:10 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'python-wordpress-xmlrpc' 44 | copyright = u'2012, Max Cutler' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '2.3' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '2.3' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'python-wordpress-xmlrpcdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'python-wordpress-xmlrpc.tex', u'python-wordpress-xmlrpc Documentation', 187 | u'Max Cutler', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'python-wordpress-xmlrpc', u'python-wordpress-xmlrpc Documentation', 217 | [u'Max Cutler'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'python-wordpress-xmlrpc', u'python-wordpress-xmlrpc Documentation', 231 | u'Max Cutler', 'python-wordpress-xmlrpc', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | 244 | 245 | # Example configuration for intersphinx: refer to the Python standard library. 246 | intersphinx_mapping = { 247 | 'python': ('http://python.readthedocs.org/en/latest/', None), 248 | } 249 | -------------------------------------------------------------------------------- /docs/dev/changelog.rst: -------------------------------------------------------------------------------- 1 | History/CHANGELOG 2 | ================= 3 | 4 | 2.3 5 | --- 6 | 7 | (June 29, 2014) 8 | 9 | * Allow custom transports for XML-RPC connections. 10 | * Fix JPEG file MIME-type. 11 | * Import media methods correctly. 12 | * Fix ``ping_status`` field definition. 13 | * Workaround a bug encountered when fetching old draft posts (#40, props strycore). 14 | * Fixed some documentation bugs. 15 | 16 | 2.2 17 | --- 18 | 19 | (December 2, 2012) 20 | 21 | * Add ``wp.newComment`` variant for anonymous comments. 22 | * Added support for user methods. 23 | * Added support for post revisions. 24 | * Fixed comment ``date_created`` field definition. 25 | * Better unicode string handling. 26 | 27 | 2.1 28 | --- 29 | 30 | (May 12, 2012) 31 | 32 | * Added missing import that was causing failures during exception handling. 33 | * Updated fields to match changes in WordPress 3.4 between Beta 1 and RC1. 34 | * Fixed some documentation bugs. 35 | 36 | 2.0 37 | --- 38 | 39 | (April 16, 2012) 40 | 41 | * Major rewrite to support new XML-RPC features in WordPress 3.4. 42 | * Rewrote ``WordPressPost`` and ``methods.posts`` module. 43 | * Removed CRUD methods for pages. 44 | * Added ``WordPressTaxonomy`` and ``WordPressTerm`` classes. 45 | * Added ``methods.taxonomies`` module. 46 | * Removed ``WordPressCategory`` and ``WordPressTag`` classes. 47 | * Removed ``methods.categories`` module. 48 | * Added ``id`` field to ``WordPressMedia``. 49 | * Removed support for legacy-style (e.g., Blogger) methods. 50 | * Removed ``args_start_position`` and ``default_args_position`` parameters on ``XmlrpcMethod``. 51 | * Removed ``requires_blog`` parameter on ``AuthenticatedMethod``. 52 | * Set default values on all fields that are used in ``str``/``unicode`` to avoid ``AttributeError`` exceptions. 53 | * Fixed bug with ``FieldMap`` that caused ``False`` boolean values to be ignored. 54 | * Added ability to override ``results_class`` via a method class constructor kwarg. 55 | * Added support for optional method arguments. 56 | 57 | 1.5 58 | --- 59 | 60 | (August 27, 2011) 61 | 62 | * Refactored ``FieldMap`` to be more flexible. 63 | * Added new ``Exception`` subclasses for more specific error handling. 64 | 65 | 1.4 66 | --- 67 | 68 | (July 31, 2011) 69 | 70 | * Added support for post formats. 71 | * Added support for media methods. 72 | 73 | 1.3 74 | --- 75 | 76 | (July 31, 2011) 77 | 78 | * Created test suite. 79 | * Added support for page methods. 80 | * Added support for post/page passwords. 81 | 82 | 1.2 83 | --- 84 | 85 | (June 25, 2011) 86 | 87 | * Added support for comments methods. 88 | 89 | 1.1 90 | --- 91 | 92 | (October 11, 2010) 93 | 94 | * Implemented automatic conversion of WordPress objects in method invocations. 95 | 96 | 1.0 97 | --- 98 | 99 | (October 10, 2010) 100 | 101 | * Initial release. 102 | -------------------------------------------------------------------------------- /docs/dev/testing.rst: -------------------------------------------------------------------------------- 1 | Testing 2 | ======= 3 | 4 | Requirements 5 | ------------ 6 | 7 | ``nose`` is used as the test runner. Use ``easy_install`` or ``pip`` to install:: 8 | 9 | pip nose 10 | 11 | 12 | Configuring against your server 13 | ------------------------------- 14 | 15 | To test this library, we must perform XML-RPC requests against an 16 | actual WordPress server. To configure against your own server: 17 | 18 | * Copy the included ``wp-config-sample.cfg`` file to ``wp-config.cfg``. 19 | * Edit ``wp-config.cfg`` and fill in the necessary values. 20 | 21 | Running Tests 22 | ------------- 23 | 24 | Note: Be sure to have installed ``nose`` and created your ``wp-config.cfg``. 25 | 26 | To run the entire test suite, run the following from the root of the repository:: 27 | 28 | nosetests 29 | 30 | To run a sub-set of the tests, you can specify a specific feature area:: 31 | 32 | nosetests -a posts 33 | 34 | You can run against multiple areas:: 35 | 36 | nosetests -a posts -a comments 37 | 38 | Or you can run everything except a specific area:: 39 | 40 | nosetests -a '!comments' 41 | 42 | You can use all the normal ``nose`` command line options. For example, to increase output level:: 43 | 44 | nosetests -a demo --verbosity=3 45 | 46 | Full usage details: 47 | 48 | * `nose`__ 49 | 50 | __ http://readthedocs.org/docs/nose/en/latest/usage.html 51 | 52 | Contributing Tests 53 | ------------------ 54 | 55 | If you are submitting a patch for this library, please be sure to include 56 | one or more tests that cover the changes. 57 | 58 | if you are adding new test methods, be sure to tag them with the appropriate 59 | feature areas using the ``@attr()`` decorator. 60 | -------------------------------------------------------------------------------- /docs/examples.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== 3 | 4 | .. toctree:: 5 | 6 | examples/posts 7 | examples/taxonomies 8 | examples/media 9 | examples/custom-methods -------------------------------------------------------------------------------- /docs/examples/custom-methods.rst: -------------------------------------------------------------------------------- 1 | Custom XML-RPC Methods 2 | ====================== 3 | 4 | See the `WordPress Codex`__ for details on how to write a WordPress plugin that adds 5 | custom XML-RPC method to WordPress. 6 | 7 | __ http://codex.wordpress.org/XML-RPC_Extending 8 | 9 | The following examples will use the sample methods from that codex page. 10 | 11 | Anonymous Methods 12 | ----------------- 13 | 14 | To use the ``mynamespace.subtractTwoNumbers`` method, create a class derived from :class:`wordpress_xmlrpc.AnonymousMethod`:: 15 | 16 | from wordpress_xmlrpc import AnonymousMethod 17 | 18 | class SubtractTwoNumbers(AnonymousMethod): 19 | method_name = 'mynamespace.subtractTwoNumbers' 20 | method_args = ('number1', 'number2') 21 | 22 | This class can then be used with :meth:`Client.call`:: 23 | 24 | from wordpress_xmlrpc import Client 25 | 26 | client = Client('http://www.example.com/xmlrpc.php', 'harrietsmith', 'mypassword') 27 | difference = client.call(SubtractTwoNumbers(10, 5)) 28 | # difference == 5 29 | 30 | Authenticated Methods 31 | --------------------- 32 | 33 | If your custom authenticated method follows the common ``method(blog_id, username, password, *args)`` structure, then you can use :class:`wordpress_xmlrpc.AuthenticatedMethod`:: 34 | 35 | from wordpress_xmlrpc import AuthenticatedMethod 36 | 37 | class GetUserID(AuthenticatedMethod): 38 | method_name = 'mynamespace.getUserID' 39 | 40 | Again, this class can then be used with :meth:`Client.call`:: 41 | 42 | user_id = client.call(GetUserID()) 43 | # user_id == 3 44 | 45 | Note that you do not have to supply ``blog_id``, ``username``, or ``password`` to the class constructor, since these are automatically added by `AuthenticatedMethod`. Custom method classes only require arguments specified by ``method_args`` and the optional ``optional_args``. 46 | -------------------------------------------------------------------------------- /docs/examples/media.rst: -------------------------------------------------------------------------------- 1 | Working with Media 2 | ================== 3 | 4 | Uploading a File 5 | ---------------- 6 | 7 | The :class:`wordpress_xmlrpc.methods.media.UploadFile` method can be used to upload new files to a WordPress blog:: 8 | 9 | from wordpress_xmlrpc import Client, WordPressPost 10 | from wordpress_xmlrpc.compat import xmlrpc_client 11 | from wordpress_xmlrpc.methods import media, posts 12 | 13 | client = Client(...) 14 | 15 | # set to the path to your file 16 | filename = '/path/to/my/picture.jpg' 17 | 18 | # prepare metadata 19 | data = { 20 | 'name': 'picture.jpg', 21 | 'type': 'image/jpeg', # mimetype 22 | } 23 | 24 | # read the binary file and let the XMLRPC library encode it into base64 25 | with open(filename, 'rb') as img: 26 | data['bits'] = xmlrpc_client.Binary(img.read()) 27 | 28 | response = client.call(media.UploadFile(data)) 29 | # response == { 30 | # 'id': 6, 31 | # 'file': 'picture.jpg' 32 | # 'url': 'http://www.example.com/wp-content/uploads/2012/04/16/picture.jpg', 33 | # 'type': 'image/jpeg', 34 | # } 35 | attachment_id = response['id'] 36 | 37 | This newly-uploaded attachment can then be set as the thumbnail for a post:: 38 | 39 | post = WordPressPost() 40 | post.title = 'Picture of the Day' 41 | post.content = 'What a lovely picture today!' 42 | post.post_status = 'publish' 43 | post.thumbnail = attachment_id 44 | post.id = client.call(posts.NewPost(post)) 45 | 46 | .. note:: 47 | 48 | If you do not know the mimetype at development time, you can use the :mod:`mimetypes ` library in Python:: 49 | 50 | data['type'] = mimetypes.read_mime_types(filename) or mimetypes.guess_type(filename)[0] 51 | 52 | Querying 53 | -------- 54 | 55 | Use :class:`wordpress_xmlrpc.methods.media.GetMediaLibrary` and :class:`wordpress_xmlrpc.methods.media.GetMediaItem` to retrieve information about attachments. 56 | -------------------------------------------------------------------------------- /docs/examples/posts.rst: -------------------------------------------------------------------------------- 1 | Working with Posts 2 | ============================== 3 | 4 | python-wordpress-xmlrpc supports all registered `WordPress post types`__. 5 | 6 | __ http://codex.wordpress.org/Post_Types 7 | 8 | Behind the scenes in WordPress, all post types are based on a single "post" database table, and all of the functionality is exposed through the `posts methods`__ in the XML-RPC API. 9 | 10 | __ http://codex.wordpress.org/XML-RPC_WordPress_API/Posts 11 | 12 | For consistency, the same approach is adopted by python-wordpress-xmlrpc. 13 | 14 | .. note:: 15 | 16 | Posts will be sent as drafts by default. If you want to publish a post, set `post.post_status = 'publish'`. 17 | 18 | Normal Posts 19 | ------------ 20 | 21 | First, let's see how to retrieve normal WordPress posts:: 22 | 23 | from wordpress_xmlrpc import Client 24 | from wordpress_xmlrpc.methods import posts 25 | 26 | client = Client(...) 27 | posts = client.call(posts.GetPosts()) 28 | # posts == [WordPressPost, WordPressPost, ...] 29 | 30 | And here's how to create and edit a new post:: 31 | 32 | from wordpress_xmlrpc import WordPressPost 33 | 34 | post = WordPressPost() 35 | post.title = 'My post' 36 | post.content = 'This is a wonderful blog post about XML-RPC.' 37 | post.id = client.call(posts.NewPost(post)) 38 | 39 | # whoops, I forgot to publish it! 40 | post.post_status = 'publish' 41 | client.call(posts.EditPost(post.id, post)) 42 | 43 | Pages 44 | ----- 45 | 46 | Out of the box, WordPress supports a post type called "page" for static non-blog pages on a WordPress site. Let's see how to do the same actions for pages:: 47 | 48 | from wordpress_xmlrpc import WordPressPage 49 | 50 | pages = client.call(posts.GetPosts({'post_type': 'page'}, results_class=WordPressPage)) 51 | # pages == [WordPressPage, WordPressPage, ...] 52 | 53 | Note two important differences: 54 | 55 | 1. The ``filter`` parameter's ``post_type`` option is used to limit the query to objects of the desired post type. 56 | 2. The constructor was passd a ``results_class`` keyword argument that told it what class to use to interpret the response values. 57 | 58 | And here's how to create and edit a page:: 59 | 60 | page = WordPressPage() 61 | page.title = 'About Me' 62 | page.content = 'I am an aspiring WordPress and Python developer.' 63 | page.post_status = 'publish' 64 | page.id = client.call(posts.NewPost(page)) 65 | 66 | # no longer aspiring 67 | page.content = 'I am a WordPress and Python developer.' 68 | client.call(posts.EditPost(page.id, page)) 69 | 70 | Custom Post Types 71 | ----------------- 72 | 73 | While the pages example used its own ``results_class``, that was a unique situation because pages are special in WordPress and have fields directly in the posts table. 74 | 75 | Most custom post types instead use post `custom fields`__ to store their additional information, and custom fields are already exposed on :class:`WordPressPost`. 76 | 77 | __ http://codex.wordpress.org/Custom_Fields 78 | 79 | For this example, let's assume that your plugin or theme has added an ``acme_product`` custom post type to WordPress: 80 | 81 | .. code-block:: python 82 | 83 | # first, let's find some products 84 | products = client.call(posts.GetPosts({'post_type': 'acme_product', 'number': 100})) 85 | 86 | # calculate the average price of these 100 widgets 87 | sum = 0 88 | for product in products: 89 | # note: product is a WordPressPost object 90 | for custom_field in product.custom_fields: 91 | if custom_field['key'] == 'price': 92 | sum = sum + custom_field['value'] 93 | break 94 | average = sum / len(products) 95 | 96 | # now let's create a new product 97 | widget = WordPressPost() 98 | widget.post_type = 'acme_product' 99 | widget.title = 'Widget' 100 | widget.content = 'This is the widget's description.' 101 | widget.custom_fields = [] 102 | widget.custom_fields.append({ 103 | 'key': 'price', 104 | 'value': 2 105 | }) 106 | widget.id = client.call(posts.NewPost(widget)) 107 | 108 | Advanced Querying 109 | ----------------- 110 | 111 | By default, :class:`wordpress_xmlrpc.methods.posts.GetPosts` returns 10 posts in reverse-chronological order (based on their publish date). However, using the ``filter`` parameter, posts can be queried in other ways. 112 | 113 | Result Paging 114 | ~~~~~~~~~~~~~ 115 | 116 | If you want to iterate through all posts in a WordPress blog, a server-friendly technique is to use result paging using the ``number`` and ``offset`` options:: 117 | 118 | # get pages in batches of 20 119 | offset = 0 120 | increment = 20 121 | while True: 122 | posts = client.call(posts.GetPosts({'number': increment, 'offset': offset})) 123 | if len(posts) == 0: 124 | break # no more posts returned 125 | for post in posts: 126 | do_something(post) 127 | offset = offset + increment 128 | 129 | Ordering 130 | ~~~~~~~~ 131 | 132 | If you don't want posts sorted by ``post_date``, then you can use ``orderby`` and ``order`` options to change that behavior. 133 | 134 | For example, in sync scenarios you might want to look for posts by modification date instead of publish date:: 135 | 136 | recently_modified = client.call(posts.GetPosts({'orderby': 'post_modified', 'number': 100})) 137 | 138 | Or if you want your ACME products sorted alphabetically:: 139 | 140 | products = client.call(posts.GetPosts({'post_type': 'acme_product', 'orderby': 'title', 'order': 'ASC'})) 141 | 142 | Post Status 143 | ~~~~~~~~~~~ 144 | 145 | Another common scenario is that you only want published posts:: 146 | 147 | published_posts = client.call(posts.GetPosts({'post_status': 'publish'})) 148 | 149 | Or only draft posts:: 150 | 151 | draft_posts = client.call(posts.GetPosts({'post_status': 'draft'})) 152 | 153 | You can find the set of valid ``post_status`` by using the :class:`wordpress_xmlrpc.methods.posts.GetPostStatusList` method. 154 | -------------------------------------------------------------------------------- /docs/examples/taxonomies.rst: -------------------------------------------------------------------------------- 1 | Working with Taxonomies 2 | ======================= 3 | 4 | `Taxonomies`__ in WordPress are a means of classifying content. Out of the box, WordPress has two primary taxonomies, categories (``category``) and tags (``post_tag``). Plugins and themes can specify additional custom taxonomies. 5 | 6 | __ http://codex.wordpress.org/Taxonomies 7 | 8 | Taxonomies 9 | ---------- 10 | 11 | To retrieve a list of taxonomies for a WordPress blog, use :class:`wordpress_xmlrpc.methods.taxonomies.GetTaxonomies`:: 12 | 13 | from wordpress_xmlrpc import Client 14 | from wordpress_xmlrpc.methods import taxonomies 15 | 16 | client = Client(...) 17 | taxes = client.call(taxonomies.GetTaxonomies()) 18 | # taxes == [WordPressTaxonomy, WordPressTaxonomy, ...] 19 | 20 | An individual taxonomy can be retrieved by name:: 21 | 22 | category_tax = client.call(taxonomies.GetTaxonomy('category')) 23 | 24 | .. note:: 25 | 26 | Taxonomies can only be created and modified within WordPress using hooks in plugins or themes. The XML-RPC API permits only reading of taxonomy metadata. 27 | 28 | Terms 29 | ----- 30 | 31 | Terms are the individual entries in a taxonomy. 32 | 33 | For example, to retrieve all blog categories:: 34 | 35 | categories = client.call(taxonomies.GetTerms('category')) 36 | 37 | And to create a new tag:: 38 | 39 | from wordpress_xmlrpc import WordPressTerm 40 | 41 | tag = WordPressTerm() 42 | tag.taxonomy = 'post_tag' 43 | tag.name = 'My New Tag' 44 | tag.id = client.call(taxonomies.NewTerm(tag)) 45 | 46 | Or to create a child category:: 47 | 48 | parent_cat = client.call(taxonomies.GetTerm('category', 3)) 49 | 50 | child_cat = WordPressTerm() 51 | child_cat.taxonomy = 'category' 52 | child_cat.parent = parent_cat.id 53 | child_cat.name = 'My Child Category' 54 | child_cat.id = client.call(taxonomies.NewTerm(child_cat)) 55 | 56 | Terms and Posts 57 | --------------- 58 | 59 | Terms are of little use on their own, they must actually be assigned to posts. 60 | 61 | If you already have :class:`WordPressTerm` objects, use ``terms`` property of :class:`WordPressPost`:: 62 | 63 | tags = client.call(taxonomies.GetTerms('post_tag', {...})) 64 | 65 | post = WordPressPost() 66 | post.title = 'Post with Tags' 67 | post.content = '...' 68 | post.terms = tags 69 | post.id = client.call(posts.NewPost(post)) 70 | 71 | If you want to add a category to an existing post:: 72 | 73 | category = client.call(taxonomies.GetTerm('category', 3)) 74 | post = client.call(posts.GetPost(5)) 75 | 76 | post.terms.append(category) 77 | client.call(posts.EditPost(post.id, post)) 78 | 79 | But what if you have not yet retrieved the terms or want to create new terms? For that, you can use the ``terms_names`` property of :class:`WordPressPost`:: 80 | 81 | post = WordPressPost() 82 | post.title = 'Post with new tags' 83 | post.content = '...' 84 | post.terms_names = { 85 | 'post_tag': ['tagA', 'another tag'], 86 | 'category': ['My Child Category'], 87 | } 88 | post.id = client.call(posts.NewPost(post)) 89 | 90 | Note that ``terms_names`` is a dictionary with taxonomy names as keys and list of strings as values. WordPress will look for existing terms with these names or else create new ones. Be careful with hierarchical taxonomies like ``category`` because of potential name ambiguities (multiple terms can have the same name if they have different parents); if WordPress detects ambiguity, it will throw an error and ask that you use ``terms`` instead with a proper :class:`WordPressTerm`. 91 | 92 | Advanced Querying 93 | ----------------- 94 | 95 | By Count 96 | ~~~~~~~~ 97 | 98 | To find the 20 most-used tags:: 99 | 100 | tags = client.call(taxonomies.GetTerms('post_tag', {'number': 20, 'orderby': 'count', 'order': 'DESC'})) 101 | 102 | for tag in tags: 103 | print tag.name, tag.count 104 | 105 | Searching/Autocomplete 106 | ~~~~~~~~~~~~~~~~~~~~~~ 107 | 108 | To perform case-insensitive searching against term names, use the ``search`` option for ``filter``:: 109 | 110 | user_input = 'wor' # e.g., from UI textbox 111 | tags = client.call(taxonomies.GetTerms('post_tag', {'search': user_input, 'orderby': 'count', 'number': 5})) 112 | 113 | suggestions = [tag.name for tag in tags] 114 | # suggestions == ['word', 'WordPress', 'world'] 115 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to python-wordpress-xmlrpc's documentation! 2 | =================================================== 3 | 4 | Python library to interface with a WordPress blog's `XML-RPC API`__. 5 | 6 | __ http://codex.wordpress.org/XML-RPC_Support 7 | 8 | An implementation of the standard `WordPress API methods`__ is provided, 9 | but the library is designed for easy integration with custom 10 | XML-RPC API methods provided by plugins. 11 | 12 | __ http://codex.wordpress.org/XML-RPC_WordPress_API 13 | 14 | A set of :doc:`classes ` are provided that wrap the standard WordPress data 15 | types (e.g., Blog, Post, User). The provided :doc:`method implementations ` 16 | return these objects when possible. 17 | 18 | .. note:: 19 | 20 | In Wordpress 3.5+, the XML-RPC API is enabled by default and cannot be disabled. 21 | In Wordpress 0.70-3.42, the XML-RPC API is disabled by default. To enable it, 22 | go to Settings->Writing->Remote Publishing and check the box for XML-RPC. 23 | 24 | .. warning:: 25 | 26 | python-wordpress-xmlrpc 2.0+ is not fully backwards-compatible with 1.x versions of the library. 27 | 28 | Getting Started 29 | --------------- 30 | .. toctree:: 31 | :maxdepth: 2 32 | 33 | overview 34 | examples 35 | 36 | Reference 37 | --------- 38 | .. toctree:: 39 | :maxdepth: 2 40 | 41 | ref/client 42 | ref/wordpress 43 | ref/methods 44 | 45 | Internals/Development 46 | --------------------- 47 | .. toctree:: 48 | :maxdepth: 2 49 | 50 | dev/changelog 51 | dev/testing 52 | 53 | Indices and tables 54 | ================== 55 | 56 | * :ref:`genindex` 57 | * :ref:`modindex` 58 | * :ref:`search` 59 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-wordpress-xmlrpc.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-wordpress-xmlrpc.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/overview.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | Installation 5 | ------------ 6 | 7 | 1. Verify you meet the following requirements: 8 | 9 | * WordPress 3.4+ **OR** WordPress 3.0-3.3 with the `XML-RPC Modernization Plugin`__. 10 | 11 | __ http://wordpress.org/extend/plugins/xml-rpc-modernization/ 12 | 13 | * Python 2.6+ **OR** Python 3.x 14 | 15 | 2. Install from `PyPI`__ using ``easy_install python-wordpress-xmlrpc`` or ``pip install python-wordpress-xmlrpc``. 16 | 17 | __ http://pypi.python.org/pypi/python-wordpress-xmlrpc 18 | 19 | 20 | Quick Start 21 | ----------- 22 | 23 | Create an instance of the ``Client`` class with the URL of the 24 | WordPress XML-RPC endpoint and user credentials. Then pass an 25 | ``XmlrpcMethod`` object into its ``call`` method to execute the 26 | remote call and return the result. 27 | 28 | :: 29 | 30 | >>> from wordpress_xmlrpc import Client, WordPressPost 31 | >>> from wordpress_xmlrpc.methods.posts import GetPosts, NewPost 32 | >>> from wordpress_xmlrpc.methods.users import GetUserInfo 33 | 34 | >>> wp = Client('http://mysite.wordpress.com/xmlrpc.php', 'username', 'password') 35 | >>> wp.call(GetPosts()) 36 | [] 37 | 38 | >>> wp.call(GetUserInfo()) 39 | 40 | 41 | >>> post = WordPressPost() 42 | >>> post.title = 'My new title' 43 | >>> post.content = 'This is the body of my new post.' 44 | >>> post.terms_names = { 45 | >>> 'post_tag': ['test', 'firstpost'], 46 | >>> 'category': ['Introductions', 'Tests'] 47 | >>> } 48 | >>> wp.call(NewPost(post)) 49 | 5 50 | 51 | Notice that properties of ``WordPress`` objects are accessed directly, 52 | and not through the ``definition`` attribute defined in the source code. 53 | 54 | When a ``WordPress`` object is used as a method parameter, its ``struct`` 55 | parameter is automatically extracted for consumption by XML-RPC. However, 56 | if you use an object in a list or other embedded data structure used as 57 | a parameter, be sure to use ``obj.struct`` or else WordPress will not receive 58 | data in the format it expects. 59 | 60 | Custom XML-RPC Methods 61 | ~~~~~~~~~~~~~~~~~~~~~~ 62 | 63 | To interface with a non-standard XML-RPC method (such as one added 64 | by a plugin), you must simply extend ``wordpress_xmlrpc.XmlrpcMethod`` 65 | or one of its subclasses (``AnonymousMethod`` or ``AuthenticatedMethod``). 66 | 67 | The ``XmlrpcMethod`` class provides a number of properties which you 68 | can override to modify the behavior of the method call. 69 | 70 | Sample class to call a custom method added by a ficticious plugin:: 71 | 72 | from wordpress_xmlrpc import AuthenticatedMethod 73 | 74 | class MyCustomMethod(AuthenticatedMethod): 75 | method_name = 'custom.MyMethod' 76 | method_args = ('arg1', 'arg2') 77 | 78 | See :doc:`examples/custom-methods` for more details. -------------------------------------------------------------------------------- /docs/ref/client.rst: -------------------------------------------------------------------------------- 1 | Client 2 | ====== 3 | 4 | The :class:`Client` class is the gateway to your WordPress blog's XML-RPC interface. 5 | 6 | Once initialized with your blog URL and user credentials, the client object is ready 7 | to execute XML-RPC methods against your WordPress blog using its :meth:`Client.call` method. 8 | 9 | Client 10 | ------ 11 | .. class:: Client(url, username, password[, blog_id, transport]) 12 | 13 | :param url: URL of the blog's XML-RPC endpoint (e.g., http://www.example.com/xmlrpc.php) 14 | :param username: Username of a valid user account on the WordPress blog 15 | :param password: The password for this user account 16 | :param blog_id: The blog's ID (note: WordPress ignores this value, but it is retained for backwards compatibility) 17 | :param transport: Custom XML-RPC transport implementation. See `Python2`_ or `Python3`_ documentation. 18 | 19 | .. _Python2: https://docs.python.org/2/library/xmlrpclib.html#example-of-client-usage 20 | .. _Python3: https://docs.python.org/3/library/xmlrpc.client.html#example-of-client-usage 21 | 22 | .. method:: call(method) 23 | 24 | :param method: :class:`wordpress_xmlrpc.XmlrpcMethod`-derived class 25 | 26 | XML-RPC Method Classes 27 | ---------------------- 28 | 29 | .. automodule:: wordpress_xmlrpc 30 | 31 | .. autoclass:: XmlrpcMethod() 32 | :members: 33 | 34 | .. autoclass:: AnonymousMethod() 35 | 36 | .. autoclass:: AuthenticatedMethod() 37 | -------------------------------------------------------------------------------- /docs/ref/methods.rst: -------------------------------------------------------------------------------- 1 | Methods 2 | ======= 3 | 4 | See :doc:`/examples` for guidance on how to use the following method classes. 5 | 6 | methods.posts 7 | ------------- 8 | 9 | .. automodule:: wordpress_xmlrpc.methods.posts 10 | 11 | .. autoclass:: GetPosts([filter, fields]) 12 | .. autoclass:: GetPost(post_id[, fields]) 13 | .. autoclass:: NewPost(content) 14 | .. autoclass:: EditPost(post_id, content) 15 | .. autoclass:: DeletePost(post_id) 16 | .. autoclass:: GetPostStatusList() 17 | .. autoclass:: GetPostFormats() 18 | .. autoclass:: GetPostTypes() 19 | .. autoclass:: GetPostType(post_type) 20 | 21 | methods.pages 22 | ------------- 23 | 24 | .. automodule:: wordpress_xmlrpc.methods.pages 25 | 26 | .. autoclass:: GetPageStatusList() 27 | .. autoclass:: GetPageTemplates() 28 | 29 | methods.taxonomies 30 | ------------------ 31 | 32 | .. automodule:: wordpress_xmlrpc.methods.taxonomies 33 | 34 | .. autoclass:: GetTaxonomies() 35 | .. autoclass:: GetTaxonomy(taxonomy) 36 | .. autoclass:: GetTerms(taxonomy[, filter]) 37 | .. autoclass:: GetTerm(taxonomy, term_id) 38 | .. autoclass:: NewTerm(term) 39 | .. autoclass:: EditTerm(term_id, term) 40 | .. autoclass:: DeleteTerm(taxonomy, term_id) 41 | 42 | methods.comments 43 | ---------------- 44 | 45 | .. automodule:: wordpress_xmlrpc.methods.comments 46 | 47 | .. autoclass:: GetComments(filter) 48 | .. autoclass:: GetComment(comment_id) 49 | .. autoclass:: NewComment(post_id, comment) 50 | .. autoclass:: NewAnonymousComment(post_id, comment) 51 | .. autoclass:: EditComment(comment_id, comment) 52 | .. autoclass:: DeleteComment(comment_id) 53 | .. autoclass:: GetCommentStatusList() 54 | .. autoclass:: GetCommentCount(post_id) 55 | 56 | methods.users 57 | ------------- 58 | 59 | .. automodule:: wordpress_xmlrpc.methods.users 60 | 61 | .. autoclass:: GetUser(user_id[, fields]) 62 | .. autoclass:: GetUsers([filter, fields]) 63 | .. autoclass:: GetProfile() 64 | .. autoclass:: EditProfile(user) 65 | .. autoclass:: GetUsersBlogs() 66 | .. autoclass:: GetAuthors() 67 | 68 | methods.media 69 | ------------- 70 | 71 | .. automodule:: wordpress_xmlrpc.methods.media 72 | 73 | .. autoclass:: GetMediaLibrary([filter]) 74 | .. autoclass:: GetMediaItem(attachmend_id) 75 | .. autoclass:: UploadFile(data) 76 | 77 | methods.options 78 | --------------- 79 | 80 | .. automodule:: wordpress_xmlrpc.methods.options 81 | 82 | .. autoclass:: GetOptions(options) 83 | .. autoclass:: SetOptions(options) 84 | 85 | methods.demo 86 | ------------ 87 | 88 | .. automodule:: wordpress_xmlrpc.methods.demo 89 | 90 | .. autoclass:: SayHello() 91 | .. autoclass:: AddTwoNumbers(number1, number2) 92 | -------------------------------------------------------------------------------- /docs/ref/wordpress.rst: -------------------------------------------------------------------------------- 1 | WordPress Objects 2 | ================= 3 | 4 | WordPressPost 5 | ------------- 6 | .. class:: WordPressPost 7 | 8 | Represents a post, page, or other registered custom post type in WordPress. 9 | 10 | * id 11 | * user 12 | * date (`datetime`) 13 | * date_modified (`datetime`) 14 | * slug 15 | * post_status 16 | * title 17 | * content 18 | * excerpt 19 | * link 20 | * comment_status 21 | * ping_status 22 | * terms (`list` of :class:`WordPressTerm`\s) 23 | * terms_names (`dict`) 24 | * custom_fields (`dict`) 25 | * enclosure (`dict`) 26 | * password 27 | * post_format 28 | * thumbnail 29 | * sticky 30 | * post_type 31 | 32 | WordPressPage 33 | ~~~~~~~~~~~~~ 34 | .. class:: WordPressPage 35 | 36 | Derived from :class:`WordPressPost`, represents a WordPress page. Additional fields: 37 | 38 | * template 39 | * parent_id 40 | * parent_title 41 | * order (`int`) 42 | * post_type = 'page' 43 | 44 | WordPressPostType 45 | ----------------- 46 | .. class:: WordPressPostType 47 | 48 | Metadata for registered WordPress post type. 49 | 50 | * name 51 | * label 52 | * labels (`dict`) 53 | * cap (`dict`) 54 | * hierarchical 55 | * menu_icon 56 | * menu_position 57 | * public 58 | * show_in_menu 59 | * taxonomies (`list`) 60 | * is_builtin 61 | * supports (`list`) 62 | 63 | WordPressTaxonomy 64 | ----------------- 65 | .. class:: WordPressTaxonomy 66 | 67 | Metadata for registered WordPress taxonomy. 68 | 69 | * name 70 | * label 71 | * labels (`dict`) 72 | * hierarchical 73 | * public 74 | * show_ui 75 | * cap (`dict`) 76 | * is_builtin 77 | * object_type (`list`) 78 | 79 | WordPressTerm 80 | ------------- 81 | .. class:: WordPressTerm 82 | 83 | Represents a term (e.g., tag or category) in a WordPress taxonomy. 84 | 85 | * id 86 | * group 87 | * taxonomy 88 | * taxonomy_id 89 | * name 90 | * slug 91 | * description 92 | * parent 93 | * count (`int`) 94 | 95 | WordPressBlog 96 | ------------- 97 | .. class:: WordPressBlog 98 | 99 | Represents a WordPress blog/site. 100 | 101 | * id 102 | * name 103 | * url 104 | * xmlrpc 105 | * is_admin (`bool`) 106 | 107 | WordPressAuthor 108 | --------------- 109 | .. class:: WordPressAuthor 110 | 111 | Minimal representation of a WordPress post author. 112 | 113 | * id 114 | * user_login 115 | * display_name 116 | 117 | WordPressUser 118 | ------------- 119 | .. class:: WordPressUser 120 | 121 | Basic representation of a WordPress user. 122 | 123 | * id 124 | * username 125 | * password 126 | * roles 127 | * nickname 128 | * url 129 | * first_name 130 | * last_name 131 | * registered 132 | * bio 133 | * email 134 | * nicename 135 | * display_name 136 | 137 | WordPressComment 138 | ---------------- 139 | .. class:: WordPressComment 140 | 141 | Represents a WordPress comment. 142 | 143 | * id 144 | * user 145 | * post 146 | * post_title 147 | * parent 148 | * date_created (`datetime`) 149 | * status 150 | * content 151 | * link 152 | * author 153 | * author_url 154 | * author_email 155 | * author_ip 156 | 157 | WordPressMedia 158 | -------------- 159 | .. class:: WordPressMedia 160 | 161 | Represents a WordPress post media attachment. 162 | 163 | * id 164 | * parent 165 | * title 166 | * description 167 | * caption 168 | * date_created (`datetime`) 169 | * link 170 | * thumbnail 171 | * metadata 172 | 173 | WordPressOption 174 | --------------- 175 | .. class:: WordPressOption 176 | 177 | Represents a WordPress blog setting/option. 178 | 179 | * name 180 | * description 181 | * value 182 | * read_only (`bool`) 183 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | 8 | setup(name='python-wordpress-xmlrpc', 9 | version='2.3', 10 | description='WordPress XML-RPC API Integration Library', 11 | author='Max Cutler', 12 | author_email='max@maxcutler.com', 13 | url='https://github.com/maxcutler/python-wordpress-xmlrpc/', 14 | packages=['wordpress_xmlrpc', 'wordpress_xmlrpc.methods'], 15 | license='BSD', 16 | test_suite='nose.collector', 17 | classifiers=[ 18 | 'Programming Language :: Python', 19 | 'License :: OSI Approved :: BSD License', 20 | 'Operating System :: OS Independent', 21 | 'Development Status :: 4 - Beta', 22 | 'Intended Audience :: Developers', 23 | 'Environment :: Console', 24 | 'Environment :: Web Environment', 25 | 'Topic :: Software Development :: Libraries :: Python Modules', 26 | 'Topic :: Internet :: WWW/HTTP :: Site Management', 27 | 'Topic :: Utilities', 28 | 'Natural Language :: English', 29 | ], 30 | include_package_data=True, 31 | long_description=open('README.rst').read(), 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import collections 4 | 5 | from wordpress_xmlrpc import Client 6 | from wordpress_xmlrpc.compat import ConfigParser 7 | 8 | 9 | class WordPressTestCase(unittest.TestCase): 10 | 11 | def setUp(self): 12 | config = ConfigParser() 13 | with open('wp-config.cfg', 'r') as f: 14 | config.readfp(f) 15 | 16 | self.xmlrpc_url = config.get('wordpress', 'url') 17 | self.username = config.get('wordpress', 'username') 18 | self.userid = config.get('wordpress', 'userid') 19 | self.client = Client(self.xmlrpc_url, 20 | self.username, 21 | config.get('wordpress', 'password')) 22 | 23 | def assert_list_of_classes(self, lst, kls): 24 | """ 25 | Verifies that a list contains objects of a specific class. 26 | """ 27 | self.assertTrue(isinstance(lst, collections.Iterable)) 28 | 29 | for obj in lst: 30 | self.assertTrue(isinstance(obj, kls)) 31 | -------------------------------------------------------------------------------- /tests/files/wordpress_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxcutler/python-wordpress-xmlrpc/7ac0a6e9934fdbf02c2250932e0c026cf530d400/tests/files/wordpress_logo.png -------------------------------------------------------------------------------- /tests/test_categories.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import taxonomies, posts 6 | from wordpress_xmlrpc.wordpress import WordPressTerm, WordPressPost 7 | 8 | 9 | class TestCategories(WordPressTestCase): 10 | 11 | @attr('categories') 12 | def test_get_tags(self): 13 | tags = self.client.call(taxonomies.GetTerms('post_tag')) 14 | for tag in tags: 15 | self.assertTrue(isinstance(tag, WordPressTerm)) 16 | self.assertEquals(tag.taxonomy, 'post_tag') 17 | 18 | @attr('categories') 19 | def test_get_categories(self): 20 | cats = self.client.call(taxonomies.GetTerms('category')) 21 | for cat in cats: 22 | self.assertTrue(isinstance(cat, WordPressTerm)) 23 | self.assertEquals(cat.taxonomy, 'category') 24 | 25 | @attr('categories') 26 | def test_category_lifecycle(self): 27 | # Create category object 28 | cat = WordPressTerm() 29 | cat.name = 'Test Category' 30 | cat.taxonomy = 'category' 31 | 32 | # Create the category in WordPress 33 | cat_id = self.client.call(taxonomies.NewTerm(cat)) 34 | self.assertTrue(cat_id) 35 | cat.id = cat_id 36 | 37 | try: 38 | # Check that the new category shows in category suggestions 39 | suggestions = self.client.call(taxonomies.GetTerms('category', {'search': 'test'})) 40 | self.assertTrue(isinstance(suggestions, list)) 41 | found = False 42 | for suggestion in suggestions: 43 | if suggestion.id == cat_id: 44 | found = True 45 | break 46 | self.assertTrue(found) 47 | finally: 48 | # Delete the category 49 | response = self.client.call(taxonomies.DeleteTerm(cat.taxonomy, cat.id)) 50 | self.assertTrue(response) 51 | 52 | @attr('categories') 53 | def test_category_post(self): 54 | # create a test post 55 | post = WordPressPost() 56 | post.title = 'Test Post' 57 | post.slug = 'test-post' 58 | post.user = self.userid 59 | post.id = self.client.call(posts.NewPost(post)) 60 | 61 | # create a test category 62 | cat = WordPressTerm() 63 | cat.name = 'Test Category' 64 | cat.taxonomy = 'category' 65 | cat.id = self.client.call(taxonomies.NewTerm(cat)) 66 | 67 | # set category on post 68 | try: 69 | post.terms = [cat] 70 | response = self.client.call(posts.EditPost(post.id, post)) 71 | self.assertTrue(response) 72 | 73 | # fetch categories for the post to verify 74 | post2 = self.client.call(posts.GetPost(post.id, ['terms'])) 75 | post_cats = post2.terms 76 | self.assert_list_of_classes(post_cats, WordPressTerm) 77 | self.assertEqual(post_cats[0].id, cat.id) 78 | finally: 79 | # cleanup 80 | self.client.call(taxonomies.DeleteTerm(cat.taxonomy, cat.id)) 81 | self.client.call(posts.DeletePost(post.id)) 82 | -------------------------------------------------------------------------------- /tests/test_comments.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from nose.plugins.attrib import attr 4 | 5 | from tests import WordPressTestCase 6 | 7 | from wordpress_xmlrpc.methods import comments, posts 8 | from wordpress_xmlrpc.wordpress import WordPressComment, WordPressPost 9 | 10 | 11 | class TestComments(WordPressTestCase): 12 | 13 | def setUp(self): 14 | super(TestComments, self).setUp() 15 | 16 | post = WordPressPost() 17 | post.title = 'Comments Test Post' 18 | post.slug = 'comments-test-post' 19 | post.user = self.userid 20 | post.comment_status = 'open' 21 | self.post_id = self.client.call(posts.NewPost(post)) 22 | 23 | @attr('comments') 24 | @attr('pycompat') 25 | def test_comment_repr(self): 26 | comment = WordPressComment() 27 | repr(comment) 28 | 29 | @attr('comments') 30 | def test_get_comment_status_list(self): 31 | status_list = self.client.call(comments.GetCommentStatusList()) 32 | self.assertTrue(isinstance(status_list, dict)) 33 | 34 | @attr('comments') 35 | def test_comment_lifecycle(self): 36 | comment = WordPressComment() 37 | comment.content = 'This is a test comment.' 38 | comment.user = self.userid 39 | 40 | # create comment 41 | comment_id = self.client.call(comments.NewComment(self.post_id, comment)) 42 | self.assertTrue(comment_id) 43 | 44 | # edit comment 45 | comment.content = 'This is an edited comment.' 46 | response = self.client.call(comments.EditComment(comment_id, comment)) 47 | self.assertTrue(response) 48 | 49 | # delete comment 50 | response = self.client.call(comments.DeleteComment(comment_id)) 51 | self.assertTrue(response) 52 | 53 | @attr('comments') 54 | def test_post_comments(self): 55 | # create a bunch of comments 56 | comment_list = [] 57 | 58 | counter = 0 59 | for i in range(1, random.randint(6, 15)): 60 | comment = WordPressComment() 61 | comment.content = 'Comment #%s' % counter 62 | comment.user = self.userid 63 | 64 | comment_id = self.client.call(comments.NewComment(self.post_id, comment)) 65 | comment_list.append(comment_id) 66 | counter += 1 67 | 68 | # retrieve comment count 69 | comment_counts = self.client.call(comments.GetCommentCount(self.post_id)) 70 | self.assertEqual(comment_counts['total_comments'], counter) 71 | 72 | # fetch a subset of the comments 73 | num_comments = 5 74 | post_comments = self.client.call(comments.GetComments({'post_id': self.post_id, 'number': num_comments})) 75 | self.assert_list_of_classes(post_comments, WordPressComment) 76 | self.assertEqual(num_comments, len(post_comments)) 77 | 78 | # cleanup 79 | for comment in comment_list: 80 | self.client.call(comments.DeleteComment(comment)) 81 | 82 | def tearDown(self): 83 | self.client.call(posts.DeletePost(self.post_id)) 84 | super(WordPressTestCase, self).tearDown() 85 | -------------------------------------------------------------------------------- /tests/test_demo.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import demo 6 | 7 | 8 | class TestDemo(WordPressTestCase): 9 | 10 | @attr('demo') 11 | def test_say_hello(self): 12 | response = self.client.call(demo.SayHello()) 13 | self.assertEqual(response, "Hello!") 14 | 15 | @attr('demo') 16 | def test_add_two_numbers(self): 17 | sum = self.client.call(demo.AddTwoNumbers(2, 3)) 18 | self.assertEqual(sum, 5) 19 | -------------------------------------------------------------------------------- /tests/test_fieldmaps.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | from nose.plugins.attrib import attr 5 | 6 | from wordpress_xmlrpc.compat import xmlrpc_client 7 | from wordpress_xmlrpc.exceptions import FieldConversionError 8 | from wordpress_xmlrpc.fieldmaps import DateTimeFieldMap 9 | from wordpress_xmlrpc.wordpress import WordPressBase 10 | 11 | 12 | class SampleModel(WordPressBase): 13 | definition = { 14 | 'date_created': DateTimeFieldMap('dateCreated'), 15 | } 16 | 17 | 18 | class FieldMapsTestCase(unittest.TestCase): 19 | 20 | @attr('fieldmaps') 21 | def test_date_conversion(self): 22 | response = { 23 | 'dateCreated': xmlrpc_client.DateTime('20111009T08:07:06'), 24 | } 25 | obj = SampleModel(response) 26 | 27 | self.assertTrue(hasattr(obj, 'date_created')) 28 | self.assertTrue(isinstance(obj.date_created, datetime.datetime)) 29 | self.assertTrue(isinstance(obj.struct['dateCreated'], xmlrpc_client.DateTime)) 30 | 31 | @attr('fieldmaps') 32 | def test_malformed_date_conversion(self): 33 | response = { 34 | 'dateCreated': xmlrpc_client.DateTime('-0001113TT0::0::00'), 35 | } 36 | self.assertRaises(FieldConversionError, SampleModel, response) 37 | 38 | @attr('fieldmaps') 39 | def test_empty_date_with_timezone_is_accepted(self): 40 | response = { 41 | 'dateCreated': xmlrpc_client.DateTime('00000000T00:00:00Z'), 42 | } 43 | obj = SampleModel(response) 44 | self.assertTrue(hasattr(obj, 'date_created')) 45 | self.assertEqual(obj.date_created, datetime.datetime(1, 1, 1, 0, 0)) 46 | -------------------------------------------------------------------------------- /tests/test_media.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | 3 | from nose.plugins.attrib import attr 4 | 5 | from tests import WordPressTestCase 6 | 7 | from wordpress_xmlrpc.compat import xmlrpc_client 8 | from wordpress_xmlrpc.methods import media 9 | from wordpress_xmlrpc.wordpress import WordPressMedia 10 | 11 | 12 | class TestMedia(WordPressTestCase): 13 | 14 | @attr('media') 15 | @attr('pycompat') 16 | def test_media_repr(self): 17 | media = WordPressMedia() 18 | repr(media) 19 | 20 | @attr('media') 21 | def test_get_media_library(self): 22 | num_items = 10 23 | library = self.client.call(media.GetMediaLibrary({'number': num_items})) 24 | self.assert_list_of_classes(library, WordPressMedia) 25 | self.assertTrue(len(library) <= num_items) 26 | 27 | @attr('media') 28 | def test_get_media_item(self): 29 | # this method cannot yet be tested, as it's impossible to get an ID to query against 30 | # media_item = self.client.call(media.GetMediaItem(__)) 31 | # self.assertTrue(isinstance(media_item, WordPressMedia)) 32 | pass 33 | 34 | @attr('media') 35 | def test_upload_file(self): 36 | # note: due to limitations in the XML-RPC API, this test will always create a new media item 37 | filename = 'wordpress_logo.png' 38 | with open('tests/files/' + filename, "rb") as img: 39 | data = { 40 | 'name': filename, 41 | 'bits': xmlrpc_client.Binary(img.read()), 42 | 'type': mimetypes.read_mime_types(filename) or mimetypes.guess_type(filename)[0], 43 | } 44 | response = self.client.call(media.UploadFile(data)) 45 | self.assertTrue(isinstance(response, dict)) 46 | -------------------------------------------------------------------------------- /tests/test_options.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import options 6 | from wordpress_xmlrpc.wordpress import WordPressOption 7 | 8 | 9 | class TestOptions(WordPressTestCase): 10 | 11 | @attr('options') 12 | @attr('pycompat') 13 | def test_option_repr(self): 14 | option = WordPressOption() 15 | repr(option) 16 | 17 | @attr('options') 18 | def test_get_all_options(self): 19 | opts = self.client.call(options.GetOptions([])) 20 | self.assert_list_of_classes(opts, WordPressOption) 21 | self.assertTrue(len(opts) > 0) 22 | 23 | @attr('options') 24 | def test_get_specific_options(self): 25 | desired_opts = ['date_format', 'time_format'] 26 | opts = self.client.call(options.GetOptions(desired_opts)) 27 | self.assert_list_of_classes(opts, WordPressOption) 28 | self.assertEqual(len(desired_opts), len(opts)) 29 | self.assertEqual(set(desired_opts), set([opt.name for opt in opts])) 30 | 31 | @attr('options') 32 | def test_set_option(self): 33 | # get current value 34 | old_tagline = self.client.call(options.GetOptions('blog_tagline'))[0].value 35 | 36 | # change value 37 | new_tagline = 'New tagline' 38 | opts = self.client.call(options.SetOptions({'blog_tagline': new_tagline})) 39 | self.assertEqual(opts[0].value, new_tagline) 40 | 41 | # set back to original value 42 | response = self.client.call(options.SetOptions({'blog_tagline': old_tagline})) 43 | self.assertTrue(response) 44 | -------------------------------------------------------------------------------- /tests/test_pages.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import pages, posts 6 | from wordpress_xmlrpc.wordpress import WordPressPage 7 | 8 | 9 | class TestPages(WordPressTestCase): 10 | 11 | @attr('pages') 12 | @attr('pycompat') 13 | def test_page_repr(self): 14 | page = WordPressPage() 15 | repr(page) 16 | 17 | @attr('pages') 18 | def test_get_page_status_list(self): 19 | status_list = self.client.call(pages.GetPageStatusList()) 20 | self.assertTrue(isinstance(status_list, dict)) 21 | 22 | @attr('pages') 23 | def test_get_page_templates(self): 24 | templates = self.client.call(pages.GetPageTemplates()) 25 | self.assertTrue(isinstance(templates, dict)) 26 | self.assertTrue('Default' in templates) 27 | 28 | @attr('pages') 29 | def test_get_pages(self): 30 | page_list = self.client.call(posts.GetPosts({'post_type': 'page'}, results_class=WordPressPage)) 31 | self.assert_list_of_classes(page_list, WordPressPage) 32 | 33 | @attr('pages') 34 | def test_page_lifecycle(self): 35 | page = WordPressPage() 36 | page.title = 'Test Page' 37 | page.content = 'This is my test page.' 38 | 39 | # create the page 40 | page_id = self.client.call(posts.NewPost(page)) 41 | self.assertTrue(page_id) 42 | 43 | # fetch the newly created page 44 | page2 = self.client.call(posts.GetPost(page_id, results_class=WordPressPage)) 45 | self.assertTrue(isinstance(page2, WordPressPage)) 46 | self.assertEqual(str(page2.id), page_id) 47 | 48 | # edit the page 49 | page2.content += '
Updated: This page has been updated.' 50 | response = self.client.call(posts.EditPost(page_id, page2)) 51 | self.assertTrue(response) 52 | 53 | # delete the page 54 | response = self.client.call(posts.DeletePost(page_id)) 55 | self.assertTrue(response) 56 | 57 | @attr('pages') 58 | def test_page_parent(self): 59 | parent_page = WordPressPage() 60 | parent_page.title = 'Parent page' 61 | parent_page.content = 'This is the parent page' 62 | parent_page.id = self.client.call(posts.NewPost(parent_page)) 63 | self.assertTrue(parent_page.id) 64 | 65 | child_page = WordPressPage() 66 | child_page.title = 'Child page' 67 | child_page.content = 'This is the child page' 68 | child_page.parent_id = parent_page.id 69 | child_page.id = self.client.call(posts.NewPost(child_page)) 70 | self.assertTrue(child_page.id) 71 | 72 | # re-fetch to confirm parent_id worked 73 | child_page2 = self.client.call(posts.GetPost(child_page.id)) 74 | self.assertTrue(child_page2) 75 | self.assertEquals(child_page.parent_id, child_page2.parent_id) 76 | 77 | # cleanup 78 | self.client.call(posts.DeletePost(parent_page.id)) 79 | self.client.call(posts.DeletePost(child_page.id)) 80 | -------------------------------------------------------------------------------- /tests/test_posts.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import posts 6 | from wordpress_xmlrpc.wordpress import WordPressPost, WordPressPostType 7 | 8 | 9 | class TestPosts(WordPressTestCase): 10 | 11 | @attr('posts') 12 | @attr('pycompat') 13 | def test_post_repr(self): 14 | post = WordPressPost() 15 | repr(post) 16 | 17 | @attr('posts') 18 | def test_get_post_status_list(self): 19 | status_list = self.client.call(posts.GetPostStatusList()) 20 | self.assertTrue(isinstance(status_list, dict)) 21 | 22 | @attr('posts') 23 | def test_get_post_formats(self): 24 | formats = self.client.call(posts.GetPostFormats()) 25 | self.assertTrue(isinstance(formats, dict)) 26 | self.assertTrue('all' in formats) 27 | self.assertTrue('supported' in formats) 28 | 29 | @attr('posts') 30 | def test_get_posts(self): 31 | num_posts = 10 32 | recent_posts = self.client.call(posts.GetPosts({'number': num_posts})) 33 | self.assert_list_of_classes(recent_posts, WordPressPost) 34 | self.assertTrue(len(recent_posts) <= num_posts) 35 | 36 | @attr('posts') 37 | def test_post_lifecycle(self): 38 | # create a post object 39 | post = WordPressPost() 40 | post.title = 'Test post' 41 | post.slug = 'test-post' 42 | post.content = 'This is test post using the XML-RPC API.' 43 | post.comment_status = 'open' 44 | post.user = self.userid 45 | 46 | # create the post as a draft 47 | post_id = self.client.call(posts.NewPost(post)) 48 | self.assertTrue(post_id) 49 | 50 | # fetch the newly-created post 51 | post2 = self.client.call(posts.GetPost(post_id)) 52 | self.assertTrue(isinstance(post2, WordPressPost)) 53 | self.assertEqual(str(post2.id), post_id) 54 | 55 | # update the post 56 | post2.content += '
Updated: This post has been updated.' 57 | post2.post_status = 'publish' 58 | response = self.client.call(posts.EditPost(post_id, post2)) 59 | self.assertTrue(response) 60 | 61 | # delete the post 62 | response = self.client.call(posts.DeletePost(post_id)) 63 | self.assertTrue(response) 64 | 65 | @attr('post_types') 66 | def test_get_post_types(self): 67 | post_types = self.client.call(posts.GetPostTypes()) 68 | self.assert_list_of_classes(post_types.values(), WordPressPostType) 69 | for name, post_type in post_types.items(): 70 | self.assertEqual(name, post_type.name) 71 | 72 | @attr('post_types') 73 | def test_get_post_type(self): 74 | post_type = self.client.call(posts.GetPostType('post')) 75 | self.assertTrue(isinstance(post_type, WordPressPostType)) 76 | 77 | @attr('posts') 78 | @attr('revisions') 79 | def test_revisions(self): 80 | original_title = 'Revisions test' 81 | post = WordPressPost() 82 | post.title = original_title 83 | post.slug = 'revisions-test' 84 | post.content = 'This is a test post using the XML-RPC API.' 85 | post_id = self.client.call(posts.NewPost(post)) 86 | self.assertTrue(post_id) 87 | 88 | post.title = 'Revisions test updated' 89 | post.content += ' This is a second revision.' 90 | response = self.client.call(posts.EditPost(post_id, post)) 91 | self.assertTrue(response) 92 | 93 | # test wp.getRevisions 94 | revision_list = self.client.call(posts.GetRevisions(post_id, ['post'])) 95 | self.assert_list_of_classes(revision_list, WordPressPost) 96 | 97 | # workaround for WP bug #22686/22687 98 | # an auto-draft revision will survive wp.newPost, so pick the 2nd revision 99 | self.assertEqual(2, len(revision_list)) 100 | real_rev = revision_list[1] 101 | self.assertTrue(real_rev) 102 | self.assertNotEquals(post_id, real_rev.id) 103 | 104 | # test wp.restoreRevision 105 | response2 = self.client.call(posts.RestoreRevision(real_rev.id)) 106 | self.assertTrue(response2) 107 | post2 = self.client.call(posts.GetPost(post_id)) 108 | self.assertEquals(original_title, post2.title) 109 | 110 | # cleanup 111 | self.client.call(posts.DeletePost(post_id)) 112 | -------------------------------------------------------------------------------- /tests/test_taxonomies.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import taxonomies 6 | from wordpress_xmlrpc.wordpress import WordPressTaxonomy, WordPressTerm 7 | 8 | 9 | class TestTaxonomies(WordPressTestCase): 10 | 11 | @attr('taxonomies') 12 | @attr('pycompat') 13 | def test_taxonomy_repr(self): 14 | tax = WordPressTaxonomy() 15 | repr(tax) 16 | 17 | @attr('taxonomies') 18 | @attr('pycompat') 19 | def test_term_repr(self): 20 | term = WordPressTerm() 21 | repr(term) 22 | 23 | @attr('taxonomies') 24 | def test_get_taxonomies(self): 25 | taxs = self.client.call(taxonomies.GetTaxonomies()) 26 | self.assert_list_of_classes(taxs, WordPressTaxonomy) 27 | 28 | @attr('taxonomies') 29 | def test_get_taxonomy(self): 30 | tax = self.client.call(taxonomies.GetTaxonomy('category')) 31 | self.assertTrue(isinstance(tax, WordPressTaxonomy)) 32 | 33 | @attr('taxonomies') 34 | def test_taxonomy_fields_match(self): 35 | """ 36 | Check that the fields returned by singular and plural versions are the same. 37 | """ 38 | tax1 = self.client.call(taxonomies.GetTaxonomy('category')) 39 | tax2 = None 40 | 41 | # find category taxonomy in the list of all taxonomies 42 | taxs = self.client.call(taxonomies.GetTaxonomies()) 43 | for tax in taxs: 44 | if tax.name == 'category': 45 | tax2 = tax 46 | break 47 | self.assertTrue(tax2 is not None) 48 | 49 | # compare the two field-by-field 50 | for field in tax1.definition.keys(): 51 | self.assertEqual(getattr(tax1, field), getattr(tax2, field)) 52 | 53 | @attr('taxonomies') 54 | @attr('terms') 55 | def test_get_terms(self): 56 | terms = self.client.call(taxonomies.GetTerms('category')) 57 | self.assert_list_of_classes(terms, WordPressTerm) 58 | 59 | @attr('taxonomies') 60 | @attr('terms') 61 | def test_get_term(self): 62 | term = self.client.call(taxonomies.GetTerm('category', 1)) 63 | self.assertTrue(isinstance(term, WordPressTerm)) 64 | 65 | @attr('taxonomies') 66 | @attr('terms') 67 | def test_term_lifecycle(self): 68 | term = WordPressTerm() 69 | term.name = 'Test Term' 70 | term.taxonomy = 'category' 71 | 72 | # create the term 73 | term_id = self.client.call(taxonomies.NewTerm(term)) 74 | self.assertTrue(term_id) 75 | term.id = term_id 76 | 77 | # re-fetch to verify 78 | term2 = self.client.call(taxonomies.GetTerm(term.taxonomy, term.id)) 79 | self.assertEqual(term.name, term2.name) 80 | 81 | # set a description and save 82 | term.description = "My test term" 83 | response = self.client.call(taxonomies.EditTerm(term.id, term)) 84 | self.assertTrue(response) 85 | 86 | # re-fetch to verify 87 | term3 = self.client.call(taxonomies.GetTerm(term.taxonomy, term.id)) 88 | self.assertEqual(term.description, term3.description) 89 | 90 | # delete the term 91 | response = self.client.call(taxonomies.DeleteTerm(term.taxonomy, term.id)) 92 | self.assertTrue(response) 93 | 94 | @attr('taxonomies') 95 | @attr('terms') 96 | def test_term_parent_child(self): 97 | parent = WordPressTerm() 98 | parent.taxonomy = 'category' 99 | parent.name = 'Test Parent Term' 100 | 101 | parent_id = self.client.call(taxonomies.NewTerm(parent)) 102 | self.assertTrue(parent_id) 103 | parent.id = parent_id 104 | 105 | child = WordPressTerm() 106 | child.taxonomy = parent.taxonomy 107 | child.name = 'Test Child Term' 108 | child.parent = parent.id 109 | 110 | child_id = self.client.call(taxonomies.NewTerm(child)) 111 | self.assertTrue(child_id) 112 | child.id = child_id 113 | 114 | try: 115 | # re-fetch to verify 116 | child2 = self.client.call(taxonomies.GetTerm(child.taxonomy, child.id)) 117 | self.assertEqual(child.parent, child2.parent) 118 | finally: 119 | # cleanup 120 | self.client.call(taxonomies.DeleteTerm(child.taxonomy, child.id)) 121 | self.client.call(taxonomies.DeleteTerm(parent.taxonomy, parent.id)) 122 | 123 | @attr('taxonomies') 124 | @attr('terms') 125 | def test_term_search(self): 126 | tag1 = WordPressTerm() 127 | tag1.taxonomy = 'post_tag' 128 | tag1.name = 'Test FoobarA' 129 | 130 | tag1_id = self.client.call(taxonomies.NewTerm(tag1)) 131 | self.assertTrue(tag1_id) 132 | tag1.id = tag1_id 133 | 134 | tag2 = WordPressTerm() 135 | tag2.taxonomy = 'post_tag' 136 | tag2.name = 'Test FoobarB' 137 | 138 | tag2_id = self.client.call(taxonomies.NewTerm(tag2)) 139 | self.assertTrue(tag2_id) 140 | tag2.id = tag2_id 141 | 142 | try: 143 | results = self.client.call(taxonomies.GetTerms('post_tag', {'search': 'foobarb'})) 144 | found_tag1 = False 145 | found_tag2 = False 146 | for tag in results: 147 | if tag.id == tag1_id: 148 | found_tag1 = True 149 | elif tag.id == tag2_id: 150 | found_tag2 = True 151 | self.assertFalse(found_tag1) 152 | self.assertTrue(found_tag2) 153 | finally: 154 | # cleanup 155 | self.client.call(taxonomies.DeleteTerm(tag1.taxonomy, tag1.id)) 156 | self.client.call(taxonomies.DeleteTerm(tag2.taxonomy, tag2.id)) 157 | -------------------------------------------------------------------------------- /tests/test_users.py: -------------------------------------------------------------------------------- 1 | from nose.plugins.attrib import attr 2 | 3 | from tests import WordPressTestCase 4 | 5 | from wordpress_xmlrpc.methods import users 6 | from wordpress_xmlrpc.wordpress import WordPressUser, WordPressBlog, WordPressAuthor 7 | 8 | 9 | class TestUsers(WordPressTestCase): 10 | 11 | @attr('users') 12 | @attr('pycompat') 13 | def test_user_repr(self): 14 | user = WordPressUser() 15 | repr(user) 16 | 17 | @attr('users') 18 | @attr('pycompat') 19 | def test_author_repr(self): 20 | author = WordPressAuthor() 21 | repr(author) 22 | 23 | @attr('users') 24 | def test_get_user(self): 25 | user = self.client.call(users.GetUser(self.userid)) 26 | self.assertTrue(isinstance(user, WordPressUser)) 27 | self.assertEqual(user.username, self.username) 28 | 29 | @attr('users') 30 | def test_get_users(self): 31 | user_list = self.client.call(users.GetUsers()) 32 | self.assert_list_of_classes(user_list, WordPressUser) 33 | found = False 34 | for user in user_list: 35 | if user.id == self.userid: 36 | found = True 37 | break 38 | self.assertTrue(found) 39 | 40 | @attr('users') 41 | def test_get_profile(self): 42 | user = self.client.call(users.GetProfile()) 43 | self.assertTrue(isinstance(user, WordPressUser)) 44 | self.assertEqual(user.username, self.username) 45 | 46 | @attr('users') 47 | def test_edit_profile(self): 48 | user = self.client.call(users.GetProfile()) 49 | self.assertTrue(isinstance(user, WordPressUser)) 50 | old_first_name = user.first_name 51 | new_first_name = 'Foo bar' 52 | user.first_name = new_first_name 53 | result = self.client.call(users.EditProfile(user)) 54 | self.assertTrue(result) 55 | 56 | # check that the value changed 57 | user2 = self.client.call(users.GetProfile()) 58 | self.assertEqual(new_first_name, user2.first_name) 59 | 60 | # cleanup 61 | user.first_name = old_first_name 62 | self.client.call(users.EditProfile(user)) 63 | 64 | @attr('users') 65 | def test_get_user_blogs(self): 66 | blogs = self.client.call(users.GetUsersBlogs()) 67 | self.assert_list_of_classes(blogs, WordPressBlog) 68 | 69 | @attr('users') 70 | def test_get_authors(self): 71 | authors = self.client.call(users.GetAuthors()) 72 | self.assert_list_of_classes(authors, WordPressAuthor) 73 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Library to interface with the WordPress XML-RPC API. 3 | 4 | See README for usage instructions. 5 | """ 6 | from wordpress_xmlrpc.base import * 7 | from wordpress_xmlrpc.wordpress import * 8 | from . import methods 9 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/base.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import sys 3 | 4 | from wordpress_xmlrpc.compat import xmlrpc_client, dict_type 5 | from wordpress_xmlrpc.exceptions import ServerConnectionError, UnsupportedXmlrpcMethodError, InvalidCredentialsError, XmlrpcDisabledError 6 | 7 | 8 | class Client(object): 9 | """ 10 | Connection to a WordPress XML-RPC API endpoint. 11 | 12 | To execute XML-RPC methods, pass an instance of an 13 | `XmlrpcMethod`-derived class to `Client`'s `call` method. 14 | """ 15 | 16 | def __init__(self, url, username, password, blog_id=0, transport=None, verbose=False): 17 | self.url = url 18 | self.username = username 19 | self.password = password 20 | self.blog_id = blog_id 21 | 22 | try: 23 | self.server = xmlrpc_client.ServerProxy(url, allow_none=True, transport=transport, 24 | verbose=verbose) 25 | self.supported_methods = self.server.mt.supportedMethods() 26 | except xmlrpc_client.ProtocolError: 27 | e = sys.exc_info()[1] 28 | raise ServerConnectionError(repr(e)) 29 | 30 | def call(self, method): 31 | if method.method_name not in self.supported_methods: 32 | raise UnsupportedXmlrpcMethodError(method.method_name) 33 | 34 | server_method = getattr(self.server, method.method_name) 35 | args = method.get_args(self) 36 | 37 | try: 38 | raw_result = server_method(*args) 39 | except xmlrpc_client.Fault: 40 | e = sys.exc_info()[1] 41 | if e.faultCode == 403: 42 | raise InvalidCredentialsError(e.faultString) 43 | elif e.faultCode == 405: 44 | raise XmlrpcDisabledError(e.faultString) 45 | else: 46 | raise 47 | return method.process_result(raw_result) 48 | 49 | 50 | class XmlrpcMethod(object): 51 | """ 52 | Base class for XML-RPC methods. 53 | 54 | Child classes can override methods and properties to customize behavior: 55 | 56 | Properties: 57 | * `method_name`: XML-RPC method name (e.g., 'wp.getUserInfo') 58 | * `method_args`: Tuple of method-specific required parameters 59 | * `optional_args`: Tuple of method-specific optional parameters 60 | * `results_class`: Python class which will convert an XML-RPC response dict into an object 61 | """ 62 | method_name = None 63 | method_args = tuple() 64 | optional_args = tuple() 65 | results_class = None 66 | 67 | def __init__(self, *args, **kwargs): 68 | if self.method_args or self.optional_args: 69 | if self.optional_args: 70 | max_num_args = len(self.method_args) + len(self.optional_args) 71 | if not (len(self.method_args) <= len(args) <= max_num_args): 72 | raise ValueError("Invalid number of parameters to %s" % self.method_name) 73 | else: 74 | if len(args) != len(self.method_args): 75 | raise ValueError("Invalid number of parameters to %s" % self.method_name) 76 | 77 | for i, arg_name in enumerate(self.method_args): 78 | setattr(self, arg_name, args[i]) 79 | 80 | if self.optional_args: 81 | for i, arg_name in enumerate(self.optional_args, start=len(self.method_args)): 82 | if i >= len(args): 83 | break 84 | setattr(self, arg_name, args[i]) 85 | 86 | if 'results_class' in kwargs: 87 | self.results_class = kwargs['results_class'] 88 | 89 | def default_args(self, client): 90 | """ 91 | Builds set of method-non-specific arguments. 92 | """ 93 | return tuple() 94 | 95 | def get_args(self, client): 96 | """ 97 | Builds final set of XML-RPC method arguments based on 98 | the method's arguments, any default arguments, and their 99 | defined respective ordering. 100 | """ 101 | default_args = self.default_args(client) 102 | 103 | if self.method_args or self.optional_args: 104 | optional_args = getattr(self, 'optional_args', tuple()) 105 | args = [] 106 | for arg in (self.method_args + optional_args): 107 | if hasattr(self, arg): 108 | obj = getattr(self, arg) 109 | if hasattr(obj, 'struct'): 110 | args.append(obj.struct) 111 | else: 112 | args.append(obj) 113 | args = list(default_args) + args 114 | else: 115 | args = default_args 116 | 117 | return args 118 | 119 | def process_result(self, raw_result): 120 | """ 121 | Performs actions on the raw result from the XML-RPC response. 122 | 123 | If a `results_class` is defined, the response will be converted 124 | into one or more object instances of that class. 125 | """ 126 | if self.results_class and raw_result: 127 | if isinstance(raw_result, dict_type): 128 | return self.results_class(raw_result) 129 | elif isinstance(raw_result, collections.Iterable): 130 | return [self.results_class(result) for result in raw_result] 131 | 132 | return raw_result 133 | 134 | 135 | class AnonymousMethod(XmlrpcMethod): 136 | """ 137 | An XML-RPC method for which no authentication is required. 138 | """ 139 | pass 140 | 141 | 142 | class AuthenticatedMethod(XmlrpcMethod): 143 | """ 144 | An XML-RPC method for which user authentication is required. 145 | 146 | Blog ID, username and password details will be passed from 147 | the `Client` instance to the method call. 148 | """ 149 | 150 | def default_args(self, client): 151 | return (client.blog_id, client.username, client.password) 152 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/compat.py: -------------------------------------------------------------------------------- 1 | try: 2 | import xmlrpclib as xmlrpc_client # py2.x 3 | except ImportError: 4 | from xmlrpc import client as xmlrpc_client # py3.x 5 | 6 | 7 | import types 8 | try: 9 | dict_type = types.DictType # py2.x 10 | except AttributeError: 11 | dict_type = dict # py3.x 12 | 13 | 14 | try: 15 | from ConfigParser import ConfigParser # py2.x 16 | except ImportError: 17 | from configparser import ConfigParser # py3.x 18 | 19 | try: 20 | unicode('test') 21 | except NameError: 22 | def unicode(s): 23 | return s 24 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/exceptions.py: -------------------------------------------------------------------------------- 1 | class ServerConnectionError(Exception): 2 | """ 3 | An error while attempting to connect to the XML-RPC endpoint. 4 | """ 5 | pass 6 | 7 | 8 | class UnsupportedXmlrpcMethodError(Exception): 9 | """ 10 | An error while attempting to call a method that is not 11 | supported by the XML-RPC server. 12 | """ 13 | pass 14 | 15 | 16 | class XmlrpcDisabledError(Exception): 17 | """ 18 | An error when XML-RPC services are disabled in WordPress. 19 | """ 20 | pass 21 | 22 | 23 | class InvalidCredentialsError(Exception): 24 | """ 25 | An error when the XML-RPC server rejects the user's credentials 26 | (username/password combination). 27 | """ 28 | pass 29 | 30 | 31 | class FieldConversionError(Exception): 32 | """ 33 | An error while converting field Python value to XML-RPC value type. 34 | 35 | Attributes: 36 | `field_name`: name of the field 37 | `input_value`: value that was passed to the conversion function 38 | """ 39 | def __init__(self, field_name, error): 40 | self.field_name = field_name 41 | self.error = error 42 | 43 | def __str__(self): 44 | return repr(self) 45 | 46 | def __repr__(self): 47 | return self.field_name 48 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/fieldmaps.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from wordpress_xmlrpc.compat import xmlrpc_client 4 | 5 | 6 | class FieldMap(object): 7 | """ 8 | Container for settings mapping a WordPress XML-RPC request/response struct 9 | to a Python, programmer-friendly class. 10 | 11 | Parameters: 12 | `inputName`: name of the field in XML-RPC response. 13 | `outputNames`: (optional) list of field names to use when generating new XML-RPC request. defaults to `[inputName]` 14 | `default`: (optional) default value to use when none is supplied in XML-RPC response. defaults to `None` 15 | `conversion`: (optional) function to convert Python value to XML-RPC value for XML-RPC request. 16 | """ 17 | 18 | def __init__(self, inputName, outputNames=None, default=None, conversion=None): 19 | self.name = inputName 20 | self.output_names = outputNames or [inputName] 21 | self.default = default 22 | self.conversion = conversion 23 | 24 | def convert_to_python(self, xmlrpc=None): 25 | """ 26 | Extracts a value for the field from an XML-RPC response. 27 | """ 28 | if xmlrpc: 29 | return xmlrpc.get(self.name, self.default) 30 | elif self.default: 31 | return self.default 32 | else: 33 | return None 34 | 35 | def convert_to_xmlrpc(self, input_value): 36 | """ 37 | Convert a Python value to the expected XML-RPC value type. 38 | """ 39 | if self.conversion: 40 | return self.conversion(input_value) 41 | else: 42 | return input_value 43 | 44 | def get_outputs(self, input_value): 45 | """ 46 | Generate a set of output values for a given input. 47 | """ 48 | output_value = self.convert_to_xmlrpc(input_value) 49 | 50 | output = {} 51 | for name in self.output_names: 52 | output[name] = output_value 53 | 54 | return output 55 | 56 | 57 | class IntegerFieldMap(FieldMap): 58 | """ 59 | FieldMap pre-configured for handling integer fields. 60 | """ 61 | 62 | def __init__(self, *args, **kwargs): 63 | if 'conversion' not in kwargs: 64 | kwargs['conversion'] = int 65 | 66 | super(IntegerFieldMap, self).__init__(*args, **kwargs) 67 | 68 | 69 | class DateTimeFieldMap(FieldMap): 70 | """ 71 | FieldMap pre-configured for handling DateTime fields. 72 | """ 73 | 74 | def __init__(self, *args, **kwargs): 75 | if 'conversion' not in kwargs: 76 | kwargs['conversion'] = xmlrpc_client.DateTime 77 | 78 | super(DateTimeFieldMap, self).__init__(*args, **kwargs) 79 | 80 | def convert_to_python(self, xmlrpc=None): 81 | if xmlrpc: 82 | # make sure we have an `xmlrpc_client.DateTime` instance 83 | raw_value = xmlrpc.get(self.name, self.default) 84 | if not isinstance(raw_value, xmlrpc_client.DateTime): 85 | raw_value = xmlrpc_client.DateTime(raw_value) 86 | 87 | # extract its timetuple and convert to datetime 88 | try: 89 | tt = raw_value.timetuple() 90 | except ValueError: 91 | # Workaround for a combination of Python and WordPress bug 92 | # which would return a null date for Draft posts. This is not 93 | # the case for recent versions of WP, but drafts created a that 94 | # time still have a null date. 95 | # The python bug is http://bugs.python.org/issue2623 and 96 | # affects xmlrpclib when fed a timezone aware DateTime 97 | if str(raw_value) == "00000000T00:00:00Z": 98 | raw_value = xmlrpc_client.DateTime("00010101T00:00:00") 99 | tt = raw_value.timetuple() 100 | else: 101 | raise 102 | return datetime.datetime(*tuple(tt)[:6]) 103 | elif self.default: 104 | return self.default 105 | else: 106 | return None 107 | 108 | 109 | class TermsListFieldMap(FieldMap): 110 | """ 111 | FieldMap that converts to/from WordPress objects. 112 | """ 113 | def __init__(self, object_class, *args, **kwargs): 114 | self.object_class = object_class 115 | super(TermsListFieldMap, self).__init__(*args, **kwargs) 116 | 117 | def convert_to_python(self, xmlrpc=None): 118 | if xmlrpc and self.name in xmlrpc: 119 | values = [] 120 | for value in xmlrpc.get(self.name): 121 | values.append(self.object_class(value)) 122 | return values 123 | else: 124 | return [] 125 | 126 | def convert_to_xmlrpc(self, input_value): 127 | if input_value: 128 | values = {} 129 | for term in input_value: 130 | if term.taxonomy not in values: 131 | values[term.taxonomy] = [] 132 | values[term.taxonomy].append(int(term.id)) 133 | return values 134 | else: 135 | return None 136 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Implementations of standard WordPress XML-RPC APIs. 3 | """ 4 | from wordpress_xmlrpc.methods import posts 5 | from wordpress_xmlrpc.methods import pages 6 | from wordpress_xmlrpc.methods import demo 7 | from wordpress_xmlrpc.methods import users 8 | from wordpress_xmlrpc.methods import options 9 | from wordpress_xmlrpc.methods import comments 10 | from wordpress_xmlrpc.methods import media 11 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/comments.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | from wordpress_xmlrpc.wordpress import WordPressComment 3 | 4 | 5 | class GetComment(AuthenticatedMethod): 6 | """ 7 | Retrieve an individual comment. 8 | 9 | Parameters: 10 | `comment_id`: ID of the comment to retrieve. 11 | 12 | Returns: :class:`WordPressPost` instance. 13 | """ 14 | method_name = 'wp.getComment' 15 | method_args = ('comment_id',) 16 | results_class = WordPressComment 17 | 18 | 19 | class NewComment(AuthenticatedMethod): 20 | """ 21 | Create a new comment on a post. 22 | 23 | Parameters: 24 | `post_id`: The id of the post to add a comment to. 25 | `comment`: A :class:`WordPressComment` instance with at least the `content` value set. 26 | 27 | Returns: ID of the newly-created comment (an integer). 28 | """ 29 | method_name = 'wp.newComment' 30 | method_args = ('post_id', 'comment') 31 | 32 | 33 | class NewAnonymousComment(AnonymousMethod): 34 | """ 35 | Create a new comment on a post without authenticating. 36 | 37 | NOTE: Requires support on the blog by setting the following filter in a plugin or theme: 38 | 39 | add_filter( 'xmlrpc_allow_anonymous_comments', '__return_true' ); 40 | 41 | Parameters: 42 | `post_id`: The id of the post to add a comment to. 43 | `comment`: A :class:`WordPressComment` instance with at least the `content` value set. 44 | 45 | Returns: ID of the newly-created comment (an integer). 46 | """ 47 | method_name = 'wp.newComment' 48 | method_args = ('post_id', 'comment') 49 | 50 | 51 | class EditComment(AuthenticatedMethod): 52 | """ 53 | Edit an existing comment. 54 | 55 | Parameters: 56 | `comment_id`: The id of the comment to edit. 57 | `comment`: A :class:`WordPressComment` instance with at least the `content` value set. 58 | 59 | Returns: `True` on successful edit. 60 | """ 61 | method_name = 'wp.editComment' 62 | method_args = ('comment_id', 'comment') 63 | 64 | 65 | class DeleteComment(AuthenticatedMethod): 66 | """ 67 | Delete an existing comment. 68 | 69 | Parameters: 70 | `comment_id`: The id of the comment to be deleted. 71 | 72 | Returns: `True` on successful deletion. 73 | """ 74 | method_name = 'wp.deleteComment' 75 | method_args = ('comment_id',) 76 | 77 | 78 | class GetCommentStatusList(AuthenticatedMethod): 79 | """ 80 | Retrieve the set of possible blog comment statuses (e.g., "approve," "hold," "spam"). 81 | 82 | Parameters: 83 | None 84 | 85 | Returns: `dict` of values and their pretty names. 86 | 87 | Example: 88 | >>> client.call(GetCommentStatusList()) 89 | {'hold': 'Unapproved', 'approve': 'Approved', 'spam': 'Spam'} 90 | """ 91 | method_name = 'wp.getCommentStatusList' 92 | 93 | 94 | class GetCommentCount(AuthenticatedMethod): 95 | """ 96 | Retrieve comment count for a specific post. 97 | 98 | Parameters: 99 | `post_id`: The id of the post to retrieve comment count for. 100 | 101 | Returns: `dict` of comment counts for the post divided by comment status. 102 | 103 | Example: 104 | >>> client.call(GetCommentCount(1)) 105 | {'awaiting_moderation': '2', 'total_comments': 23, 'approved': '18', 'spam': 3} 106 | """ 107 | method_name = 'wp.getCommentCount' 108 | method_args = ('post_id',) 109 | 110 | 111 | class GetComments(AuthenticatedMethod): 112 | """ 113 | Gets a set of comments for a post. 114 | 115 | Parameters: 116 | `filter`: a `dict` with the following values: 117 | * `post_id`: the id of the post to retrieve comments for 118 | * `status`: type of comments of comments to retrieve (optional, defaults to 'approve') 119 | * `number`: number of comments to retrieve (optional, defaults to 10) 120 | * `offset`: retrieval offset (optional, defaults to 0) 121 | 122 | Returns: `list` of :class:`WordPressComment` instances. 123 | """ 124 | method_name = 'wp.getComments' 125 | method_args = ('filter',) 126 | results_class = WordPressComment 127 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/demo.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | 3 | 4 | class SayHello(AnonymousMethod): 5 | method_name = 'demo.sayHello' 6 | 7 | 8 | class AddTwoNumbers(AnonymousMethod): 9 | method_name = 'demo.addTwoNumbers' 10 | method_args = ('number1', 'number2') 11 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/media.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | from wordpress_xmlrpc.wordpress import WordPressMedia 3 | 4 | 5 | class GetMediaLibrary(AuthenticatedMethod): 6 | """ 7 | Retrieve filtered list of media library items. 8 | 9 | Parameters: 10 | `filter`: `dict` with optional keys: 11 | * `number`: number of media items to retrieve 12 | * `offset`: query offset 13 | * `parent_id`: ID of post the media item is attached to. 14 | Use empty string (default) to show all media items. 15 | Use `0` to show unattached media items. 16 | * `mime_type`: file mime-type to filter by (e.g., 'image/jpeg') 17 | 18 | Returns: `list` of :class:`WordPressMedia` instances. 19 | """ 20 | method_name = 'wp.getMediaLibrary' 21 | method_args = ('filter',) 22 | results_class = WordPressMedia 23 | 24 | 25 | class GetMediaItem(AuthenticatedMethod): 26 | """ 27 | Retrieve an individual media item. 28 | 29 | Parameters: 30 | `attachment_id`: ID of the media item. 31 | 32 | Returns: :class:`WordPressMedia` instance. 33 | """ 34 | method_name = 'wp.getMediaItem' 35 | method_args = ('attachment_id',) 36 | results_class = WordPressMedia 37 | 38 | 39 | class UploadFile(AuthenticatedMethod): 40 | """ 41 | Upload a file to the blog. 42 | 43 | Note: the file is not attached to or inserted into any blog posts. 44 | 45 | Parameters: 46 | `data`: `dict` with three items: 47 | * `name`: filename 48 | * `type`: MIME-type of the file 49 | * `bits`: base-64 encoded contents of the file. See xmlrpclib.Binary() 50 | * `overwrite` (optional): flag to override an existing file with this name 51 | 52 | Returns: `dict` with keys `id`, `file` (filename), `url` (public URL), and `type` (MIME-type). 53 | """ 54 | method_name = 'wp.uploadFile' 55 | method_args = ('data',) 56 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/options.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | from wordpress_xmlrpc.wordpress import WordPressOption 3 | 4 | 5 | class GetOptions(AuthenticatedMethod): 6 | """ 7 | Retrieve list of blog options. 8 | 9 | Parameters: 10 | `options`: `list` of option names to retrieve; if empty, all options will be retrieved 11 | 12 | Returns: `list` of :class:`WordPressOption` instances. 13 | """ 14 | method_name = 'wp.getOptions' 15 | method_args = ('options',) 16 | 17 | def process_result(self, options_dict): 18 | options = [] 19 | for key, value in options_dict.items(): 20 | value['name'] = key 21 | options.append(WordPressOption(value)) 22 | return options 23 | 24 | 25 | class SetOptions(GetOptions): 26 | """ 27 | Update the value of an existing blog option. 28 | 29 | Parameters: 30 | `options`: `dict` of key/value pairs 31 | 32 | Returns: `list` of :class:`WordPressOption` instances representing the updated options. 33 | """ 34 | method_name = 'wp.setOptions' 35 | method_args = ('options',) 36 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/pages.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | 3 | 4 | class GetPageStatusList(AuthenticatedMethod): 5 | """ 6 | Retrieve the set of possible blog page statuses (e.g., "draft," "private," "publish"). 7 | 8 | Parameters: 9 | None 10 | 11 | Returns: `dict` of values and their pretty names. 12 | 13 | Example: 14 | >>> client.call(GetPageStatusList()) 15 | {'draft': 'Draft', 'private': 'Private', 'publish': 'Published'} 16 | """ 17 | method_name = 'wp.getPageStatusList' 18 | 19 | 20 | class GetPageTemplates(AuthenticatedMethod): 21 | """ 22 | Retrieve the list of blog templates. 23 | 24 | Parameters: 25 | None 26 | 27 | Returns: `dict` of values and their paths. 28 | 29 | Example: 30 | >>> client.call(GetPageTemplates()) 31 | {'Default': 'default', 'Sidebar Template': 'sidebar-page.php', 'Showcase Template': 'showcase.php'} 32 | """ 33 | method_name = 'wp.getPageTemplates' 34 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/posts.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | from wordpress_xmlrpc.wordpress import WordPressPost, WordPressPostType 3 | 4 | 5 | class GetPosts(AuthenticatedMethod): 6 | """ 7 | Retrieve posts from the blog. 8 | 9 | Parameters: 10 | `filter`: optional `dict` of filters: 11 | * `number` 12 | * `offset` 13 | * `orderby` 14 | * `order`: 'ASC' or 'DESC' 15 | * `post_type`: Defaults to 'post' 16 | * `post_status` 17 | 18 | Returns: `list` of :class:`WordPressPost` instances. 19 | """ 20 | method_name = 'wp.getPosts' 21 | optional_args = ('filter', 'fields') 22 | results_class = WordPressPost 23 | 24 | 25 | class GetPost(AuthenticatedMethod): 26 | """ 27 | Retrieve an individual blog post. 28 | 29 | Parameters: 30 | `post_id`: ID of the blog post to retrieve. 31 | 32 | Returns: :class:`WordPressPost` instance. 33 | """ 34 | method_name = 'wp.getPost' 35 | method_args = ('post_id',) 36 | optional_args = ('fields',) 37 | results_class = WordPressPost 38 | 39 | 40 | class NewPost(AuthenticatedMethod): 41 | """ 42 | Create a new post on the blog. 43 | 44 | Parameters: 45 | `content`: A :class:`WordPressPost` instance with at least the `title` and `content` values set. 46 | 47 | Returns: ID of the newly-created blog post (an integer). 48 | """ 49 | method_name = 'wp.newPost' 50 | method_args = ('content',) 51 | 52 | 53 | class EditPost(AuthenticatedMethod): 54 | """ 55 | Edit an existing blog post. 56 | 57 | Parameters: 58 | `post_id`: ID of the blog post to edit. 59 | `content`: A :class:`WordPressPost` instance with the new values for the blog post. 60 | 61 | Returns: `True` on successful edit. 62 | """ 63 | method_name = 'wp.editPost' 64 | method_args = ('post_id', 'content') 65 | 66 | 67 | class DeletePost(AuthenticatedMethod): 68 | """ 69 | Delete a blog post. 70 | 71 | Parameters: 72 | `post_id`: ID of the blog post to delete. 73 | 74 | Returns: `True` on successful deletion. 75 | """ 76 | method_name = 'wp.deletePost' 77 | method_args = ('post_id', ) 78 | 79 | 80 | class GetPostStatusList(AuthenticatedMethod): 81 | """ 82 | Retrieve the set of possible blog post statuses (e.g., "draft," "private," "publish"). 83 | 84 | Parameters: 85 | None 86 | 87 | Returns: `dict` of values and their pretty names. 88 | 89 | Example: 90 | >>> client.call(GetPostStatusList()) 91 | {'draft': 'Draft', 'private': 'Private', 'pending': 'Pending Review', 'publish': 'Published'} 92 | """ 93 | method_name = 'wp.getPostStatusList' 94 | 95 | 96 | class GetPostFormats(AuthenticatedMethod): 97 | """ 98 | Retrieve the set of post formats used by the blog. 99 | 100 | Parameters: 101 | None 102 | 103 | Returns: `dict` containing a `dict` of all blog post formats (`all`) 104 | and a list of formats `supported` by the theme. 105 | 106 | Example: 107 | >>> client.call(GetPostFormats()) 108 | {'all': {'status': 'Status', 'quote': 'Quote', 'image': 'Image', 'aside': 'Aside', 'standard': 'Standard', 'link': 'Link', 'chat': 'Chat', 'video': 'Video', 'audio': 'Audio', 'gallery': 'Gallery'}, 109 | 'supported': ['aside', 'link', 'gallery', 'status', 'quote', 'image']} 110 | """ 111 | method_name = 'wp.getPostFormats' 112 | 113 | def get_args(self, client): 114 | args = super(GetPostFormats, self).get_args(client) 115 | args += ({'show-supported': True},) 116 | return args 117 | 118 | 119 | class GetPostTypes(AuthenticatedMethod): 120 | """ 121 | Retrieve a list of post types used by the blog. 122 | 123 | Parameters: 124 | None 125 | 126 | Returns: `dict` with names as keys and :class:`WordPressPostType` instances as values. 127 | """ 128 | method_name = 'wp.getPostTypes' 129 | results_class = WordPressPostType 130 | 131 | def process_result(self, raw_result): 132 | result = {} 133 | for name, raw_value in raw_result.items(): 134 | result[name] = self.results_class(raw_value) 135 | return result 136 | 137 | 138 | class GetPostType(AuthenticatedMethod): 139 | """ 140 | Retrieve an individual blog post type. 141 | 142 | Parameters: 143 | `post_type`: Name of the blog post type to retrieve. 144 | 145 | Returns: :class:`WordPressPostType` instance. 146 | """ 147 | method_name = 'wp.getPostType' 148 | method_args = ('post_type',) 149 | results_class = WordPressPostType 150 | 151 | 152 | class GetRevisions(AuthenticatedMethod): 153 | """ 154 | Retrieve all revisions of a post. 155 | 156 | Parameters: 157 | `post_id`: ID of the post. 158 | 159 | Returns: `list` of :class:`WordPressPost` instances. 160 | """ 161 | method_name = 'wp.getRevisions' 162 | method_args = ('post_id',) 163 | optional_args = ('fields',) 164 | results_class = WordPressPost 165 | 166 | 167 | class RestoreRevision(AuthenticatedMethod): 168 | """ 169 | Restores a post to a previous revision. 170 | 171 | Parameters: 172 | `revision_id`: ID of the revision to revert to. 173 | 174 | Returns: `True` on successful reversion. 175 | """ 176 | method_name = 'wp.restoreRevision' 177 | method_args = ('revision_id',) 178 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/taxonomies.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | from wordpress_xmlrpc.wordpress import WordPressTaxonomy, WordPressTerm 3 | 4 | 5 | class GetTaxonomies(AuthenticatedMethod): 6 | """ 7 | Retrieve the list of available taxonomies for the blog. 8 | 9 | Parameters: 10 | None 11 | 12 | Returns: `list` of :class:`WordPressTaxonomy` instances. 13 | """ 14 | method_name = 'wp.getTaxonomies' 15 | results_class = WordPressTaxonomy 16 | 17 | 18 | class GetTaxonomy(AuthenticatedMethod): 19 | """ 20 | Retrieve an individual taxonomy. 21 | 22 | Parameters: 23 | `taxonomy`: name of the taxonomy 24 | 25 | Returns: :class:`WordPressTaxonomy` instance. 26 | """ 27 | method_name = 'wp.getTaxonomy' 28 | method_args = ('taxonomy',) 29 | results_class = WordPressTaxonomy 30 | 31 | 32 | class GetTerms(AuthenticatedMethod): 33 | """ 34 | Retrieve the list of available terms for a taxonomy. 35 | 36 | Parameters: 37 | `taxonomy`: name of the taxonomy 38 | 39 | `filter`: optional `dict` of filters: 40 | * `number` 41 | * `offset` 42 | * `orderby` 43 | * `order`: 'ASC' or 'DESC' 44 | * `hide_empty`: Whether to return terms with count==0 45 | * `search`: Case-insensitive search on term names 46 | 47 | Returns: `list` of :class:`WordPressTerm` instances. 48 | """ 49 | method_name = 'wp.getTerms' 50 | method_args = ('taxonomy',) 51 | optional_args = ('filter',) 52 | results_class = WordPressTerm 53 | 54 | 55 | class GetTerm(AuthenticatedMethod): 56 | """ 57 | Retrieve an individual term. 58 | 59 | Parameters: 60 | `taxonomy`: name of the taxonomy 61 | 62 | `term_id`: ID of the term 63 | 64 | Returns: :class:`WordPressTerm` instance. 65 | """ 66 | method_name = 'wp.getTerm' 67 | method_args = ('taxonomy', 'term_id') 68 | results_class = WordPressTerm 69 | 70 | 71 | class NewTerm(AuthenticatedMethod): 72 | """ 73 | Create new term. 74 | 75 | Parameters: 76 | `term`: instance of :class:`WordPressTerm` 77 | 78 | Returns: ID of newly-created term (an integer). 79 | """ 80 | method_name = 'wp.newTerm' 81 | method_args = ('term',) 82 | 83 | 84 | class EditTerm(AuthenticatedMethod): 85 | """ 86 | Edit an existing term. 87 | 88 | Parameters: 89 | `term_id`: ID of the term to edit. 90 | 91 | `term`: A :class:`WordPressTerm` instance with the new values for the term. 92 | 93 | Returns: `True` on successful edit. 94 | """ 95 | method_name = 'wp.editTerm' 96 | method_args = ('term_id', 'term') 97 | 98 | 99 | class DeleteTerm(AuthenticatedMethod): 100 | """ 101 | Delete a term. 102 | 103 | Parameters: 104 | `taxonomy`: name of the taxonomy 105 | 106 | `term_id`: ID of the term to delete. 107 | 108 | Returns: `True` on successful deletion. 109 | """ 110 | method_name = 'wp.deleteTerm' 111 | method_args = ('taxonomy', 'term_id') 112 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/methods/users.py: -------------------------------------------------------------------------------- 1 | from wordpress_xmlrpc.base import * 2 | from wordpress_xmlrpc.wordpress import WordPressBlog, WordPressAuthor, WordPressUser 3 | 4 | 5 | class GetUsers(AuthenticatedMethod): 6 | """ 7 | Retrieve list of users in the blog. 8 | 9 | Parameters: 10 | `filter`: optional `dict` of filters: 11 | * `number` 12 | * `offset` 13 | * `role` 14 | 15 | `fields`: optional `list` of fields to return. Specific fields, or groups 'basic' or 'all'. 16 | 17 | Returns: `list` of :class:`WordPressUser` instances. 18 | """ 19 | method_name = 'wp.getUsers' 20 | optional_args = ('filter', 'fields') 21 | results_class = WordPressUser 22 | 23 | 24 | class GetUser(AuthenticatedMethod): 25 | """ 26 | Retrieve an individual user. 27 | 28 | Parameters: 29 | `user_id`: ID of the user 30 | `fields`: (optional) `list` of fields to return. Specific fields, or groups 'basic' or 'all'. 31 | 32 | Returns: :class:`WordPressUser` instance. 33 | """ 34 | method_name = 'wp.getUser' 35 | method_args = ('user_id',) 36 | optional_args = ('fields',) 37 | results_class = WordPressUser 38 | 39 | 40 | class GetProfile(AuthenticatedMethod): 41 | """ 42 | Retrieve information about the connected user. 43 | 44 | Parameters: 45 | None 46 | 47 | Returns: instance of :class:`WordPressUser` representing the user whose credentials are being used with the XML-RPC API. 48 | """ 49 | method_name = 'wp.getProfile' 50 | results_class = WordPressUser 51 | 52 | 53 | class EditProfile(AuthenticatedMethod): 54 | """ 55 | Edit profile fields of the connected user. 56 | 57 | Parameters: 58 | `user`: `WordPressUser` instance. 59 | 60 | Returns: `True` on successful edit. 61 | """ 62 | method_name = 'wp.editProfile' 63 | method_args = ('user',) 64 | 65 | 66 | class GetUserInfo(GetProfile): 67 | """Alias for GetProfile for backwards compatibility""" 68 | pass 69 | 70 | 71 | class GetUsersBlogs(AuthenticatedMethod): 72 | """ 73 | Retrieve list of blogs that this user belongs to. 74 | 75 | Parameters: 76 | None 77 | 78 | Returns: `list` of :class:`WordPressBlog` instances. 79 | """ 80 | method_name = 'wp.getUsersBlogs' 81 | results_class = WordPressBlog 82 | 83 | def get_args(self, client): 84 | # strip off first (blog_id) parameter 85 | return super(GetUsersBlogs, self).get_args(client)[1:] 86 | 87 | 88 | class GetAuthors(AuthenticatedMethod): 89 | """ 90 | Retrieve list of authors in the blog. 91 | 92 | Parameters: 93 | None 94 | 95 | Returns: `list` of :class:`WordPressAuthor` instances. 96 | """ 97 | method_name = 'wp.getAuthors' 98 | results_class = WordPressAuthor 99 | -------------------------------------------------------------------------------- /wordpress_xmlrpc/wordpress.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .compat import * 3 | from .fieldmaps import FieldMap, IntegerFieldMap, DateTimeFieldMap, TermsListFieldMap 4 | from wordpress_xmlrpc.exceptions import FieldConversionError 5 | 6 | 7 | class WordPressBase(object): 8 | """ 9 | Base class for representing a WordPress object. Handles conversion 10 | of an XML-RPC response to an object, and construction of a `struct` 11 | to use in XML-RPC requests. 12 | 13 | Child classes should define a `definition` property that contains 14 | the list of fields and a `FieldMap` instance to handle conversion 15 | for XML-RPC calls. 16 | """ 17 | definition = {} 18 | 19 | def __init__(self, xmlrpc=None): 20 | # create private variable containing all FieldMaps for the `definition` 21 | self._def = {} 22 | 23 | for key, value in self.definition.items(): 24 | # if the definition was not a FieldMap, create a simple FieldMap 25 | if isinstance(value, FieldMap): 26 | self._def[key] = value 27 | else: 28 | self._def[key] = FieldMap(value) 29 | 30 | # convert and store the value on this instance if non-empty 31 | try: 32 | converted_value = self._def[key].convert_to_python(xmlrpc) 33 | except Exception: 34 | e = sys.exc_info()[1] 35 | raise FieldConversionError(key, e) 36 | if converted_value is not None: 37 | setattr(self, key, converted_value) 38 | 39 | @property 40 | def struct(self): 41 | """ 42 | XML-RPC-friendly representation of the current object state 43 | """ 44 | data = {} 45 | for var, fmap in self._def.items(): 46 | if hasattr(self, var): 47 | data.update(fmap.get_outputs(getattr(self, var))) 48 | return data 49 | 50 | def __repr__(self): 51 | return '<%s: %s>' % (self.__class__.__name__, str(self).encode('utf-8')) 52 | 53 | 54 | class WordPressTaxonomy(WordPressBase): 55 | definition = { 56 | 'name': FieldMap('name', default=''), 57 | 'label': 'label', 58 | 'labels': 'labels', 59 | 'hierarchical': 'hierarchical', 60 | 'public': 'public', 61 | 'show_ui': 'show_ui', 62 | 'cap': 'cap', 63 | 'is_builtin': '_builtin', 64 | 'object_type': 'object_type' 65 | } 66 | 67 | def __str__(self): 68 | if hasattr(self, 'name'): 69 | return self.name 70 | return unicode('') 71 | 72 | 73 | class WordPressTerm(WordPressBase): 74 | definition = { 75 | 'id': 'term_id', 76 | 'group': 'term_group', 77 | 'taxonomy': 'taxonomy', 78 | 'taxonomy_id': 'term_taxonomy_id', 79 | 'name': FieldMap('name', default=''), 80 | 'slug': 'slug', 81 | 'description': 'description', 82 | 'parent': 'parent', 83 | 'count': IntegerFieldMap('count') 84 | } 85 | 86 | def __str__(self): 87 | if hasattr(self, 'name'): 88 | return self.name 89 | return unicode('') 90 | 91 | 92 | class WordPressPost(WordPressBase): 93 | definition = { 94 | 'id': 'post_id', 95 | 'user': 'post_author', 96 | 'date': DateTimeFieldMap('post_date_gmt'), 97 | 'date_modified': DateTimeFieldMap('post_modified_gmt'), 98 | 'slug': 'post_name', 99 | 'post_status': 'post_status', 100 | 'title': FieldMap('post_title', default='Untitled'), 101 | 'content': 'post_content', 102 | 'excerpt': 'post_excerpt', 103 | 'link': 'link', 104 | 'comment_status': 'comment_status', 105 | 'ping_status': 'ping_status', 106 | 'terms': TermsListFieldMap(WordPressTerm, 'terms'), 107 | 'terms_names': 'terms_names', 108 | 'custom_fields': 'custom_fields', 109 | 'enclosure': 'enclosure', 110 | 'password': 'post_password', 111 | 'post_format': 'post_format', 112 | 'thumbnail': 'post_thumbnail', 113 | 'sticky': 'sticky', 114 | 'post_type': FieldMap('post_type', default='post'), 115 | 'parent_id': 'post_parent', 116 | 'menu_order': IntegerFieldMap('menu_order'), 117 | 'guid': 'guid', 118 | 'mime_type': 'post_mime_type', 119 | } 120 | 121 | def __str__(self): 122 | if hasattr(self, 'title'): 123 | return self.title 124 | return unicode('') 125 | 126 | 127 | class WordPressPage(WordPressPost): 128 | definition = dict(WordPressPost.definition, **{ 129 | 'template': 'wp_page_template', 130 | 'post_type': FieldMap('post_type', default='page'), 131 | }) 132 | 133 | 134 | class WordPressComment(WordPressBase): 135 | definition = { 136 | 'id': 'comment_id', 137 | 'user': 'user_id', 138 | 'post': 'post_id', 139 | 'post_title': 'post_title', 140 | 'parent': 'comment_parent', 141 | 'date_created': DateTimeFieldMap('date_created_gmt'), 142 | 'status': 'status', 143 | 'content': FieldMap('content', default=''), 144 | 'link': 'link', 145 | 'author': 'author', 146 | 'author_url': 'author_url', 147 | 'author_email': 'author_email', 148 | 'author_ip': 'author_ip', 149 | } 150 | 151 | def __str__(self): 152 | if hasattr(self, 'content'): 153 | return self.content 154 | return unicode('') 155 | 156 | 157 | class WordPressBlog(WordPressBase): 158 | definition = { 159 | 'id': 'blogid', 160 | 'name': FieldMap('blogName', default=''), 161 | 'url': 'url', 162 | 'xmlrpc': 'xmlrpc', 163 | 'is_admin': FieldMap('isAdmin', default=False), 164 | } 165 | 166 | def __str__(self): 167 | if hasattr(self, 'name'): 168 | return self.name 169 | return unicode('') 170 | 171 | 172 | class WordPressAuthor(WordPressBase): 173 | definition = { 174 | 'id': 'user_id', 175 | 'user_login': 'user_login', 176 | 'display_name': FieldMap('display_name', default=''), 177 | } 178 | 179 | def __str__(self): 180 | if hasattr(self, 'display_name'): 181 | return self.display_name 182 | return unicode('') 183 | 184 | 185 | class WordPressUser(WordPressBase): 186 | definition = { 187 | 'id': 'user_id', 188 | 'username': 'username', 189 | 'roles': 'roles', 190 | 'nickname': 'nickname', 191 | 'url': 'url', 192 | 'first_name': 'first_name', 193 | 'last_name': 'last_name', 194 | 'registered': DateTimeFieldMap('registered'), 195 | 'bio': 'bio', 196 | 'email': 'email', 197 | 'nicename': 'nicename', 198 | 'display_name': 'display_name', 199 | } 200 | 201 | def __str__(self): 202 | if hasattr(self, 'nickname'): 203 | return self.nickname 204 | return unicode('') 205 | 206 | 207 | class WordPressMedia(WordPressBase): 208 | definition = { 209 | 'id': 'attachment_id', 210 | 'parent': 'parent', 211 | 'title': FieldMap('title', default=''), 212 | 'description': 'description', 213 | 'caption': 'caption', 214 | 'date_created': DateTimeFieldMap('date_created_gmt'), 215 | 'link': 'link', 216 | 'thumbnail': 'thumbnail', 217 | 'metadata': 'metadata', 218 | } 219 | 220 | def __str__(self): 221 | if hasattr(self, 'title'): 222 | return self.title 223 | return unicode('') 224 | 225 | 226 | class WordPressOption(WordPressBase): 227 | definition = { 228 | 'name': FieldMap('name', default=''), 229 | 'description': 'desc', 230 | 'value': FieldMap('value', default=''), 231 | 'read_only': FieldMap('readonly', default=False), 232 | } 233 | 234 | def __str__(self): 235 | if hasattr(self, 'name') and hasattr(self, 'value'): 236 | return '%s="%s"' % (self.name, self.value) 237 | return unicode('') 238 | 239 | 240 | class WordPressPostType(WordPressBase): 241 | definition = { 242 | 'name': 'name', 243 | 'label': FieldMap('label', default=''), 244 | 'labels': 'labels', 245 | 'cap': 'cap', 246 | 'map_meta_cap': 'map_meta_cap', 247 | 'hierarchical': 'hierarchical', 248 | 'menu_icon': 'menu_icon', 249 | 'menu_position': 'menu_position', 250 | 'public': 'public', 251 | 'show_in_menu': 'show_in_menu', 252 | 'taxonomies': 'taxonomies', 253 | 'is_builtin': '_builtin', 254 | 'supports': 'supports', 255 | } 256 | 257 | def __str__(self): 258 | if hasattr(self, 'name'): 259 | return self.name 260 | return unicode('') 261 | -------------------------------------------------------------------------------- /wp-config-sample.cfg: -------------------------------------------------------------------------------- 1 | [wordpress] 2 | url = http://www.mysite.com/xmlrpc.php 3 | username = admin 4 | password = mypassword 5 | userid = 1 6 | --------------------------------------------------------------------------------