├── .gitignore ├── COPYING ├── LICENSE ├── LICENSE.CECILL-C ├── LICENSE.LGPL ├── MANIFEST ├── README ├── VERSION ├── doc ├── conf.py ├── developers.rst ├── index.rst └── users.rst ├── gexf ├── __init__.py └── _gexf.py ├── setup.py └── test ├── exp.gexf ├── gexf.net.dynamics_openintervals.gexf ├── test.py └── test_pygexf.py /.gitignore: -------------------------------------------------------------------------------- 1 | html 2 | *.pyc 3 | dist 4 | build 5 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright médialab SciencesPo (2013) 2 | 3 | 4 | This program is free software distributed under the terms of two licenses, the 5 | CeCILL-C license that fits European law, and the GNU Lesser General Public 6 | License. You can use, modify and/ or redistribute the software under the terms 7 | of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following 8 | URL or under the terms of the GNU LGPL as published by 9 | the Free Software Foundation, either version 3 of the License, or (at your 10 | option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, but WITHOUT ANY 13 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 14 | PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public License 17 | along with this program. If not, see . 18 | 19 | The fact that you are presently reading this means that you have had 20 | knowledge of the CeCILL-C and LGPL licenses and that you accept their terms. 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgirard/pygexf/4ec08e9b8c381e030c30f86a562712b681d97e55/LICENSE -------------------------------------------------------------------------------- /LICENSE.CECILL-C: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgirard/pygexf/4ec08e9b8c381e030c30f86a562712b681d97e55/LICENSE.CECILL-C -------------------------------------------------------------------------------- /LICENSE.LGPL: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | README 2 | setup.py 3 | gexf/__init__.py 4 | gexf/_gexf.py 5 | test/test_pygexf.py 6 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgirard/pygexf/4ec08e9b8c381e030c30f86a562712b681d97e55/README -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.2 2 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Pygexf documentation build configuration file, created by 4 | # sphinx-quickstart on Fri Jun 18 16:06:18 2010. 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.append(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.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode'] 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'Pygexf' 44 | copyright = u'2010, Paul Girard' 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 = '0.2' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.2.2' 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 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = '' 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'Pygexfdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | # The paper size ('letter' or 'a4'). 173 | #latex_paper_size = 'letter' 174 | 175 | # The font size ('10pt', '11pt' or '12pt'). 176 | #latex_font_size = '10pt' 177 | 178 | # Grouping the document tree into LaTeX files. List of tuples 179 | # (source start file, target name, title, author, documentclass [howto/manual]). 180 | latex_documents = [ 181 | ('index', 'Pygexf.tex', u'Pygexf Documentation', 182 | u'Paul Girard', 'manual'), 183 | ] 184 | 185 | # The name of an image file (relative to this directory) to place at the top of 186 | # the title page. 187 | #latex_logo = None 188 | 189 | # For "manual" documents, if this is true, then toplevel headings are parts, 190 | # not chapters. 191 | #latex_use_parts = False 192 | 193 | # If true, show page references after internal links. 194 | #latex_show_pagerefs = False 195 | 196 | # If true, show URL addresses after external links. 197 | #latex_show_urls = False 198 | 199 | # Additional stuff for the LaTeX preamble. 200 | #latex_preamble = '' 201 | 202 | # Documents to append as an appendix to all manuals. 203 | #latex_appendices = [] 204 | 205 | # If false, no module index is generated. 206 | #latex_domain_indices = True 207 | 208 | 209 | # -- Options for manual page output -------------------------------------------- 210 | 211 | # One entry per manual page. List of tuples 212 | # (source start file, name, description, authors, manual section). 213 | man_pages = [ 214 | ('index', 'pygexf', u'Pygexf Documentation', 215 | [u'Paul Girard'], 1) 216 | ] 217 | -------------------------------------------------------------------------------- /doc/developers.rst: -------------------------------------------------------------------------------- 1 | 2 | welcome developers 3 | ------------------ 4 | 5 | versioning system used is git, hosted in github : http://github.com/paul.girard/pygexf 6 | 7 | documentation is on the way... 8 | 9 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Pygexf documentation master file, created by 2 | sphinx-quickstart on Fri Jun 18 16:06:18 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Pygexf's documentation! 7 | ================================== 8 | 9 | Hello World ! 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | 16 | for users 17 | for developers 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /doc/users.rst: -------------------------------------------------------------------------------- 1 | welcome users ! 2 | --------------- 3 | 4 | 5 | installation 6 | ============ 7 | 8 | requirements 9 | ~~~~~~~~~~~~ 10 | 11 | pygexf uses lxml as XML engine. 12 | you'll need lxml to use it. 13 | 14 | See http://codespeak.net/lxml/ for installation 15 | 16 | easy_install method 17 | ~~~~~~~~~~~~~~~~~~~ 18 | 19 | pygexf is hosted in pypi.python.org. 20 | Thus you can use easy_install from setuptools to install it. 21 | 22 | First install setup tools if not already in your system : http://pypi.python.org/pypi/setuptools 23 | 24 | Then it's quite easy : 25 | 26 | $ sudo easy_install pygexf 27 | 28 | to check : 29 | 30 | $ python 31 | 32 | 33 | >>> import gexf 34 | 35 | If no errors are raised then you're done. 36 | 37 | 38 | embed pygexf in your project 39 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 40 | 41 | This is a quick and durty method. Use easy_install instead. 42 | (but still sometime usefull) 43 | 44 | pygexf is a single file package. 45 | You can decide not to install it in your python environement but directly in your source where you need it. 46 | In such case go directly to the soruce repository : 47 | http://github.com/paulgirard/pygexf 48 | 49 | Download the gexf directory to your source. 50 | $ls 51 | gexf 52 | 53 | $python 54 | 55 | 56 | >>> import gexf 57 | 58 | no errors ? (remember to install lxml first !) 59 | you're done. 60 | 61 | 62 | Use Cases 63 | ========= 64 | 65 | You can find useful information here : http://gexf.net and here http://forum.gephi.org, before a real comfy and warm documentation is available. 66 | 67 | -------------------------------------------------------------------------------- /gexf/__init__.py: -------------------------------------------------------------------------------- 1 | from ._gexf import Gexf 2 | from ._gexf import Node 3 | from ._gexf import Edge 4 | from ._gexf import Graph 5 | from ._gexf import GexfImport 6 | -------------------------------------------------------------------------------- /gexf/_gexf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # 4 | # Gexf library in python 5 | # see gephi.org and gexf.net 6 | # 7 | # repository : http://github.com/paulgirard/pygexf 8 | # documentation : http://packages.python.org/pygexf 9 | # 10 | # main developper : Paul Girard, médialab Sciences Po 11 | # licence : GPL v3 12 | # 13 | 14 | from lxml import etree 15 | from datetime import date 16 | import itertools 17 | import traceback 18 | 19 | # missing features : 20 | # data validation regarding attribute types 21 | # phylogeny 22 | 23 | 24 | # evolution ideas : 25 | # add display stats on graph composition when exportingto xml 26 | # add anti-paralell edges test 27 | 28 | def msg_unexpected_tag(expected, got): 29 | print("Error : incorrect xml. Expected tag {expected}, not {got}.".format(expected=expected, got=got)) 30 | 31 | 32 | def ns_clean(token): 33 | i = token.find('}') 34 | return token[i + 1:] 35 | 36 | 37 | class Gexf: 38 | 39 | def __init__(self, creator, description): 40 | self.creator = creator 41 | self.description = description 42 | self.graphs = [] 43 | self.xmlns = "http://www.gephi.org/gexf/1.2draft" 44 | self.xsi = "http://www.w3.org/2001/XMLSchema-instance" 45 | self.schemaLocation = "http://www.gephi.org/gexf/1.1draft http://gephi.org/gexf/1.2draft.xsd" 46 | self.viz = "http://www.gexf.net/1.2draft/viz" 47 | self.version = "1.2" 48 | 49 | def addGraph(self, type, mode, label, timeformat=""): 50 | g = Graph(type, mode, label, timeformat) 51 | self.graphs.append(g) 52 | return g 53 | 54 | def getXML(self): 55 | gexfXML = etree.Element("{" + self.xmlns + "}gexf", version=self.version, nsmap={None: self.xmlns, 'viz': self.viz, 'xsi': self.xsi}) 56 | # gexfXML.set("xmlnsxsi",) 57 | gexfXML.set("{xsi}schemaLocation", self.schemaLocation) 58 | meta = etree.SubElement(gexfXML, "meta") 59 | meta.set("lastmodified", date.today().isoformat()) 60 | etree.SubElement(meta, "creator").text = self.creator 61 | etree.SubElement(meta, "description").text = self.description 62 | for graph in self.graphs: 63 | gexfXML.append(graph.getXML()) 64 | 65 | return gexfXML 66 | 67 | def write(self, file, print_stat=True): 68 | file.write(etree.tostring(self.getXML(), pretty_print=True, encoding='utf-8', xml_declaration=True)) 69 | if print_stat is True: 70 | self.print_stat() 71 | 72 | def print_stat(self): 73 | for graph in self.graphs: 74 | graph.print_stat() 75 | 76 | @staticmethod 77 | def importXML(gexf_file): 78 | """ import gexf xml meta tags to create a Gexf Object and delegate Graph extraction to Graph class""" 79 | # parse the gexf file 80 | parser = etree.XMLParser(ns_clean=True) 81 | tree = etree.parse((gexf_file), parser) 82 | # start create Gexf Object 83 | gexf_xml = tree.getroot() 84 | tag = ns_clean(gexf_xml.tag).lower() 85 | if tag != "gexf": 86 | msg_unexpected_tag("gexf", tag) 87 | return 88 | gexf_obj = None 89 | for child in gexf_xml: 90 | tag = ns_clean(child.tag).lower() 91 | # create a gexf object by importing meta tag 92 | if tag == "meta": 93 | meta_xml = child 94 | for child in meta_xml: 95 | tag = ns_clean(child.tag).lower() 96 | if tag == "creator": 97 | creator = child.text 98 | if tag == "description": 99 | description = child.text 100 | gexf_obj = Gexf(creator=creator, description=description) 101 | # export graph xml through Graph Class 102 | if tag == "graph": 103 | graph_xml = child 104 | if gexf_obj is None: 105 | msg_unexpected_tag("meta", tag) 106 | return 107 | Graph.importXML(graph_xml, gexf_obj) 108 | return gexf_obj 109 | 110 | 111 | class Graph: 112 | 113 | def __init__(self, type, mode, label, time_format="double", start="", end=""): 114 | 115 | # control variable 116 | self.authorizedType = ("directed", "undirected") 117 | self.authorizedMode = ("dynamic", "static") 118 | # time format 119 | # Discrete: integer or double 120 | # Continuous : date (yyyy-mm-dd) or dateTime 121 | # default : double 122 | self.authorizedTimeFormat = ("integer", "double", "date", "dateTime") 123 | 124 | self.defaultTimeFormat = "double" 125 | self.defaultType = "directed" 126 | self.defaultMode = "static" 127 | 128 | self.label = label 129 | 130 | if type in self.authorizedType: 131 | self.type = type 132 | else: 133 | self.type = self.defaultType 134 | if mode in self.authorizedMode: 135 | self.mode = mode 136 | else: 137 | self.mode = self.defaultMode 138 | 139 | if time_format in self.authorizedTimeFormat: 140 | self.time_format = time_format 141 | else: 142 | self.time_format = self.defaultTimeFormat 143 | 144 | self.start = start 145 | self.end = end 146 | 147 | self._attributes = Attributes() 148 | self.attributes = self._attributes 149 | self._nodes = {} 150 | self.nodes = self._nodes 151 | self._edges = {} 152 | self.edges = self._edges 153 | 154 | def addNode(self, id, label, start="", end="", startopen=False, endopen=False, pid="", r="", g="", b="", spells=[]): 155 | self._nodes[str(id)] = Node(self, id, label, start, end, pid, r, g, b, spells, startopen, endopen) 156 | return self._nodes[str(id)] 157 | 158 | def nodeExists(self, id): 159 | if id in self._nodes.keys(): 160 | return 1 161 | else: 162 | return 0 163 | 164 | def addEdge(self, id, source, target, weight="", start="", end="", label="", r="", g="", b="", spells=[], startopen=False, endopen=False): 165 | self._edges[str(id)] = Edge(self, id, source, target, weight, start, end, label, r, g, b, spells, startopen, endopen) 166 | return self._edges[str(id)] 167 | 168 | def addNodeAttribute(self, title, defaultValue=None, type="integer", mode="static", force_id=""): 169 | # add to NodeAttributes 170 | return self._attributes.declareAttribute("node", type, defaultValue, title, mode, force_id) 171 | 172 | def addDefaultAttributesToNode(self, node): 173 | """ deprecated """ 174 | pass 175 | 176 | def checkNodeAttribute(self, id, value, start, end): 177 | """deprecated""" 178 | pass 179 | # check conformity with type is missing 180 | # if id in self._nodesAttributes.keys() : 181 | # if self._nodesAttributes[id]["mode"]=="static" and ( not start=="" or not end=="") : 182 | # raise Exception("attribute "+str(id)+" is static you can't specify start or end dates. Declare Attribute as dynamic") 183 | # return 1 184 | # else : 185 | # raise Exception("attribute id unknown. Add Attribute to graph first") 186 | 187 | def addEdgeAttribute(self, title, defaultValue, type="integer", mode="static", force_id=""): 188 | return self._attributes.declareAttribute("edge", type, defaultValue, title, mode, force_id) 189 | 190 | def addDefaultAttributesToEdge(self, edge): 191 | """ deprecated """ 192 | pass 193 | 194 | def checkEdgeAttribute(self, id, value, start, end): 195 | """deprecated """ 196 | pass 197 | # # check conformity with type is missing 198 | # if id in self._edgesAttributes.keys() : 199 | # if self._edgesAttributes[id]["mode"]=="static" and ( not start=="" or not end=="") : 200 | # raise Exception("attribute "+str(id)+" is static you can't specify start or end dates. Declare Attribute as dynamic") 201 | # return 1 202 | # else : 203 | # raise Exception("attribute id unknown. Add Attribute to graph first") 204 | 205 | def getXML(self): 206 | # return lxml etree element 207 | graphXML = etree.Element("graph", defaultedgetype=self.type, mode=self.mode, label=self.label, timeformat=self.time_format) 208 | 209 | for attributesElement in self.attributes.getAttributesDeclarationXML(): 210 | graphXML.append(attributesElement) 211 | 212 | nodesXML = etree.SubElement(graphXML, "nodes") 213 | node_ids = list(self._nodes.keys()) 214 | node_ids.sort() 215 | for id in node_ids: 216 | nodesXML.append(self._nodes[id].getXML()) 217 | 218 | edgesXML = etree.SubElement(graphXML, "edges") 219 | edge_ids = list(self._edges.keys()) 220 | edge_ids.sort() 221 | for id in edge_ids: 222 | edgesXML.append(self._edges[id].getXML()) 223 | 224 | return graphXML 225 | 226 | @staticmethod 227 | def importXML(graph_xml, gexf_obj): 228 | """ import graph xml tag to create a Graph Object and delegate Node/Edges extraction to Edge/Node class""" 229 | # get Graph attributes 230 | type = "" 231 | mode = "" 232 | label = "" 233 | timeformat = "double" 234 | for attr in graph_xml.attrib: 235 | attr = attr.lower() 236 | if attr == "defaultedgetype": 237 | type = graph_xml.attrib[attr] 238 | if attr == "mode": 239 | mode = graph_xml.attrib[attr] 240 | if attr == "label": 241 | label = graph_xml.attrib[attr] 242 | if attr == "timeformat": 243 | timeformat = graph_xml.attrib[attr] 244 | # create and attache the graph object to the Gexf object 245 | graph_obj = gexf_obj.addGraph(type=type, mode=mode, label=label, timeformat=timeformat) 246 | 247 | for child in graph_xml: 248 | tag = ns_clean(child.tag).lower() 249 | 250 | if tag == "attributes": 251 | attributes_xml = child 252 | # Delegate Attributes declaration to the attribute object 253 | graph_obj.attributes.importAttributesXML(attributes_xml) 254 | 255 | if tag == "nodes": 256 | nodes_xml = child 257 | # Delegate nodes creation to the Node class 258 | Node.importXML(nodes_xml, graph_obj) 259 | if tag == "edges": 260 | edges_xml = child 261 | # Delegate edges creation to the Edge class 262 | Edge.importXML(edges_xml, graph_obj) 263 | 264 | def print_stat(self): 265 | print(self.label + " " + self.type + " " + self.mode + " " + self.start + " " + self.end) 266 | print("number of nodes : " + str(len(self._nodes))) 267 | print("number of edges : " + str(len(self._edges))) 268 | 269 | 270 | class Attributes(dict): 271 | """ 272 | attributes= 273 | { 274 | "node" : 275 | { "id1" : {"id":"id1","title":"age","type":"integer","defaultValue":50,"mode":"static"}, }, 276 | "edge" : 277 | { "id2" : {"id":"id2","title":"relationship","type":"string","defaultValue":"friend",mode:"dynamic"}, }, 278 | } 279 | 280 | 281 | """ 282 | 283 | def __init__(self): 284 | self.type_choices = ["integer", "string", "float", "double", "boolean", "date", "URI"] 285 | self.attClass_choices = ["node", "edge"] 286 | self.mode_choices = ["static", "dynamic"] 287 | for attClass in self.attClass_choices: 288 | self[attClass] = {} 289 | 290 | def declareAttribute(self, attClass, type, defaultValue, title="", mode="static", id=None): 291 | """ 292 | add a new attribute declaration to the graph 293 | """ 294 | if attClass in self.attClass_choices: 295 | # should add quality control here on type and defaultValue 296 | # if no id given generating a numerical one based on dict length 297 | if not id: 298 | id = str(len(self[attClass])) 299 | self[attClass][id] = {"id": id, "type": type, "defaultValue": defaultValue, "mode": mode, "title": title} 300 | return id 301 | else: 302 | raise Exception("wrong attClass : " + str(attClass) + " Should be in " + str(type_choices)) 303 | 304 | def makeAttributeInstance(self, attClass, id=None, value=None, start=None, end=None, startopen=False, endopen=False): 305 | """ 306 | generate an attribute to be include to a node or edge. 307 | copied from the declared attributes, thus any attribute has to be declared first 308 | """ 309 | if attClass in self.attClass_choices: 310 | if id in self[attClass].keys(): 311 | att = {"id": id} 312 | att["value"] = value if value else self[attClass][id]["defaultValue"] 313 | if self[attClass][id]["mode"] == "dynamic" and start or end: 314 | # start & end will be discarded if the mode is set to static 315 | if start: 316 | att["start"] = start 317 | if startopen: 318 | att["startopen"] = startopen 319 | if end: 320 | att["end"] = end 321 | if endopen: 322 | att["endopen"] = endopen 323 | return att 324 | else: 325 | raise Exception("wrong attribute id (%s), declare the attribute first with declareAttribute" % (id, )) 326 | else: 327 | raise Exception("wrong attClass : " + str(attClass) + " Should be in " + str(self.type_choices)) 328 | 329 | def getAttributesDeclarationXML(self): 330 | """ generate attributes declaration XML """ 331 | # return lxml etree element 332 | allAttributesXML = [] 333 | if len(self) > 0: 334 | # iter on node and then edge atts 335 | for attClass, atts in self.items(): 336 | # group by mode 337 | key_mode = lambda att: att["mode"] 338 | atts_sorted_by_mode = sorted(list(atts.values()), key=key_mode, reverse=True) 339 | for mode, atts in itertools.groupby(atts_sorted_by_mode, key_mode): 340 | # generate on attributes by mode 341 | attributesXML = etree.Element("attributes") 342 | attributesXML.set("class", attClass) 343 | attributesXML.set("mode", mode) 344 | # generate attribute by id order 345 | for att in sorted(atts, key=lambda att: att["id"]): 346 | attributeXML = etree.SubElement(attributesXML, "attribute") 347 | attributeXML.set("id", str(att["id"])) 348 | attributeXML.set("title", att["title"]) 349 | attributeXML.set("type", att["type"]) 350 | if att["defaultValue"]: 351 | etree.SubElement(attributeXML, "default").text = att["defaultValue"] 352 | allAttributesXML.append(attributesXML) 353 | return allAttributesXML 354 | 355 | @staticmethod 356 | def getAttributesXML(atts): 357 | """ get XML attValues for an element (Node or Edge) by passing an attribute values list (stored in Nodes and Edges)""" 358 | if len(atts) > 0: 359 | attValuesXML = etree.Element("attvalues") 360 | for att in atts: 361 | attValueXML = etree.SubElement(attValuesXML, "attvalue") 362 | attValueXML.set("for", str(att["id"])) 363 | attValueXML.set("value", att["value"]) 364 | if "start" in att.keys() and not att["start"] == "": 365 | attValueXML.set("start" if not "startopen" in att.keys() or not att["startopen"] else "startopen", att["start"]) 366 | if "end" in att.keys() and not att["end"] == "": 367 | attValueXML.set("end" if not "endopen" in att.keys() or not att["endopen"] else "endopen", att["end"]) 368 | return attValuesXML 369 | else: 370 | return None 371 | 372 | def importAttributesXML(self, attributes_xml): 373 | """ get XML attributes declaration of a graph gexf""" 374 | attr_class = None 375 | mode = "" 376 | for attr in attributes_xml.attrib: 377 | attr = attr.lower() 378 | if attr == "class": 379 | attr_class = attributes_xml.attrib[attr].lower() 380 | if attr == "mode": 381 | mode = attributes_xml.attrib[attr] 382 | 383 | for child in attributes_xml: 384 | tag = ns_clean(child.tag).lower() 385 | if tag == "attribute": 386 | attribute_xml = child 387 | id = "" 388 | title = "" 389 | type = "" 390 | 391 | for attr in attribute_xml.attrib: 392 | attr = attr.lower() 393 | if attr == "id": 394 | id = attribute_xml.attrib[attr] 395 | if attr == "title": 396 | title = attribute_xml.attrib[attr] 397 | if attr == "type": 398 | type = attribute_xml.attrib[attr] 399 | 400 | default = "" 401 | 402 | for child in attribute_xml: 403 | tag = ns_clean(child.tag).lower() 404 | if tag == "default": 405 | default = child.text 406 | 407 | self.declareAttribute(attr_class, type, default, title, mode, id) 408 | 409 | def importAttributesValuesXML(self, attClass, attvalues_xml): 410 | """ import attributes values from attvalues gexf xml tag attached to nodes or edges""" 411 | atts = [] 412 | for attvalues in attvalues_xml: 413 | for child in attvalues: 414 | tag = ns_clean(child.tag).lower() 415 | if tag == "attvalue": 416 | attvalue_xml = child 417 | id = "" 418 | value = "" 419 | start = "" 420 | startopen = False 421 | end = "" 422 | endopen = False 423 | for attr in attvalue_xml.attrib: 424 | if attr == "for": 425 | id = attvalue_xml.attrib[attr] 426 | if attr == "value": 427 | value = attvalue_xml.attrib[attr] 428 | if attr == "start": 429 | start = attvalue_xml.attrib[attr] 430 | if attr == "end": 431 | end = attvalue_xml.attrib[attr] 432 | if attr == "startopen": 433 | start = attvalue_xml.attrib[attr] 434 | startopen = True 435 | if attr == "endopen": 436 | end = attvalue_xml.attrib[attr] 437 | endopen = True 438 | 439 | atts.append(self.makeAttributeInstance(attClass, id, value, start, end, startopen, endopen)) 440 | return atts 441 | 442 | 443 | class Spells(list): 444 | ''' 445 | spells are time periods 446 | spells is a list of dictionaries 447 | a spell is a dict : {"start":"YYYY-MM-DD","end":"YYYY-MM-DD"} 448 | ''' 449 | 450 | def getXML(self): 451 | 452 | spellsXML = etree.Element("spells") 453 | for spell in self: 454 | spellXML = etree.SubElement(spellsXML, "spell") 455 | if "start" in spell.keys(): 456 | spellXML.set("start", spell["start"]) 457 | if "end" in spell.keys(): 458 | spellXML.set("end", spell["end"]) 459 | return spellsXML 460 | 461 | @staticmethod 462 | def importXML(spellsxmltree): 463 | return Spells([spell.attrib for spell in spellsxmltree]) 464 | 465 | 466 | class Node: 467 | 468 | def __init__(self, graph, id, label, start="", end="", pid="", r="", g="", b="", spells=[], startopen=False, endopen=False): 469 | self.id = id 470 | self.label = label 471 | self.start = start 472 | self.startopen = startopen 473 | self.end = end 474 | self.endopen = endopen 475 | self.pid = pid 476 | self._graph = graph 477 | self.setColor(r, g, b) 478 | 479 | #spells expecting format = [{start:"",end:""},...] 480 | self.spells = spells 481 | 482 | if not self.pid == "": 483 | if not self._graph.nodeExists(self.pid): 484 | raise Exception("pid " + self.pid + " node unknown, add nodes to graph first") 485 | 486 | self._attributes = [] 487 | self.attributes = self._attributes 488 | # add existing nodesattributes default values : bad idea and unecessary 489 | #self._graph.addDefaultAttributesToNode(self) 490 | 491 | def addAttribute(self, id, value, start="", end="", startopen=False, endopen=False): 492 | self._attributes.append(self._graph.attributes.makeAttributeInstance("node", id, value, start, end, startopen, endopen)) 493 | 494 | def getXML(self): 495 | # return lxml etree element 496 | try: 497 | nodeXML = etree.Element("node", id=str(self.id), label=self.label) 498 | if not self.start == "": 499 | nodeXML.set("start" if not self.startopen else "startopen", self.start) 500 | if not self.end == "": 501 | nodeXML.set("end" if not self.endopen else "endopen", self.end) 502 | if not self.pid == "": 503 | nodeXML.set("pid", self.pid) 504 | 505 | # attributes 506 | if self._attributes: 507 | nodeXML.append(Attributes.getAttributesXML(self._attributes)) 508 | 509 | # spells 510 | if self.spells: 511 | print("found spells in node " + self.id) 512 | nodeXML.append(self.spells.getXML()) 513 | 514 | 515 | if not self.r == "" and not self.g == "" and not self.b == "": 516 | #color : 517 | colorXML = etree.SubElement(nodeXML, "{http://www.gexf.net/1.1draft/viz}color") 518 | colorXML.set("r", self.r) 519 | colorXML.set("g", self.g) 520 | colorXML.set("b", self.b) 521 | 522 | return nodeXML 523 | except Exception as e: 524 | print(self.label) 525 | print(self._attributes) 526 | print(e) 527 | traceback.print_exc() 528 | exit() 529 | 530 | def getAttributes(self): 531 | attsFull = [] 532 | for att in self._attributes: 533 | attFull = self._graph.attributes["node"][att["id"]].copy() 534 | attFull.update(att) 535 | attsFull.append(attFull) 536 | return attsFull 537 | 538 | @staticmethod 539 | def importXML(nodes_xml, graph_obj): 540 | 541 | for child in nodes_xml: 542 | tag = ns_clean(child.tag).lower() 543 | if tag == "node": 544 | node_xml = child 545 | id = "" 546 | label = "" 547 | start = "" 548 | startopen = False 549 | end = "" 550 | endopen = False 551 | pid = "" 552 | r = "" 553 | g = "" 554 | b = "" 555 | 556 | for attr in node_xml.attrib: 557 | attr = attr.lower() 558 | if attr == "id": 559 | id = node_xml.attrib[attr] 560 | if attr == "label": 561 | label = node_xml.attrib[attr] 562 | if attr == "start": 563 | start = node_xml.attrib[attr] 564 | if attr == "end": 565 | start = node_xml.attrib[attr] 566 | if attr == "startopen": 567 | start = attvalue_xml.attrib[attr] 568 | startopen = True 569 | if attr == "endopen": 570 | end = attvalue_xml.attrib[attr] 571 | endopen = True 572 | if attr == "pid": 573 | pid = node_xml.attrib[attr] 574 | 575 | attvalues_xml = [] 576 | spells = [] 577 | 578 | for child in node_xml: 579 | tag = ns_clean(child.tag).lower() 580 | if tag == "attvalues": 581 | attvalues_xml.append(child) 582 | if tag == "viz:color": 583 | r = child.attrib["r"] 584 | g = child.attrib["g"] 585 | b = child.attrib["b"] 586 | if tag == "spells": 587 | spells = Spells.importXML(child) 588 | 589 | node_obj = graph_obj.addNode(id=id, label=label, start=start, end=end, startopen=startopen, endopen=endopen, pid=pid, r=r, g=g, b=b, spells=spells) 590 | node_obj._attributes = graph_obj.attributes.importAttributesValuesXML("node", attvalues_xml) 591 | 592 | def setColor(self, r, g, b): 593 | self.r = r 594 | self.g = g 595 | self.b = b 596 | 597 | def __str__(self): 598 | return self.label 599 | 600 | 601 | class Edge: 602 | 603 | def __init__(self, graph, id, source, target, weight="", start="", end="", label="", r="", g="", b="", spells=[], startopen=False, endopen=False): 604 | 605 | self.id = id 606 | self._graph = graph 607 | 608 | if self._graph.nodeExists(source): 609 | self._source = source 610 | self.source = self._source 611 | else: 612 | raise Exception("source " + source + " node unknown, add nodes to graph first") 613 | 614 | if self._graph.nodeExists(target): 615 | self._target = target 616 | self.target = self._target 617 | else: 618 | raise Exception("target " + target + " node unknown, add nodes to graph first") 619 | 620 | self.start = start 621 | self.startopen = startopen 622 | self.end = end 623 | self.endopen = endopen 624 | 625 | self.weight = weight 626 | self.label = label 627 | self._attributes = [] 628 | self.attributes = self._attributes 629 | # COLOR on edges now supported in GEXF 1.2 630 | self.setColor(r, g, b) 631 | 632 | #spells expecting format = [{start:"",end:""},...] 633 | self.spells = Spells(spells) 634 | # add existing nodesattributes default values : bad idea and unecessary 635 | #self._graph.addDefaultAttributesToEdge(self) 636 | 637 | def addAttribute(self, id, value, start="", end="", startopen=False, endopen=False): 638 | self._attributes.append(self._graph.attributes.makeAttributeInstance("edge", id, value, start, end, startopen, endopen)) 639 | 640 | def getXML(self): 641 | # return lxml etree element 642 | try: 643 | edgeXML = etree.Element("edge", id=str(self.id), source=str(self._source), target=str(self._target)) 644 | if not self.start == "": 645 | edgeXML.set("start" if not self.startopen else "startopen", self.start) 646 | if not self.end == "": 647 | edgeXML.set("end" if not self.endopen else "endopen", self.end) 648 | if not self.weight == "": 649 | edgeXML.set("weight", str(self.weight)) 650 | if not self.label == "": 651 | edgeXML.set("label", self.label) 652 | 653 | # attributes 654 | if self._attributes: 655 | edgeXML.append(Attributes.getAttributesXML(self._attributes)) 656 | 657 | # spells 658 | if self.spells: 659 | #spellsXML = etree.SubElement(edgeXML, "spells") 660 | #spellsXML.append(self.spells.getXML()) 661 | edgeXML.append(self.spells.getXML()) 662 | 663 | # COLOR on edges is supported in GEXF since 1.2 664 | if not self.r == "" and not self.g == "" and not self.b == "": 665 | #color : 666 | colorXML = etree.SubElement(edgeXML, "{http://www.gexf.net/1.2draft/viz}color") 667 | colorXML.set("r", self.r) 668 | colorXML.set("g", self.g) 669 | colorXML.set("b", self.b) 670 | 671 | return edgeXML 672 | except Exception as e: 673 | print(self._source + " " + self._target) 674 | print(e) 675 | exit() 676 | 677 | def getAttributes(self): 678 | attsFull = [] 679 | for att in self._attributes: 680 | attFull = self._graph.attributes["edge"][att["id"]].copy() 681 | attFull.update(att) 682 | attsFull.append(attFull) 683 | return attsFull 684 | 685 | @staticmethod 686 | def importXML(edges_xml, graph_obj): 687 | 688 | for child in edges_xml: 689 | 690 | tag = ns_clean(child.tag).lower() 691 | if tag == "edge": 692 | edge_xml = child 693 | id = "" 694 | source = "" 695 | target = "" 696 | weight = "" 697 | start = "" 698 | startopen = False 699 | end = "" 700 | endopen = False 701 | label = "" 702 | r = "" 703 | g = "" 704 | b = "" 705 | 706 | for attr in edge_xml.attrib: 707 | attr = attr.lower() 708 | if attr == "id": 709 | id = edge_xml.attrib[attr] 710 | if attr == "source": 711 | source = edge_xml.attrib[attr] 712 | if attr == "target": 713 | target = edge_xml.attrib[attr] 714 | if attr == "weight": 715 | weight = edge_xml.attrib[attr] 716 | if attr == "start": 717 | start = edge_xml.attrib[attr] 718 | if attr == "end": 719 | end = edge_xml.attrib[attr] 720 | if attr == "startopen": 721 | start = edge_xml.attrib[attr] 722 | startopen = True 723 | if attr == "endopen": 724 | end = edge_xml.attrib[attr] 725 | endopen = True 726 | if attr == "label": 727 | label = edge_xml.attrib[attr] 728 | 729 | spells = [] 730 | attvalues_xml = [] 731 | for child in edge_xml: 732 | tag = ns_clean(child.tag).lower() 733 | if tag == "attvalues": 734 | attvalues_xml.append(child) 735 | if tag == "spells": 736 | spells = Spells.importXML(child) 737 | if tag == "viz:color": 738 | r = child.attrib["r"] 739 | g = child.attrib["g"] 740 | b = child.attrib["b"] 741 | 742 | edge_obj = graph_obj.addEdge(id=id, source=source, target=target, weight=weight, start=start, end=end, startopen=startopen, endopen=endopen, label=label, r=r, g=g, b=b, spells=spells) 743 | edge_obj._attributes = graph_obj.attributes.importAttributesValuesXML("edge", attvalues_xml) 744 | 745 | # COLOR on edges is supported in GEXF since 1.2 746 | def setColor(self, r, g, b): 747 | self.r = r 748 | self.g = g 749 | self.b = b 750 | 751 | 752 | class GexfImport: 753 | # class coded by elie Rotenberg, médialab 20/07/2010 754 | # deprecated : import XML codes are now included to the Gexf, Graph, Attribute, Node, Edge classes 755 | 756 | def __init__(self, file_like): 757 | parser = etree.XMLParser(ns_clean=True) 758 | tree = etree.parse(file_like, parser) 759 | gexf_xml = tree.getroot() 760 | tag = self.ns_clean(gexf_xml.tag).lower() 761 | if tag != "gexf": 762 | self.msg_unexpected_tag("gexf", tag) 763 | return 764 | self.gexf_obj = None 765 | for child in gexf_xml: 766 | tag = self.ns_clean(child.tag).lower() 767 | if tag == "meta": 768 | meta_xml = child 769 | self.gexf_obj = self.extract_gexf_obj(meta_xml) 770 | if tag == "graph": 771 | graph_xml = child 772 | if self.gexf_obj == None: 773 | self.msg_unexpected_tag("meta", tag) 774 | return 775 | self.graph_obj = self.extract_graph_obj(graph_xml) 776 | 777 | def ns_clean(self, token): 778 | i = token.find('}') 779 | return token[i + 1:] 780 | 781 | def msg_unexpected_tag(self, expected, got): 782 | print("Error : incorrect xml. Expected tag {expected}, not {got}.".format(expected=expected, got=got)) 783 | 784 | def extract_gexf_obj(self, meta_xml): 785 | for child in meta_xml: 786 | tag = self.ns_clean(child.tag).lower() 787 | if tag == "creator": 788 | creator = child.text 789 | if tag == "description": 790 | description = child.text 791 | return Gexf(creator=creator, description=description) 792 | 793 | def extract_graph_obj(self, graph_xml): 794 | type = "" 795 | mode = "" 796 | label = "" 797 | timeformat = "double" 798 | for attr in graph_xml.attrib: 799 | attr = attr.lower() 800 | if attr == "defaultedgetype": 801 | type = graph_xml.attrib[attr] 802 | if attr == "mode": 803 | mode = graph_xml.attrib[attr] 804 | if attr == "label": 805 | label = graph_xml.attrib[attr] 806 | if attr == "timeformat": 807 | timeformat = graph_xml.attrib[attr] 808 | 809 | self.graph_obj = self.gexf_obj.addGraph(type=type, mode=mode, label=label, timeformat=timeformat) 810 | 811 | for child in graph_xml: 812 | tag = self.ns_clean(child.tag).lower() 813 | if tag == "attributes": 814 | attributes_xml = child 815 | self.extract_attributes(attributes_xml) 816 | if tag == "nodes": 817 | nodes_xml = child 818 | self.extract_nodes(nodes_xml) 819 | if tag == "edges": 820 | edges_xml = child 821 | self.extract_edges(edges_xml) 822 | 823 | def extract_attributes(self, attributes_xml): 824 | attr_class = None 825 | mode = "" 826 | for attr in attributes_xml.attrib: 827 | attr = attr.lower() 828 | if attr == "class": 829 | attr_class = attributes_xml.attrib[attr].lower() 830 | if attr == "mode": 831 | mode = attributes_xml.attrib[attr] 832 | 833 | for child in attributes_xml: 834 | tag = self.ns_clean(child.tag).lower() 835 | if tag == "attribute": 836 | attribute_xml = child 837 | self.extract_attribute(attribute_xml, attr_class, mode) 838 | 839 | def extract_attribute(self, attribute_xml, attr_class, mode): 840 | id = "" 841 | title = "" 842 | type = "" 843 | 844 | for attr in attribute_xml.attrib: 845 | attr = attr.lower() 846 | if attr == "id": 847 | id = attribute_xml.attrib[attr] 848 | if attr == "title": 849 | title = attribute_xml.attrib[attr] 850 | if attr == "type": 851 | type = attribute_xml.attrib[attr] 852 | 853 | default = "" 854 | 855 | for child in attribute_xml: 856 | tag = self.ns_clean(child.tag).lower() 857 | if tag == "default": 858 | default = child.text 859 | 860 | if attr_class == "node": 861 | self.graph_obj.addNodeAttribute(title, default, type, mode, force_id=id) 862 | 863 | if attr_class == "edge": 864 | self.graph_obj.addEdgeAttribute(title, default, type, mode, force_id=id) 865 | 866 | def extract_nodes(self, nodes_xml): 867 | for child in nodes_xml: 868 | tag = self.ns_clean(child.tag).lower() 869 | if tag == "node": 870 | node_xml = child 871 | self.extract_node(node_xml) 872 | 873 | def extract_node(self, node_xml): 874 | id = "" 875 | label = "" 876 | start = "" 877 | startopen = False 878 | end = "" 879 | endopen = False 880 | pid = "" 881 | r = "" 882 | g = "" 883 | b = "" 884 | 885 | for attr in node_xml.attrib: 886 | attr = attr.lower() 887 | if attr == "id": 888 | id = node_xml.attrib[attr] 889 | if attr == "label": 890 | label = node_xml.attrib[attr] 891 | if attr == "start": 892 | start = node_xml.attrib[attr] 893 | if attr == "end": 894 | start = node_xml.attrib[attr] 895 | if attr == "startopen": 896 | start = attvalue_xml.attrib[attr] 897 | startopen = True 898 | if attr == "endopen": 899 | end = attvalue_xml.attrib[attr] 900 | endopen = True 901 | if attr == "pid": 902 | pid = node_xml.attrib[attr] 903 | 904 | attvalues_xmls = [] 905 | spells = [] 906 | 907 | for child in node_xml: 908 | tag = self.ns_clean(child.tag).lower() 909 | if tag == "attvalues": 910 | attvalues_xmls.append(child) 911 | if tag == "viz:color": 912 | r = child.attrib["r"] 913 | g = child.attrib["g"] 914 | b = child.attrib["b"] 915 | if tag == "spells": 916 | spells = [spell.attrib for spell in child] 917 | 918 | self.node_obj = self.graph_obj.addNode(id=id, label=label, start=start, end=end, startopen=startopen, endopen=endopen, pid=pid, r=r, g=g, b=b, spells=spells) 919 | 920 | for attvalues_xml in attvalues_xmls: 921 | self.extract_node_attvalues(attvalues_xml) 922 | 923 | def extract_node_attvalues(self, attvalues_xml): 924 | for child in attvalues_xml: 925 | tag = self.ns_clean(child.tag).lower() 926 | if tag == "attvalue": 927 | attvalue_xml = child 928 | self.extract_node_attvalue(attvalue_xml) 929 | 930 | def extract_node_attvalue(self, attvalue_xml): 931 | id = "" 932 | value = "" 933 | start = "" 934 | startopen = False 935 | end = "" 936 | endopen = False 937 | for attr in attvalue_xml.attrib: 938 | attr = attr.lower() 939 | if attr == "for": 940 | id = attvalue_xml.attrib[attr] 941 | if attr == "value": 942 | value = attvalue_xml.attrib[attr] 943 | if attr == "start": 944 | start = attvalue_xml.attrib[attr] 945 | if attr == "end": 946 | end = attvalue_xml.attrib[attr] 947 | if attr == "startopen": 948 | start = attvalue_xml.attrib[attr] 949 | startopen = True 950 | if attr == "endopen": 951 | end = attvalue_xml.attrib[attr] 952 | endopen = True 953 | self.node_obj.addAttribute(id=id, value=value, start=start, end=end, startopen=startopen, endopen=endopen) 954 | 955 | def extract_edges(self, edges_xml): 956 | for child in edges_xml: 957 | tag = self.ns_clean(child.tag).lower() 958 | if tag == "edge": 959 | edge_xml = child 960 | self.extract_edge(edge_xml) 961 | 962 | def extract_edge(self, edge_xml): 963 | id = "" 964 | source = "" 965 | target = "" 966 | weight = "" 967 | start = "" 968 | startopen = False 969 | end = "" 970 | endopen = False 971 | label = "" 972 | r = "" 973 | g = "" 974 | b = "" 975 | 976 | for attr in edge_xml.attrib: 977 | attr = attr.lower() 978 | if attr == "id": 979 | id = edge_xml.attrib[attr] 980 | if attr == "source": 981 | source = edge_xml.attrib[attr] 982 | if attr == "target": 983 | target = edge_xml.attrib[attr] 984 | if attr == "weight": 985 | weight = edge_xml.attrib[attr] 986 | if attr == "start": 987 | start = edge_xml.attrib[attr] 988 | if attr == "end": 989 | end = edge_xml.attrib[attr] 990 | if attr == "startopen": 991 | start = edge_xml.attrib[attr] 992 | startopen = True 993 | if attr == "endopen": 994 | end = edge_xml.attrib[attr] 995 | endopen = True 996 | if attr == "label": 997 | label = edge_xml.attrib[attr] 998 | 999 | spells = [] 1000 | attvalues_xml = [] 1001 | for child in edge_xml: 1002 | tag = self.ns_clean(child.tag).lower() 1003 | if tag == "attvalues": 1004 | attvalues_xml = child 1005 | if tag == "spells": 1006 | spells = [spell.attrib for spell in child] 1007 | if tag == "viz:color": 1008 | r = child.attrib["r"] 1009 | g = child.attrib["g"] 1010 | b = child.attrib["b"] 1011 | 1012 | self.edge_obj = self.graph_obj.addEdge(id=id, source=source, target=target, weight=weight, start=start, end=end, startopen=startopen, endopen=endopen, label=label, r=r, g=g, b=b, spells=spells) 1013 | self.extract_edge_attvalues(attvalues_xml) 1014 | 1015 | def extract_edge_attvalues(self, attvalues_xml): 1016 | for child in attvalues_xml: 1017 | tag = self.ns_clean(child.tag).lower() 1018 | if tag == "attvalue": 1019 | attvalue_xml = child 1020 | self.extract_edge_attvalue(attvalue_xml) 1021 | # def addAttribute(self,id,value,start="",end="") : 1022 | 1023 | def extract_edge_attvalue(self, attvalue_xml): 1024 | id = "" 1025 | value = "" 1026 | start = "" 1027 | startopen = True 1028 | end = "" 1029 | endopen = True 1030 | for attr in attvalue_xml.attrib: 1031 | if attr == "for": 1032 | id = attvalue_xml.attrib[attr] 1033 | if attr == "value": 1034 | value = attvalue_xml.attrib[attr] 1035 | if attr == "start": 1036 | start = attvalue_xml.attrib[attr] 1037 | if attr == "end": 1038 | end = attvalue_xml.attrib[attr] 1039 | if attr == "startopen": 1040 | startopen = attvalue_xml.attrib[attr] 1041 | if attr == "endopen": 1042 | endopen = attvalue_xml.attrib[attr] 1043 | 1044 | self.edge_obj.addAttribute(id=id, value=value, start=start, end=end, startopen=startopen, endopen=endopen) 1045 | 1046 | def gexf(self): 1047 | return self.gexf_obj 1048 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | setup(name='pygexf', 3 | version='0.2.2', 4 | packages=['gexf'], 5 | url='http://github.com/paulgirard/pygexf', 6 | author='Paul Girard', 7 | author_email='paul.girard@sciences-po.fr' 8 | ) 9 | -------------------------------------------------------------------------------- /test/gexf.net.dynamics_openintervals.gexf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Gexf.net 5 | A Web network changing over time 6 | 7 | 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys,pprint 4 | 5 | sys.path.append('../gexf') 6 | from _gexf import Gexf, GexfImport 7 | 8 | # test helloworld.gexf 9 | gexf = Gexf("Paul Girard","A hello world! file") 10 | graph=gexf.addGraph("directed","static","a hello world graph") 11 | 12 | graph.addNode("0","hello") 13 | graph.addNode("1","World") 14 | graph.addEdge("0","0","1") 15 | 16 | output_file=open("helloworld.gexf","w") 17 | gexf.write(output_file) 18 | 19 | # test GexfImport 20 | f = open("gexf.net.dynamics_openintervals.gexf") 21 | gexf_import = Gexf.importXML(f) 22 | f.close() 23 | f = open("gexf.net.dynamics_openintervals.gexf") 24 | gexf_import2 = Gexf.importXML(f) 25 | f.close() 26 | print "test gexf comparision "+str(gexf_import==gexf_import2) 27 | 28 | 29 | 30 | graph=gexf_import.graphs[0] 31 | # display nodes list 32 | for node_id,node in graph.nodes.iteritems() : 33 | print node.label 34 | pprint.pprint(node.getAttributes(),indent=1,width=1) 35 | 36 | # display edges list 37 | for edgeid,edge in graph.edges.iteritems() : 38 | print str(graph.nodes[edge.source])+" -> "+str(graph.nodes[edge.target]) 39 | pprint.pprint(edge.getAttributes(),indent=1,width=1) 40 | 41 | 42 | o = open("gexf.net.dynamics_openintervals_copied.gexf", "w") 43 | 44 | gexf_import.write(o) 45 | -------------------------------------------------------------------------------- /test/test_pygexf.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paulgirard/pygexf/4ec08e9b8c381e030c30f86a562712b681d97e55/test/test_pygexf.py --------------------------------------------------------------------------------