├── .gitignore
├── .travis.yml
├── LICENSE
├── MANIFEST.in
├── README.rst
├── requirements.txt
├── seria
├── __init__.py
├── cli.py
├── compat.py
├── providers.py
├── serializers.py
└── utils.py
├── setup.py
└── tests
├── resources
├── bad.json
├── bad.xml
├── bad.yaml
├── good.json
├── good.xml
└── good.yaml
├── test_cli.py
├── test_seria.py
└── test_utils.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/linux,osx,windows,emacs,vim,xcode,eclipse,intellij,textmate,sublimetext,grunt,yeoman,latex,maven,python,virtualenv,ipythonnotebook,django,ruby,go,java,c,c++,vagrant,mercurial,svn,git
2 |
3 | ### Linux ###
4 | *~
5 |
6 | # KDE directory preferences
7 | .directory
8 |
9 | # Linux trash folder which might appear on any partition or disk
10 | .Trash-*
11 |
12 |
13 | ### OSX ###
14 | .DS_Store
15 | .AppleDouble
16 | .LSOverride
17 |
18 | # Icon must end with two \r
19 | Icon
20 |
21 | # Thumbnails
22 | ._*
23 |
24 | # Files that might appear in the root of a volume
25 | .DocumentRevisions-V100
26 | .fseventsd
27 | .Spotlight-V100
28 | .TemporaryItems
29 | .Trashes
30 | .VolumeIcon.icns
31 |
32 | # Directories potentially created on remote AFP share
33 | .AppleDB
34 | .AppleDesktop
35 | Network Trash Folder
36 | Temporary Items
37 | .apdisk
38 |
39 |
40 | ### Windows ###
41 | # Windows image file caches
42 | Thumbs.db
43 | ehthumbs.db
44 |
45 | # Folder config file
46 | Desktop.ini
47 |
48 | # Recycle Bin used on file shares
49 | $RECYCLE.BIN/
50 |
51 | # Windows Installer files
52 | *.cab
53 | *.msi
54 | *.msm
55 | *.msp
56 |
57 | # Windows shortcuts
58 | *.lnk
59 |
60 |
61 | ### Emacs ###
62 | # -*- mode: gitignore; -*-
63 | *~
64 | \#*\#
65 | /.emacs.desktop
66 | /.emacs.desktop.lock
67 | *.elc
68 | auto-save-list
69 | tramp
70 | .\#*
71 |
72 | # Org-mode
73 | .org-id-locations
74 | *_archive
75 |
76 | # flymake-mode
77 | *_flymake.*
78 |
79 | # eshell files
80 | /eshell/history
81 | /eshell/lastdir
82 |
83 | # elpa packages
84 | /elpa/
85 |
86 | # reftex files
87 | *.rel
88 |
89 | # AUCTeX auto folder
90 | /auto/
91 |
92 | # cask packages
93 | .cask/
94 |
95 |
96 | ### Vim ###
97 | [._]*.s[a-w][a-z]
98 | [._]s[a-w][a-z]
99 | *.un~
100 | Session.vim
101 | .netrwhist
102 | *~
103 |
104 |
105 | ### Xcode ###
106 | # Xcode
107 | #
108 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
109 |
110 | ## Build generated
111 | build/
112 | DerivedData
113 |
114 | ## Various settings
115 | *.pbxuser
116 | !default.pbxuser
117 | *.mode1v3
118 | !default.mode1v3
119 | *.mode2v3
120 | !default.mode2v3
121 | *.perspectivev3
122 | !default.perspectivev3
123 | xcuserdata
124 |
125 | ## Other
126 | *.xccheckout
127 | *.moved-aside
128 | *.xcuserstate
129 |
130 |
131 | ### Eclipse ###
132 | *.pydevproject
133 | .metadata
134 | .gradle
135 | bin/
136 | tmp/
137 | *.tmp
138 | *.bak
139 | *.swp
140 | *~.nib
141 | local.properties
142 | .settings/
143 | .loadpath
144 |
145 | # Eclipse Core
146 | .project
147 |
148 | # External tool builders
149 | .externalToolBuilders/
150 |
151 | # Locally stored "Eclipse launch configurations"
152 | *.launch
153 |
154 | # CDT-specific
155 | .cproject
156 |
157 | # JDT-specific (Eclipse Java Development Tools)
158 | .classpath
159 |
160 | # Java annotation processor (APT)
161 | .factorypath
162 |
163 | # PDT-specific
164 | .buildpath
165 |
166 | # sbteclipse plugin
167 | .target
168 |
169 | # TeXlipse plugin
170 | .texlipse
171 |
172 |
173 | ### Intellij ###
174 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
175 |
176 | *.iml
177 |
178 | ## Directory-based project format:
179 | .idea/
180 | # if you remove the above rule, at least ignore the following:
181 |
182 | # User-specific stuff:
183 | # .idea/workspace.xml
184 | # .idea/tasks.xml
185 | # .idea/dictionaries
186 |
187 | # Sensitive or high-churn files:
188 | # .idea/dataSources.ids
189 | # .idea/dataSources.xml
190 | # .idea/sqlDataSources.xml
191 | # .idea/dynamic.xml
192 | # .idea/uiDesigner.xml
193 |
194 | # Gradle:
195 | # .idea/gradle.xml
196 | # .idea/libraries
197 |
198 | # Mongo Explorer plugin:
199 | # .idea/mongoSettings.xml
200 |
201 | ## File-based project format:
202 | *.ipr
203 | *.iws
204 |
205 | ## Plugin-specific files:
206 |
207 | # IntelliJ
208 | /out/
209 |
210 | # mpeltonen/sbt-idea plugin
211 | .idea_modules/
212 |
213 | # JIRA plugin
214 | atlassian-ide-plugin.xml
215 |
216 | # Crashlytics plugin (for Android Studio and IntelliJ)
217 | com_crashlytics_export_strings.xml
218 | crashlytics.properties
219 | crashlytics-build.properties
220 |
221 |
222 | ### TextMate ###
223 | *.tmproj
224 | *.tmproject
225 | tmtags
226 |
227 |
228 | ### SublimeText ###
229 | # cache files for sublime text
230 | *.tmlanguage.cache
231 | *.tmPreferences.cache
232 | *.stTheme.cache
233 |
234 | # workspace files are user-specific
235 | *.sublime-workspace
236 |
237 | # project files should be checked into the repository, unless a significant
238 | # proportion of contributors will probably not be using SublimeText
239 | # *.sublime-project
240 |
241 | # sftp configuration file
242 | sftp-config.json
243 |
244 |
245 | ### grunt ###
246 | # Grunt usually compiles files inside this directory
247 | dist/
248 |
249 | # Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory
250 | .tmp/
251 |
252 |
253 | ### Yeoman ###
254 | node_modules/
255 | bower_components/
256 | *.log
257 |
258 | build/
259 | dist/
260 |
261 |
262 | ### LaTeX ###
263 | *.acn
264 | *.acr
265 | *.alg
266 | *.aux
267 | *.bbl
268 | *.bcf
269 | *.blg
270 | *.dvi
271 | *.fdb_latexmk
272 | *.fls
273 | *.glg
274 | *.glo
275 | *.gls
276 | *.idx
277 | *.ilg
278 | *.ind
279 | *.ist
280 | *.lof
281 | *.log
282 | *.lot
283 | *.maf
284 | *.mtc
285 | *.mtc0
286 | *.nav
287 | *.nlo
288 | *.out
289 | *.pdfsync
290 | *.ps
291 | *.run.xml
292 | *.snm
293 | *.synctex.gz
294 | *.toc
295 | *.vrb
296 | *.xdy
297 | *.tdo
298 |
299 |
300 | ### Maven ###
301 | target/
302 | pom.xml.tag
303 | pom.xml.releaseBackup
304 | pom.xml.versionsBackup
305 | pom.xml.next
306 | release.properties
307 | dependency-reduced-pom.xml
308 | buildNumber.properties
309 | .mvn/timing.properties
310 |
311 |
312 | ### Python ###
313 | # Byte-compiled / optimized / DLL files
314 | __pycache__/
315 | *.py[cod]
316 | *$py.class
317 |
318 | # C extensions
319 | *.so
320 |
321 | # Distribution / packaging
322 | .Python
323 | env/
324 | build/
325 | develop-eggs/
326 | dist/
327 | downloads/
328 | eggs/
329 | .eggs/
330 | lib/
331 | lib64/
332 | parts/
333 | sdist/
334 | var/
335 | *.egg-info/
336 | .installed.cfg
337 | *.egg
338 |
339 | # PyInstaller
340 | # Usually these files are written by a python script from a template
341 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
342 | *.manifest
343 | *.spec
344 |
345 | # Installer logs
346 | pip-log.txt
347 | pip-delete-this-directory.txt
348 |
349 | # Unit test / coverage reports
350 | htmlcov/
351 | .tox/
352 | .coverage
353 | .coverage.*
354 | .cache
355 | nosetests.xml
356 | coverage.xml
357 | *,cover
358 |
359 | # Translations
360 | *.mo
361 | *.pot
362 |
363 | # Django stuff:
364 | *.log
365 |
366 | # Sphinx documentation
367 | docs/_build/
368 |
369 | # PyBuilder
370 | target/
371 |
372 |
373 | ### VirtualEnv ###
374 | # Virtualenv
375 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
376 | .Python
377 | [Bb]in
378 | [Ii]nclude
379 | [Ll]ib
380 | [Ss]cripts
381 | pyvenv.cfg
382 | pip-selfcheck.json
383 |
384 |
385 | ### IPythonNotebook ###
386 | # Temporary data
387 | .ipynb_checkpoints/
388 |
389 |
390 | ### Django ###
391 | *.log
392 | *.pot
393 | *.pyc
394 | __pycache__/
395 | local_settings.py
396 |
397 |
398 | ### Ruby ###
399 | *.gem
400 | *.rbc
401 | /.config
402 | /coverage/
403 | /InstalledFiles
404 | /pkg/
405 | /spec/reports/
406 | /spec/examples.txt
407 | /test/tmp/
408 | /test/version_tmp/
409 | /tmp/
410 |
411 | ## Specific to RubyMotion:
412 | .dat*
413 | .repl_history
414 | build/
415 |
416 | ## Documentation cache and generated files:
417 | /.yardoc/
418 | /_yardoc/
419 | /doc/
420 | /rdoc/
421 |
422 | ## Environment normalisation:
423 | /.bundle/
424 | /vendor/bundle
425 | /lib/bundler/man/
426 |
427 | # for a library or gem, you might want to ignore these files since the code is
428 | # intended to run in multiple environments; otherwise, check them in:
429 | # Gemfile.lock
430 | # .ruby-version
431 | # .ruby-gemset
432 |
433 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
434 | .rvmrc
435 |
436 |
437 | ### Go ###
438 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
439 | *.o
440 | *.a
441 | *.so
442 |
443 | # Folders
444 | _obj
445 | _test
446 |
447 | # Architecture specific extensions/prefixes
448 | *.[568vq]
449 | [568vq].out
450 |
451 | *.cgo1.go
452 | *.cgo2.c
453 | _cgo_defun.c
454 | _cgo_gotypes.go
455 | _cgo_export.*
456 |
457 | _testmain.go
458 |
459 | *.exe
460 | *.test
461 | *.prof
462 |
463 |
464 | ### Java ###
465 | *.class
466 |
467 | # Mobile Tools for Java (J2ME)
468 | .mtj.tmp/
469 |
470 | # Package Files #
471 | *.jar
472 | *.war
473 | *.ear
474 |
475 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
476 | hs_err_pid*
477 |
478 |
479 | ### C ###
480 | # Object files
481 | *.o
482 | *.ko
483 | *.obj
484 | *.elf
485 |
486 | # Precompiled Headers
487 | *.gch
488 | *.pch
489 |
490 | # Libraries
491 | *.lib
492 | *.a
493 | *.la
494 | *.lo
495 |
496 | # Shared objects (inc. Windows DLLs)
497 | *.dll
498 | *.so
499 | *.so.*
500 | *.dylib
501 |
502 | # Executables
503 | *.exe
504 | *.out
505 | *.app
506 | *.i*86
507 | *.x86_64
508 | *.hex
509 |
510 | # Debug files
511 | *.dSYM/
512 |
513 |
514 | ### C++ ###
515 | # Compiled Object files
516 | *.slo
517 | *.lo
518 | *.o
519 | *.obj
520 |
521 | # Precompiled Headers
522 | *.gch
523 | *.pch
524 |
525 | # Compiled Dynamic libraries
526 | *.so
527 | *.dylib
528 | *.dll
529 |
530 | # Fortran module files
531 | *.mod
532 |
533 | # Compiled Static libraries
534 | *.lai
535 | *.la
536 | *.a
537 | *.lib
538 |
539 | # Executables
540 | *.exe
541 | *.out
542 | *.app
543 |
544 |
545 | ### Vagrant ###
546 | .vagrant/
547 |
548 |
549 | ### Mercurial ###
550 | /.hg/*
551 | */.hg/*
552 | .hgignore
553 |
554 |
555 | ### SVN ###
556 | .svn/
557 |
558 |
559 | #!! ERROR: git is undefined. Use list command to see defined gitignore types !!#
560 | .python-version
561 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | except:
3 | - gh-pages
4 |
5 | language: python
6 |
7 | python:
8 | - "2.7"
9 | - "3.3"
10 | - "3.4"
11 | - "3.5"
12 |
13 | script: "python setup.py test"
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Ryan Luckie
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | include LICENSE
3 | recursive-include tests *
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Seria: Serialization for Humans
2 | ===============================
3 | .. image:: https://img.shields.io/pypi/v/seria.svg
4 | :target: https://pypi.python.org/pypi/seria
5 |
6 | .. image:: https://img.shields.io/pypi/dm/seria.svg
7 | :target: https://pypi.python.org/pypi/seria
8 |
9 | .. image:: https://travis-ci.org/rtluckie/seria.svg?branch=master
10 | :target: https://travis-ci.org/rtluckie/seria
11 |
12 | Basic Usage
13 | -----------
14 |
15 | .. code-block:: python
16 |
17 | import seria
18 | with open("tests/resources/good.xml", "rb") as f:
19 | s = seria.load(f)
20 | print s.dump('xml')
21 | print s.dump('json')
22 | print s.dump('yaml')
23 |
24 | CLI Tool
25 | -----------
26 | Seria includes a useful command line tool.
27 |
28 | .. code-block:: bash
29 |
30 | cat tests/resources/good.xml | seria -y -
31 | cat tests/resources/good.json | seria -j -
32 | cat tests/resources/good.yaml | seria -x -
33 | cat tests/resources/good.xml | seria -x - | seria -j - | seria -y -
34 |
35 |
36 | Features
37 | --------
38 |
39 | - Support for (with a few limitations) json, yaml, xml
40 |
41 | Installation
42 | ------------
43 |
44 | .. code-block:: bash
45 |
46 | pip install seria
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | mock>=1.0.1
2 | pytest>=2.5.2
3 | PyYAML>=3.11
4 | xmltodict>=0.9.0
5 | click>=3.3
6 |
--------------------------------------------------------------------------------
/seria/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .serializers import Serializer
4 |
5 |
6 | def load(stream, *args, **kwargs):
7 | return Serializer(stream, *args, **kwargs)
--------------------------------------------------------------------------------
/seria/cli.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .compat import StringIO, str
4 | import seria
5 |
6 | import click
7 |
8 |
9 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
10 |
11 | input = None
12 | output = None
13 | out_fmt = None
14 |
15 |
16 | @click.command(context_settings=CONTEXT_SETTINGS)
17 | @click.option('--xml', '-x', 'out_fmt', flag_value='xml')
18 | @click.option('--yaml', '--yml', '-y', 'out_fmt', flag_value='yaml')
19 | @click.option('--yml', 'out_fmt', flag_value='yaml')
20 | @click.option('--json', '-j', 'out_fmt', flag_value='json')
21 | @click.argument('input', type=click.File('r'), default='-')
22 | @click.argument('output', type=click.File('w'), default='-')
23 | def cli(out_fmt, input, output):
24 | """Converts text."""
25 | _input = StringIO()
26 | for l in input:
27 | try:
28 | _input.write(str(l))
29 | except TypeError:
30 | _input.write(bytes(l, 'utf-8'))
31 | _input = seria.load(_input)
32 | _out = (_input.dump(out_fmt))
33 | output.write(_out)
34 |
35 |
36 | if __name__ == '__main__':
37 | cli(out_fmt, input, output)
--------------------------------------------------------------------------------
/seria/compat.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import sys
4 |
5 | _ver = sys.version_info
6 | is_py2 = (_ver[0] == 2)
7 | is_py3 = (_ver[0] == 3)
8 |
9 | try:
10 | import simplejson as json
11 | except (ImportError, SyntaxError):
12 | import json
13 |
14 | if is_py2:
15 | from StringIO import StringIO
16 |
17 | try:
18 | from collections import OrderedDict
19 | except ImportError:
20 | try:
21 | from ordereddict import OrderedDict
22 | except ImportError:
23 | OrderedDict = dict
24 |
25 | basestring = basestring
26 | builtin_str = str
27 | bytes = str
28 | str = unicode
29 |
30 |
31 | elif is_py3:
32 | from io import StringIO
33 | from collections import OrderedDict
34 |
35 | basestring = (str, bytes)
36 | builtin_str = str
37 | bytes = bytes
38 | str = str
--------------------------------------------------------------------------------
/seria/providers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from xml.etree import ElementTree as ET
4 |
5 | import xmltodict
6 | import yaml
7 |
8 | from .compat import str, basestring, OrderedDict, json, builtin_str
9 | from .utils import str_to_num, set_defaults
10 |
11 |
12 | class JSON(object):
13 | @staticmethod
14 | def validate_json(stream):
15 | stream.seek(0)
16 | try:
17 | json.loads(str(stream.read()))
18 | return True
19 | except ValueError:
20 | return False
21 |
22 | @staticmethod
23 | def is_it(stream):
24 | stream.seek(0)
25 | return JSON.validate_json(stream)
26 |
27 | @staticmethod
28 | def load(stream):
29 | stream.seek(0)
30 | return JSON.ordered_load(stream)
31 |
32 | @staticmethod
33 | def dump(obj, *args, **kwargs):
34 | defaults = {'indent': 4}
35 | set_defaults(kwargs, defaults)
36 | return json.dumps(obj, *args, **kwargs)
37 |
38 | @staticmethod
39 | def ordered_load(stream):
40 | return json.load(stream, object_pairs_hook=OrderedDict)
41 |
42 |
43 | class XML(object):
44 | @staticmethod
45 | def validate_xml(stream):
46 | stream.seek(0)
47 | # TODO: Support for standard xml validation mechanisms
48 | _ = ET.parse(stream)
49 |
50 | @staticmethod
51 | def is_it(stream):
52 | stream.seek(0)
53 | try:
54 | XML.validate_xml(stream)
55 | return True
56 | except ET.ParseError:
57 | return False
58 |
59 |
60 | @staticmethod
61 | def load(stream):
62 | stream.seek(0)
63 | serialized = XML.ordered_load(stream)
64 | if 'SERIAROOT' in serialized:
65 | serialized = serialized['SERIAROOT']
66 | return serialized
67 |
68 | @staticmethod
69 | def dump(obj, *args, **kwargs):
70 | defaults = {"pretty": True, "newl": '\n', "indent": ' '}
71 | set_defaults(kwargs, defaults)
72 | if len(obj.keys()) > 1:
73 | obj = OrderedDict({"SERIAROOT": obj})
74 | return XML.ordered_dump(obj, *args, **kwargs)
75 |
76 |
77 | @staticmethod
78 | def ordered_load(stream, *args, **kwargs):
79 | defaults = {'postprocessor': XML.postprocessor}
80 | set_defaults(kwargs, defaults)
81 | return xmltodict.parse(stream.read(), *args, **kwargs)
82 |
83 | @staticmethod
84 | def ordered_dump(data, *args, **kwargs):
85 | return xmltodict.unparse(data, *args, **kwargs)
86 |
87 | @staticmethod
88 | def postprocessor(path, key, value):
89 | try:
90 | if isinstance(key, basestring):
91 | key = str(key)
92 | if isinstance(value, basestring):
93 | value = str(value)
94 | value = str_to_num(value)
95 | return key, value
96 | except (ValueError, TypeError):
97 | return key, value
98 |
99 |
100 | class YAML(object):
101 | @staticmethod
102 | def validate_yaml(stream):
103 | stream.seek(0)
104 | try:
105 | _ = yaml.load(stream)
106 | # All strings, as long as they don't violate basic yaml guidelines, are valid yaml.
107 | # This is problematic because valid xml (and in some cases invalid xml) are considered valid yaml.
108 | # ie a yaml document that contains a string
109 | # For now, we are uninterested in yaml docs that contain merely a string.
110 | # TODO: Better detection of valid yaml
111 | if isinstance(_, (str, builtin_str)):
112 | return False
113 | return True
114 | except (yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
115 | return False
116 |
117 | @staticmethod
118 | def is_it(stream):
119 | stream.seek(0)
120 | return YAML.validate_yaml(stream)
121 |
122 | @staticmethod
123 | def load(stream):
124 | stream.seek(0)
125 | return YAML.ordered_load(stream)
126 |
127 | @staticmethod
128 | def dump(serialized, *args, **kwargs):
129 | defaults = {"default_flow_style": False}
130 | set_defaults(kwargs, defaults)
131 | return YAML.ordered_dump(serialized, Dumper=yaml.SafeDumper, *args, **kwargs)
132 |
133 |
134 | @staticmethod
135 | def ordered_load(stream, Loader=yaml.SafeLoader, object_pairs_hook=OrderedDict):
136 | class OrderedLoader(Loader):
137 | pass
138 |
139 | def construct_mapping(loader, node):
140 | loader.flatten_mapping(node)
141 | return object_pairs_hook(loader.construct_pairs(node))
142 |
143 | OrderedLoader.add_constructor(
144 | yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
145 | construct_mapping)
146 | return yaml.load(stream, OrderedLoader)
147 |
148 | @staticmethod
149 | def ordered_dump(obj, stream=None, Dumper=yaml.Dumper, **kwargs):
150 | class OrderedDumper(Dumper):
151 | pass
152 |
153 | def _dict_representer(dumper, data):
154 | return dumper.represent_mapping(
155 | yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
156 | data.items())
157 |
158 | OrderedDumper.add_representer(OrderedDict, _dict_representer)
159 | return yaml.dump(obj, stream, OrderedDumper, **kwargs)
--------------------------------------------------------------------------------
/seria/serializers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from .providers import (
4 | XML,
5 | JSON,
6 | YAML,
7 | )
8 |
9 |
10 | def get_formats(stream):
11 | fmts = []
12 | for _fmt in [JSON, XML, YAML]:
13 | stream.seek(0)
14 | fmts.append(_fmt.is_it(stream))
15 | return tuple(fmts)
16 |
17 |
18 | class Serializer(object):
19 | class Error(Exception):
20 | pass
21 |
22 | def __init__(self, stream):
23 | self.stream = stream
24 | self.formats = get_formats(self.stream)
25 |
26 | @property
27 | def stream(self):
28 | return self._stream
29 |
30 | @stream.setter
31 | def stream(self, stream):
32 | if not (hasattr(stream, 'read') or hasattr(stream, 'getvalue')):
33 | raise Serializer.Error("Stream must be file-like object")
34 | self._stream = stream
35 | self.formats = get_formats(self.stream)
36 | self.serialized = self.load()
37 |
38 | @property
39 | def is_json(self):
40 | return self.formats[0]
41 |
42 | @property
43 | def is_xml(self):
44 | return self.formats[1]
45 |
46 | @property
47 | def is_yaml(self):
48 | return self.formats[2]
49 |
50 | def dump(self, fmt, *args, **kwargs):
51 | if fmt == 'yaml':
52 | return YAML.dump(self.serialized, *args, **kwargs)
53 | elif fmt == 'json':
54 | return JSON.dump(self.serialized, *args, **kwargs)
55 | elif fmt == 'xml':
56 | return XML.dump(self.serialized, *args, **kwargs)
57 | else:
58 | raise Serializer.Error("Requested format '%s' is not a supported format" % fmt)
59 |
60 | def load(self):
61 | if self.is_xml:
62 | return XML.load(self._stream)
63 | elif self.is_yaml:
64 | return YAML.load(self._stream)
65 | # No need for JSON.load since yaml is a superset of json
66 | else:
67 | raise Serializer.Error("Input does not appear to be a supported format")
--------------------------------------------------------------------------------
/seria/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | def str_to_num(i, exact_match=True):
4 | """
5 | Attempts to convert a str to either an int or float
6 | """
7 | # TODO: Cleanup -- this is really ugly
8 | if not isinstance(i, str):
9 | return i
10 | try:
11 | if not exact_match:
12 | return int(i)
13 | elif str(int(i)) == i:
14 | return int(i)
15 | elif str(float(i)) == i:
16 | return float(i)
17 | else:
18 | pass
19 | except ValueError:
20 | pass
21 | return i
22 |
23 |
24 | def set_defaults(kwargs, defaults):
25 | for k, v in defaults.items():
26 | if k not in kwargs:
27 | kwargs[k] = v
28 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | from __future__ import with_statement
4 |
5 | from setuptools import setup
6 | import sys
7 |
8 | from setuptools.command.test import test as TestCommand
9 |
10 |
11 | class PyTest(TestCommand):
12 | user_options = [('pytest-args=', 'a', "Arguments to pass to py.test")]
13 |
14 | def initialize_options(self):
15 | TestCommand.initialize_options(self)
16 | self.pytest_args = []
17 |
18 | def finalize_options(self):
19 | TestCommand.finalize_options(self)
20 | self.test_args = []
21 | self.test_suite = True
22 |
23 | def run_tests(self):
24 | #import here, cause outside the eggs aren't loaded
25 | import pytest
26 | errno = pytest.main(self.pytest_args)
27 | sys.exit(errno)
28 |
29 | # TODO: Add readme
30 | with open('README.rst') as f:
31 | readme = f.read()
32 | # TODO: add long description
33 | long_description = "%s" % (readme)
34 |
35 | packages = [
36 | 'seria',
37 | ]
38 | install_requires = [
39 | 'PyYAML>=3.11',
40 | 'xmltodict>=0.9.2',
41 | 'click>=3.3',
42 | ]
43 |
44 | setup(
45 | name='seria',
46 | version='0.1.5',
47 | description='Serialization for Humans',
48 | long_description=long_description,
49 | author='Ryan Luckie',
50 | author_email='rtluckie@gmail.com',
51 | url='https://github.com/rtluckie/seria',
52 | packages=packages,
53 | platforms=['all'],
54 | tests_require=['pytest', 'mock'],
55 | cmdclass = {'test': PyTest},
56 | install_requires=install_requires,
57 | package_dir={'seria': 'seria'},
58 | license='MIT',
59 | keywords='yaml json yaml yml serialize serialization deserialize deserialization cli',
60 | entry_points={
61 | 'console_scripts': [
62 | 'seria = seria.cli:cli',
63 | ]
64 | },
65 | classifiers=[
66 | 'Development Status :: 3 - Alpha',
67 | 'License :: OSI Approved :: MIT License',
68 | 'Environment :: Console',
69 | 'Intended Audience :: Developers',
70 | 'Intended Audience :: System Administrators',
71 | 'Operating System :: MacOS :: MacOS X',
72 | 'Operating System :: Unix',
73 | 'Operating System :: POSIX',
74 | 'Programming Language :: Python :: 2.5',
75 | 'Programming Language :: Python :: 2.6',
76 | 'Programming Language :: Python :: 2.7',
77 | 'Programming Language :: Python :: 3.2',
78 | 'Programming Language :: Python :: 3.3',
79 | 'Topic :: Utilities',
80 | 'Topic :: Software Development',
81 | 'Topic :: Software Development :: Libraries',
82 | 'Topic :: Software Development :: Libraries :: Python Modules',
83 | 'Topic :: System :: Systems Administration',
84 | ],
85 | )
86 |
--------------------------------------------------------------------------------
/tests/resources/bad.json:
--------------------------------------------------------------------------------
1 | {
2 | "foo": "bar"
3 |
--------------------------------------------------------------------------------
/tests/resources/bad.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | elements
5 | more elements
6 |
7 | element as well
8 |
--------------------------------------------------------------------------------
/tests/resources/bad.yaml:
--------------------------------------------------------------------------------
1 | {sddfsd
2 |
--------------------------------------------------------------------------------
/tests/resources/good.json:
--------------------------------------------------------------------------------
1 | {
2 | "mydocument": {
3 | "@has": "an attribute",
4 | "and": {
5 | "many": [
6 | "elements",
7 | "more elements"
8 | ]
9 | },
10 | "plus": {
11 | "@a": "complex",
12 | "#text": "element as well"
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/tests/resources/good.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | elements
5 | more elements
6 |
7 | element as well
8 |
--------------------------------------------------------------------------------
/tests/resources/good.yaml:
--------------------------------------------------------------------------------
1 | mydocument:
2 | '@has': an attribute
3 | and:
4 | many:
5 | - elements
6 | - more elements
7 | plus:
8 | '@a': complex
9 | '#text': element as well
10 |
--------------------------------------------------------------------------------
/tests/test_cli.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import click
4 | from click.testing import CliRunner
5 |
6 | import seria
7 | from seria import cli
8 |
9 | import click
10 | from click.testing import CliRunner
11 |
12 | test_resources = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'resources')
13 |
14 | runner = CliRunner()
15 | class TestSeriaCLI(object):
16 | def test_cli_xml_to_xml_to_stdout(self):
17 | _input_fmt = 'xml'
18 | _output_fmt = 'xml'
19 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
20 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
21 | _s = seria.load(f)
22 | _comp_val = _s.dump(_output_fmt)
23 | assert result.exit_code == 0
24 | assert result.output == _comp_val
25 |
26 |
27 | def test_cli_xml_to_json_to_stdout(self):
28 | _input_fmt = 'xml'
29 | _output_fmt = 'json'
30 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
31 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
32 | _s = seria.load(f)
33 | _comp_val = _s.dump(_output_fmt)
34 | assert result.exit_code == 0
35 | assert result.output == _comp_val
36 |
37 |
38 | def test_cli_xml_to_yaml_to_stdout(self):
39 | _input_fmt = 'xml'
40 | _output_fmt = 'yaml'
41 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
42 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
43 | _s = seria.load(f)
44 | _comp_val = _s.dump(_output_fmt)
45 | assert result.exit_code == 0
46 | assert result.output == _comp_val
47 |
48 |
49 | def test_cli_json_to_json_to_stdout(self):
50 | _input_fmt = 'json'
51 | _output_fmt = 'json'
52 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
53 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
54 | _s = seria.load(f)
55 | _comp_val = _s.dump(_output_fmt)
56 | assert result.exit_code == 0
57 | assert result.output == _comp_val
58 |
59 | def test_cli_json_to_xml_to_stdout(self):
60 | _input_fmt = 'json'
61 | _output_fmt = 'xml'
62 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
63 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
64 | _s = seria.load(f)
65 | _comp_val = _s.dump(_output_fmt)
66 | assert result.exit_code == 0
67 | assert result.output == _comp_val
68 |
69 |
70 | def test_cli_json_to_yaml_to_stdout(self):
71 | _input_fmt = 'json'
72 | _output_fmt = 'yaml'
73 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
74 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
75 | _s = seria.load(f)
76 | _comp_val = _s.dump(_output_fmt)
77 | assert result.exit_code == 0
78 | assert result.output == _comp_val
79 |
80 | def test_cli_yaml_to_yaml_to_stdout(self):
81 | _input_fmt = 'yaml'
82 | _output_fmt = 'yaml'
83 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
84 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
85 | _s = seria.load(f)
86 | _comp_val = _s.dump(_output_fmt)
87 | assert result.exit_code == 0
88 | assert result.output == _comp_val
89 |
90 | def test_cli_yaml_to_json_to_stdout(self):
91 | _input_fmt = 'yaml'
92 | _output_fmt = 'json'
93 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
94 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
95 | _s = seria.load(f)
96 | _comp_val = _s.dump(_output_fmt)
97 | assert result.exit_code == 0
98 | assert result.output == _comp_val
99 |
100 | def test_cli_yaml_to_xml_to_stdout(self):
101 | _input_fmt = 'yaml'
102 | _output_fmt = 'xml'
103 | result = runner.invoke(cli.cli, ['--%s' % (_output_fmt), "%s/good.%s" % (test_resources, _input_fmt)], '-')
104 | with open("%s/good.%s" % (test_resources, _input_fmt), 'rb') as f:
105 | _s = seria.load(f)
106 | _comp_val = _s.dump(_output_fmt)
107 | assert result.exit_code == 0
108 | assert result.output == _comp_val
--------------------------------------------------------------------------------
/tests/test_seria.py:
--------------------------------------------------------------------------------
1 | from seria import compat
2 |
3 | import pytest
4 |
5 | import os
6 | test_resources = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'resources')
7 | from seria.compat import StringIO
8 |
9 | import seria
10 | from seria.serializers import get_formats, Serializer
11 | from seria.providers import XML, YAML, JSON
12 |
13 |
14 | class TestSeriaFuncs(object):
15 | def test_validate_xml_good(self):
16 | with open(os.path.join(test_resources, 'good.xml'), 'r') as f:
17 | _ = XML.is_it(f)
18 | assert _ == True
19 |
20 | def test_validate_xml_bad(self):
21 | with open(os.path.join(test_resources, 'bad.xml'), 'r') as f:
22 | _ = XML.is_it(f)
23 | assert _ == False
24 |
25 | def test_validate_json_good(self):
26 | with open(os.path.join(test_resources, 'good.json'), 'r') as f:
27 | _ = JSON.is_it(f)
28 | assert _ == True
29 |
30 | def test_validate_json_bad(self):
31 | with open(os.path.join(test_resources, 'bad.json'), 'r') as f:
32 | _ = JSON.is_it(f)
33 | assert _ == False
34 |
35 | def test_validate_yaml_good(self):
36 | with open(os.path.join(test_resources, 'good.yaml'), 'r') as f:
37 | _ = YAML.is_it(f)
38 | assert _ == True
39 |
40 | def test_validate_yaml_bad(self):
41 | with open(os.path.join(test_resources, 'bad.yaml'), 'r') as f:
42 | _ = YAML.is_it(f)
43 | assert _ == False
44 |
45 | def test_validate_yaml_with_good_json_input(self):
46 | """
47 | YAML is technically a superset of json, ie valid json == valid yaml
48 | """
49 | with open(os.path.join(test_resources, 'good.json'), 'r') as f:
50 | _ = YAML.is_it(f)
51 | assert _ == True
52 |
53 | def test_validate_yaml_with_bad_json_input(self):
54 | with open(os.path.join(test_resources, 'bad.json'), 'r') as f:
55 | _ = YAML.is_it(f)
56 | assert _ == False
57 |
58 |
59 | def test_get_format_with_good_json_input(self):
60 | with open(os.path.join(test_resources, 'good.json'), 'r') as f:
61 | _ = get_formats(f)
62 | assert _ == (True, False, True)
63 |
64 | def test_get_format_with_bad_json_input(self):
65 | with open(os.path.join(test_resources, 'bad.json'), 'r') as f:
66 | _ = get_formats(f)
67 | assert _ == (False, False, False)
68 |
69 |
70 | def test_get_format_with_good_yaml_input(self):
71 | with open(os.path.join(test_resources, 'good.yaml'), 'r') as f:
72 | _ = get_formats(f)
73 | assert _ == (False, False, True)
74 |
75 | def test_get_format_with_bad_yaml_input(self):
76 | with open(os.path.join(test_resources, 'bad.yaml'), 'r') as f:
77 | _ = get_formats(f)
78 | assert _ == (False, False, False)
79 |
80 | def test_get_format_with_good_xml_input(self):
81 | with open(os.path.join(test_resources, 'good.xml'), 'r') as f:
82 | _ = get_formats(f)
83 | assert _ == (False, True, False)
84 |
85 | def test_get_format_with_bad_xml_input(self):
86 | with open(os.path.join(test_resources, 'bad.xml'), 'r') as f:
87 | _ = get_formats(f)
88 | assert _ == (False, False, False)
89 |
90 |
91 | class TestSeria(object):
92 | def test_get_formats_when_stream_changes(self):
93 | with open(os.path.join(test_resources, 'good.xml'), 'r') as f:
94 | _stream = seria.load(f)
95 | assert _stream.formats == (False, True, False)
96 | with open(os.path.join(test_resources, 'good.yaml'), 'r') as f:
97 | _stream = seria.load(f)
98 | assert _stream.formats == (False, False, True)
99 |
100 | def test_format_property_access(self):
101 | with open(os.path.join(test_resources, 'good.xml'), 'r') as f:
102 | _stream = seria.load(f)
103 | assert _stream.is_json == False
104 | assert _stream.is_xml == True
105 | assert _stream.is_yaml == False
106 |
107 |
108 | class TestSeriaErrors(object):
109 | def test_input_must_be_flo(self):
110 | with pytest.raises(Serializer.Error):
111 | _ = seria.load("somestring")
112 |
113 |
114 | class TestSeriaRoundTrips(object):
115 | def test_xml_to_json_to_xml(self):
116 | _source_fmt = 'xml'
117 | _target_fmt = 'json'
118 | with open(os.path.join(test_resources, 'good.%s' % _source_fmt), 'r') as _a:
119 | _a_seria = seria.load(_a)
120 | _b = StringIO()
121 | _b.write(_a_seria.dump(fmt=_target_fmt))
122 | _b_seria = seria.load(_b)
123 | _c = StringIO()
124 | _c.write(_b_seria.dump(fmt=_source_fmt, pretty=True, newl='\n', indent=' '))
125 | _c.seek(0)
126 | _a.seek(0)
127 | assert _a.read() == _c.read()
128 |
129 | def test_xml_to_yaml_to_xml(self):
130 | _source_fmt = 'xml'
131 | _target_fmt = 'yaml'
132 | with open(os.path.join(test_resources, 'good.%s' % _source_fmt), 'r') as _a:
133 | _a_seria = seria.load(_a)
134 | _b = StringIO()
135 | _b.write(_a_seria.dump(fmt=_target_fmt))
136 | _b_seria = seria.load(_b)
137 | _c = StringIO()
138 | _c.write(_b_seria.dump(fmt=_source_fmt, pretty=True, newl='\n', indent=' '))
139 | _c.seek(0)
140 | _a.seek(0)
141 | assert _a.read() == _c.read()
142 |
143 | # def test_json_to_xml_to_json(self):
144 | # _source_fmt = 'json'
145 | # _target_fmt = 'xml'
146 | # with open(os.path.join(test_resources, 'good.%s' % _source_fmt), 'r') as _a:
147 | # _a_seria = seria.load(_a)
148 | # _b = StringIO()
149 | # _b.write(_a_seria.dump(fmt=_target_fmt))
150 | # _b_seria = seria.load(_b)
151 | # _c = StringIO()
152 | # _c.write(_b_seria.dump(fmt=_source_fmt))
153 | # _a.seek(0)
154 | # _c.seek(0)
155 | # assert _a.read() == _c.read()
156 |
157 | # def test_json_to_yaml_to_json(self):
158 | # _source_fmt = 'json'
159 | # _target_fmt = 'yaml'
160 | # with open(os.path.join(test_resources, 'good.%s' % _source_fmt), 'r') as _a:
161 | # _a_seria = seria.load(_a)
162 | # _b = StringIO()
163 | # _b.write(_a_seria.dump(fmt=_target_fmt))
164 | # _b_seria = seria.load(_b)
165 | # _c = StringIO()
166 | # _c.write(_b_seria.dump(fmt=_source_fmt))
167 | # _a.seek(0)
168 | # _c.seek(0)
169 | # assert _a.read() == _c.read()
170 |
171 | def test_yaml_to_json_to_yaml(self):
172 | _source_fmt = 'yaml'
173 | _target_fmt = 'json'
174 | with open(os.path.join(test_resources, 'good.%s' % _source_fmt), 'r') as _a:
175 | _a_seria = seria.load(_a)
176 | _b = StringIO()
177 | _b.write(_a_seria.dump(fmt=_target_fmt))
178 | _b_seria = seria.load(_b)
179 | _c = StringIO()
180 | _c.write(_b_seria.dump(fmt=_source_fmt))
181 | _a.seek(0)
182 | _c.seek(0)
183 | assert _a.read() == _c.read()
184 |
185 | def test_yaml_to_xml_to_yaml(self):
186 | _source_fmt = 'yaml'
187 | _target_fmt = 'xml'
188 | with open(os.path.join(test_resources, 'good.%s' % _source_fmt), 'r') as _a:
189 | _a_seria = seria.load(_a)
190 | _b = StringIO()
191 | _b.write(_a_seria.dump(fmt=_target_fmt))
192 | _b_seria = seria.load(_b)
193 | _c = StringIO()
194 | _c.write(_b_seria.dump(fmt=_source_fmt))
195 | _a.seek(0)
196 | _c.seek(0)
197 | assert _a.read() == _c.read()
--------------------------------------------------------------------------------
/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | from seria.utils import str_to_num, set_defaults
2 | import pytest
3 |
4 |
5 | class TestSeriaUtils(object):
6 | # def test_set_default_add(self):
7 | # kwargs = {"arg1": True}
8 | # defaults = {"arg2": True}
9 | # combined = dict(kwargs.items() + defaults.items())
10 | # set_defaults(kwargs, defaults)
11 | # assert kwargs == combined
12 | #
13 | def test_set_default_dont_overide(self):
14 | kwargs = {"arg1": False}
15 | defaults = {"arg1": True}
16 | set_defaults(kwargs, defaults)
17 | assert kwargs == {"arg1": False}
--------------------------------------------------------------------------------