├── .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} --------------------------------------------------------------------------------