Obsidian tools - a Python package for analysing an Obsidian.md vault

Overview

PyPI version PyPI version Licence Documentation codecov

obsidiantools 🪨 ⚒️

obsidiantools is a Python package for getting structured metadata about your Obsidian.md notes and analysing your vault. Complement your Obsidian workflows by getting metrics and detail about all your notes in one place through the widely-used Python data stack.

It's incredibly easy to explore structured data on your vault through this fluent interface. This is all the code you need to generate a vault object that stores the key data:

import obsidiantools.api as otools

vault = otools.Vault(<VAULT_DIRECTORY>).connect()

See some of the key features below - all accessible from the vault object either through a method or an attribute.

As this package relies upon note (file)names, it is only recommended for use on vaults where wikilinks are not formatted as paths and where note names are unique. This should cover the vast majority of vaults that people create.

💡 Key features

This is how obsidiantools can complement your workflows for note-taking:

  • Access a networkx graph of your vault (vault.graph)
    • NetworkX is the main Python library for network analysis, enabling sophisticated analyses of your vault.
    • NetworkX also supports the ability to export your graph to other data formats.
  • Get summary stats about your notes, e.g. number of backlinks and wikilinks, in a Pandas dataframe
    • Get the dataframe via vault.get_note_metadata()
  • Retrieve detail about your notes' links as built-in Python types
    • The various types of links:
      • Wikilinks (incl. header links, links with alt text)
      • Backlinks
    • You can access all the links in one place, or you can load them for an individual note:
      • e.g. vault.backlinks_index for all backlinks in the vault
      • e.g. vault.get_backlinks( ) for the backlinks of an individual note
    • Check which notes are isolated (vault.isolated_notes)
    • Check which notes do not exist as files yet (vault.nonexistent_notes)

Check out the functionality in the demo repo. Launch the '10 minutes' demo in a virtual machine via Binder:

Documentation Binder

There are other API features that try to mirror the Obsidian.md app, for your convenience when working with Python, but they are no substitute for the interactivity of the app!

The text from vault notes goes through this process: markdown → HTML → ASCII plaintext. The functions for text processing are in the md_utils module so they can be used to get text, e.g. for use in NLP analysis.

⏲️ Installation

pip install obsidiantools

Developed for Python 3.9 but may still work on lower versions.

As of Sep 2021, NetworkX requires Python 3.7 or higher (similar for Pandas too) so that is recommended as a minimum.

🖇️ Dependencies

  • markdown
  • html2text
  • pandas
  • numpy
  • networkx

🏗️ Tests

A small 'dummy vault' vault of lipsum notes is in tests/vault-stub (generated with help of the lorem-markdownum tool). Sense-checking on the API functionality was also done on a personal vault of up to 100 notes.

I am not sure how the parsing will work outside of Latin languages - if you have ideas on how that can be supported feel free to suggest a feature or pull request.

⚖️ Licence

Modified BSD (3-clause)

Comments
  • [FR] Options : choose to use file name / frontmatter title for graph

    [FR] Options : choose to use file name / frontmatter title for graph

    I noticed that the graph created use the filepath, and I want to choose the frontmatter title or the filename instead. How can I do that ?

    Graphic reference : image Generated using pyvis

    enhancement make recipe 
    opened by Lisandra-dev 20
  • `TypeError: 'NoneType' object is not iterable` (in `_remove_front_matter`)

    `TypeError: 'NoneType' object is not iterable` (in `_remove_front_matter`)

    Running the following on my vault:

    import obsidiantools.api as ot
    vault = ot.Vault(Path("/path/to/a/vault").connect()
    

    Results in:

    # --->8--- Irrelevant frames omitted --->8---
    
    ~/.cache/pypoetry/virtualenvs/knowledgebase-scripts-Fe_uWe_V-py3.9/lib/python3.9/site-packages/obsidiantools/md_utils.py in _get_ascii_plaintext_from_md_file(filepath)
        190     html = _get_html_from_md_file(filepath)
        191     # strip out front matter (if any):
    --> 192     html = _remove_front_matter(html)
        193     return _get_ascii_plaintext_from_html(html)
        194 
    
    ~/.cache/pypoetry/virtualenvs/knowledgebase-scripts-Fe_uWe_V-py3.9/lib/python3.9/site-packages/obsidiantools/md_utils.py in _remove_front_matter(html)
        201     if hr_content:
        202         # wipe out content from first hr (the front matter)
    --> 203         for fm_detail in hr_content.find_next("p"):
        204             fm_detail.extract()
        205         # then wipe all hr elements
    
    TypeError: 'NoneType' object is not iterable
    

    A quick roundtrip in a debugger shows this happens with at least:

    1. Notes containing an hr (---) but no YAML frontmatter.
    2. Notes containing only frontmatter, no body.
    bug 
    opened by zoni 10
  • performance - file opens & reads

    performance - file opens & reads

    Hi.

    Every markdown file is being opened & read a total of 8 times in normal connect & gather flow. Might make sense to model a note as a class and have it load its own data once.

    enhancement 
    opened by stepsal 6
  • Unable to filter index using Windows filepath with include_subdirs=[]

    Unable to filter index using Windows filepath with include_subdirs=[]

    Hi,

    I can successfully view my vault file index in Windows. If I then try to filter the list by subdirectory I can successfully list notes in the root and in the 'docs' folders. If I filter by the name of a lower subdirectory using a Windows filepath the returned list is empty.

    For example, my file index includes the following list items:

    {'README': WindowsPath('README.md'),
     'index': WindowsPath('docs/index.md'),
     'Quotations': WindowsPath('docs/Quotations.md'),
     'Creative Commons': WindowsPath('docs/Concepts/Creative Commons.md'),
     'Crowdsourcing': WindowsPath('docs/Concepts/Crowdsourcing.md'),
     'Data Format': WindowsPath('docs/Concepts/Data Format.md'),
     'Data Model': WindowsPath('docs/Concepts/Data Model.md'),
     'Data Sovereignty': WindowsPath('docs/Concepts/Data Sovereignty.md'),
    }
    

    Based on the obsidiantools-demo I would expect to be able to list all the markdown files in the 'Concepts' folder using the following call:

    (otools.Vault(vault_dir, include_subdirs=['docs/Concepts'], include_root=False) .file_index)

    Instead the returned object is empty {}.

    Reversing the slash to create a linux path resolves the issue:

    (otools.Vault(vault_dir, include_subdirs=['docs\Concepts'], include_root=False)
     .file_index)
    

    Returns:

    {'Creative Commons': WindowsPath('docs/Concepts/Creative Commons.md'),
     'Crowdsourcing': WindowsPath('docs/Concepts/Crowdsourcing.md'),
     'Data Format': WindowsPath('docs/Concepts/Data Format.md'),
     'Data Model': WindowsPath('docs/Concepts/Data Model.md'),
     'Data Sovereignty': WindowsPath('docs/Concepts/Data Sovereignty.md')}
    

    Ideally this would be resolved by the obsidiantools package rather than the user. Alternatively suggest updating the documentation.

    bug 
    opened by virtualarchitectures 6
  • UnicodeDecodeError when connecting to Obsidian Vault

    UnicodeDecodeError when connecting to Obsidian Vault

    Hi, I'm testing out the package and I'm getting an error when I try to connect via Jupyter notebook in Windows 10: vault = otools.Vault(vault_dir).connect().gather().

    I'm receiving the following UnicodeDecodeError: UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 1400: character maps to <undefined>.

    Assuming the filepath and connection are working I'm unclear whether it is a problem I can correct in Obsidian or if it is a problem with the parser used by obsidiantools. Can you advise how I can resolve the issue?

    image

    For reference the stack trace is as follows:

    ---------------------------------------------------------------------------
    UnicodeDecodeError                        Traceback (most recent call last)
    ~\AppData\Local\Temp/ipykernel_19592/2568229718.py in <module>
    ----> 1 vault = otools.Vault(vault_dir).connect().gather()
          2 print(f"Connected?: {vault.is_connected}")
          3 print(f"Gathered?:  {vault.is_gathered}")
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\api.py in connect(self)
        199         if not self._is_connected:
        200             # default graph to mirror Obsidian's link counts
    --> 201             wiki_link_map = self._get_wikilinks_index()
        202             G = nx.MultiDiGraph(wiki_link_map)
        203             self._graph = G
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\api.py in _get_wikilinks_index(self)
        438         where k is the md filename
        439         and v is list of ALL wikilinks found in k"""
    --> 440         return {k: get_wikilinks(self._dirpath / v)
        441                 for k, v in self._file_index.items()}
        442 
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\api.py in <dictcomp>(.0)
        438         where k is the md filename
        439         and v is list of ALL wikilinks found in k"""
    --> 440         return {k: get_wikilinks(self._dirpath / v)
        441                 for k, v in self._file_index.items()}
        442 
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\md_utils.py in get_wikilinks(filepath)
         92         list of strings
         93     """
    ---> 94     plaintext = _get_ascii_plaintext_from_md_file(filepath, remove_code=True)
         95 
         96     wikilinks = _get_all_wikilinks_from_html_content(
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\md_utils.py in _get_ascii_plaintext_from_md_file(filepath, remove_code)
        265     """md file -> html -> ASCII plaintext"""
        266     # strip out front matter (if any):
    --> 267     html = _get_html_from_md_file(filepath)
        268     if remove_code:
        269         html = _remove_code(html)
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\md_utils.py in _get_html_from_md_file(filepath)
        251 def _get_html_from_md_file(filepath):
        252     """md file -> html (without front matter)"""
    --> 253     _, content = _get_md_front_matter_and_content(filepath)
        254     return markdown.markdown(content, output_format='html')
        255 
    
    ~\anaconda3\envs\Obsidian_Tools\lib\site-packages\obsidiantools\md_utils.py in _get_md_front_matter_and_content(filepath)
        242     with open(filepath) as f:
        243         try:
    --> 244             front_matter, content = frontmatter.parse(f.read())
        245         except yaml.scanner.ScannerError:
        246             # for invalid YAML, return the whole file as content:
    
    ~\anaconda3\envs\Obsidian_Tools\lib\encodings\cp1252.py in decode(self, input, final)
         21 class IncrementalDecoder(codecs.IncrementalDecoder):
         22     def decode(self, input, final=False):
    ---> 23         return codecs.charmap_decode(input,self.errors,decoding_table)[0]
         24 
         25 class StreamWriter(Codec,codecs.StreamWriter):
    
    UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 1400: character maps to <undefined>
    
    bug 
    opened by virtualarchitectures 5
  • Reference to non-md files

    Reference to non-md files

    It seems that obsidiantools does not track non-markdown files (pictures, etc). As a consequence, vault.nonexistent_notes list all references to such files. I suggest to include non-markdown files in the graph as well.

    And, on a related note, vault.nonexistent_notes wrongly includes notes that are referenced with extension. For example, a reference of the form [[note.md]] leads to note.md being listed as non-existent even if the file note.md exists.

    enhancement 
    opened by martinlackner 4
  • error on malformed frontmatter

    error on malformed frontmatter

    in case of a malformed frontmatter in a document an exception is raised and not handled.

    • error can be handled in https://github.com/mfarragher/obsidiantools/blob/ddd78669ef27346fdb4b13cdc956a6f8c00e98f4/obsidiantools/md_utils.py#L259
    • adding the following solves the problem (allthough the specific error should be named)
    except:
        print("problem with file ", filepath)
    
    bug 
    opened by Dorianux 4
  • get_md_relpaths_from_dir() globbing issue

    get_md_relpaths_from_dir() globbing issue

    Hello. Thanks for this library

    I have tried this with 3.9 as suggested. But I'm getting an error straight away on gather.

    File "/home/steve/.pyenv/versions/obsidian-python3.9.0/lib/python3.9/site-packages/obsidiantools/md_utils.py", line 29, in get_md_relpaths_from_dir
        for p in glob(str(dir_path / '**/*.md'), recursive=True)]
    TypeError: unsupported operand type(s) for /: 'str' and 'str'
    

    I have to change the /' to a + in the line in get_md_relpaths_from_dir() to get the globbing working!

    change from

    return [Path(p).relative_to(dir_path)
    for p in glob(str(dir_path / '**/*.md'), recursive=True)]
    

    to

    return [Path(p).relative_to(dir_path)
    for p in glob(str(dir_path + '**/*.md'), recursive=True)]
    
    bug 
    opened by stepsal 4
  • Tags in code blocks are taken

    Tags in code blocks are taken

    As a placeholder. I think code blocks should be ignored for tags? What do you think?

    image
     "file_tags": [
            "meta",
            "idea",
            "shower-thought",
            "to-digest",
            "shower-thought",
            "introduction",
            "shower-thought\"",
            "guru\"",
            "shroedinger-uncertain\"",
            "floating-point-error\"",
            "socratic\""
          ]
    
    bug 
    opened by louis030195 3
  • Handle .md inside wikilinks to reflect Obsidian graph

    Handle .md inside wikilinks to reflect Obsidian graph

    [[Foo]] and [[Bar.md]] will both be related to note 'Foo' in the knowledge graph.

    Currently, wikilinks getters will extract the wikilinks as 'Foo' and 'Bar.md'. The expected behaviour of getters to reflect Obsidian's behaviour is 'Foo' and 'Bar' respectively.

    bug 
    opened by mfarragher 2
  • Text goes missing even though the HTML is OK (html2text parsing issues)

    Text goes missing even though the HTML is OK (html2text parsing issues)

    For one of my notes with a mix of tables, LaTeX, lists & code blocks, there is a lot of text from the note that isn't captured in source_text_index, but is kept in the HTML. This suggests some parsing issues with how html2text is configured.

    Whole paragraph blocks & headers can be completely missing.

    This starts to happen after a table with LaTeX. Anything in body text (<p>) afterwards is missing, yet it keeps all the remaining LaTeX (even the stuff in tables).

    Perhaps it doesn't like MathJax? Maybe wiping out a few tags from HTML, for the source_text functionality, before it gets processed by html2text could make the output smoother in this case.

    Need to think more about:

    • What HTML tags are not necessary for source_text?
      • LaTeX is one aspect to remove if causing problems. Keep as much as possible for html2text to handle (including URLs, images, etc.). Anything more opinionated (e.g. do we want strikethrough text or not) would be better covered in readable_text.
      • May involve another Markdown class if switching off markdown extensions, more functions to do this specific HTML generation, etc.
    • A test case from reduced format of my note
    bug 
    opened by mfarragher 2
  • Incremental refresh

    Incremental refresh

    Hey, for https://github.com/louis030195/obsidian-ava, I'm trying to implement increment refresh of the state of the vault.

    Concretely, I build sentence embeddings of the whole vault and would like to re-compute embeddings every time a note is updated/deleted/created.

    Do you see any way of doing this incrementally rather than reloading the vault and recomputing everything every time? (It takes ~1 min on mps device on my 500k words vault)

    Ideally, I'd see maybe an API that let me listen to vault changes with callback(s) in this library?

    Thanks 🚀😃

    enhancement make recipe 
    opened by louis030195 3
Releases(0.10.0)
  • 0.10.0(Jan 8, 2023)

    New features:

    • connect method has attachments argument to give the option to include 'attachment' files like media files and canvas files in the graph. The behaviour from v0.9 is kept in this new release (via the default attachments=False).
    • Information about media files & their filepaths is stored in the media_file_index attribute.
    • New methods for metadata: get_canvas_file_metadata and get_media_file_metadata. The get_all_file_metadata method is new method that is best-placed to get all the metadata for files & notes in a vault.
    • isolated_media_files and isolated_canvas_files attributes.
    • nonexistent_media_files and nonexistent_media_files attributes.

    Important API changes vs previous version:

    • file_index attribute is now md_file_index, to avoid ambiguity from the extra support now for media files and canvas files.

    Other improvements:

    • Speed improvements for the gather() method and processing of HTML content.
    • Tweaks to the code to address deprecation warnings from other packages.
    Source code(tar.gz)
    Source code(zip)
  • 0.9.0(Dec 24, 2022)

    New features:

    • Support for canvas files (the latest addition to Obsidian). See the Canvas files features notebook for detail on functionality.
    • Nested tags are now supported. By default, Vault will not account for nested tags (as has previously been the case).
    • Column for n_tags in get_note_metadata method.
    • get_wikilink_counts method added.

    Other improvements:

    • Vault can handle duplicate filenames, better reflecting the 'Shortest path when possible' wikilink setting in Obsidian.
    • More robust regex for tags.
    • Fix bug where "tags" were being parsed from code block.
    • More error handling for front matter.
    • Fix bug where source_text gets cut off after a LaTeX block.
    • Wikilinks are robust to the use of file extension md in them.

    Package now requires Python 3.9 as a minimum.

    Wiki has now been added to the Github repo, to cover detail for advanced users.

    Source code(tar.gz)
    Source code(zip)
  • 0.8.1(Aug 7, 2022)

    Bug fixes:

    • Fixed issue where markdown could not parse at all on some environments. md_mermaid extension has been removed. This issue was hard to reproduce so I have removed the extension for now.
    Source code(tar.gz)
    Source code(zip)
  • 0.8.0(Aug 7, 2022)

    New features:

    • The API now has two forms of text: 'source text' and 'readable text'. These have their own object attributes and methods, e.g. get_source_text() and get_readable_text(). The old text attributes and objects have been removed, but they were closest to the source text functionality. The readable text is essentially has a lot of formatting removed, while still retaining the context within notes, so it is in a form that can be used quite easily for NLP analysis. The source text best reflects how notes are formatted in the Obsidian app's 'source mode'.
    • Applied multiple pymarkdown extensions to the logic, to reflect some of the main features of how Obsidian uses extended markdown. For example, mermaid diagrams, LaTeX equations and tables can now be parsed, tilde characters mark the deletion of text, etc.
    • LaTeX equations can now be accessed for notes via get_math method and math_index attribute.
    • More robust tag parsing.

    Bug fixes:

    • More robust paths for cross-platform support.
    • Front matter parsing is more robust.
    Source code(tar.gz)
    Source code(zip)
  • 0.7.0(Dec 27, 2021)

    New features:

    • Support for tags
    • Support for instantiating Vault on a filtered list of subdirectories
    • 'Gather' functionality for note text: gather function; get_note function and notes_index attr

    Fixes:

    • Fix embedded files output where the pipe operator is used (e.g. to scale images): avoid backslashes appearing in output
    • More robust processing of front matter
    Source code(tar.gz)
    Source code(zip)
  • 0.6.0(Oct 19, 2021)

  • 0.5.0(Sep 13, 2021)

Owner
Mark Farragher
🧬 I solve data problems in areas like healthcare, SaaS & economics
Mark Farragher
Install, run, and update apps without root and only in your home directory

Qube Apps Install, run, and update apps in the private storage of a Qube Building instrutions

Micah Lee 26 Dec 27, 2022
A fast Python implementation of Ac Auto Mechine

A fast Python implementation of Ac Auto Mechine

Jin Zitian 1 Dec 07, 2021
Control-Alt-Delete - Help Tux Escape Beastie's Jail!

Control-Alt-Delete Help Tux escape Beastie's jail by completing the following challenges! Challenges Challenge 00: Drinks: Tux needs to drink less. Ch

NDLUG 8 Oct 31, 2021
NFT-Generator is the best way to generate thousands of NFTs quick and easily with Python.

NFT-Generator is the best way to generate thousands of NFTs quick and easily with Python. Just add your files, set your configuration and run the scri

78 Dec 27, 2022
A small python tool to get relevant values from SRI invoices

SriInvoiceProcessing A small python tool to get relevant values from SRI invoices Some useful info to run the tool Login into your SRI account and ret

Wladymir Brborich 2 Jan 07, 2022
Pass arguments by reference—in Python!

byref Pass arguments by reference—in Python! byrefis a decorator that allows Python functions to declare reference parameters, with similar semantics

9 Feb 10, 2022
Python Classes Without Boilerplate

attrs is the Python package that will bring back the joy of writing classes by relieving you from the drudgery of implementing object protocols (aka d

The attrs Cabal 4.6k Jan 06, 2023
✨ Un bot Twitter totalement fait en Python par moi, et en français.

Twitter Bot ❗ Un bot Twitter totalement fait en Python par moi, et en français. Il faut remplacer auth = tweepy.OAuthHandler(consumer_key, consumer_se

MrGabin 3 Jun 06, 2021
A string to hashtags module

A string to hashtags module

Fayas Noushad 4 Dec 01, 2021
A python tool give n number of inputs and parallelly you will get a output by separetely

http-status-finder Hello Everyone!! This is kavisurya, In this tool you can give n number of inputs and parallelly you will get a output by separetely

KAVISURYA V 3 Dec 05, 2021
Teleport Ur Logs with Love

Whatever you pipe into tull, will get a unique UUID and the data gets stored locally - accessible via a flask server with simple endpoints. You can use ngrok or localtunnel then to share it outside L

Lokendra Sharma 11 Jul 30, 2021
ecowater-softner is a Python library for collecting information from Ecowater water softeners.

Ecowater Softner ecowater-softner is a Python library for collecting information from Ecowater water softeners. Installation Use the package manager p

6 Dec 08, 2022
Search, generate & deliver Msfvenom payloads in an quick and easy way

Goal Search, generate & deliver payloads in an quick and easy way Be as simple as possible BUT with all msfvenom payloads. Ever lost time searching th

2 Mar 03, 2022
✨ Un code pour voir les disponibilités des vaccins contre le covid totalement fait en Python par moi, et en français.

Vaccine Notifier ❗ Un chois aléatoire d'un article sur Wikipedia totalement fait en Python par moi, et en français. 🔮 Grâce a une requète API, on peu

MrGabin 3 Jun 06, 2021
BOLT12 Lightning Address Format

BOLT12 Address Support (DRAFT!) Inspired by the awesome lightningaddress.com, except for BOLT12: Supports BOLT12 Allows BOLT12 vendor string authentic

Rusty Russell 28 Sep 14, 2022
Fraud Multiplication Table Detection in python

Fraud-Multiplication-Table-Detection-in-python In this program, I have detected fraud multiplication table using python without class. Here, I have co

Sachin Vinayak Dabhade 4 Sep 24, 2021
Small Python script to parse endlessh's output and print some neat statistics

endlessh_parser endlessh_parser is a small Python script that parses endlessh's output and prints some neat statistics about it Usage Install all the

ManicRobot 1 Oct 18, 2021
A simple python script to generate an iCalendar file for the university classes.

iCal Generator This is a simple python script to generate an iCalendar file for the university classes. Installation Clone the repository git clone ht

Foad Rashidi 2 Sep 01, 2022
Writing Alfred copy/paste scripts in Python

Writing Alfred copy/paste scripts in Python This repository shows how to create Alfred scripts in Python. It assumes that you using pyenv for Python v

Will Fitzgerald 2 Oct 26, 2021
Utility to add/remove licenses to/from source files

Utility to add/remove licenses to/from source files. Supports processing any combination of globs, files, and directories (recurse). Pruning options allow skipping non-licensing files.

Eduardo Ponce Mojica 2 Jan 29, 2022