Skip to content

Commit

Permalink
Add filters for regex_findall, regex_match, regex_search, regex_split…
Browse files Browse the repository at this point in the history
…, regex_sub
  • Loading branch information
itdependsnetworks committed Jul 22, 2023
1 parent 1f0e72e commit 5b621f6
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 62 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Hostname
# Regex

::: netutils.hostname
::: netutils.regex
options:
show_submodules: True
6 changes: 5 additions & 1 deletion docs/user/include_jinja_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
| paloalto_panos_brace_to_set | netutils.config.conversion.paloalto_panos_brace_to_set |
| fqdn_to_ip | netutils.dns.fqdn_to_ip |
| is_fqdn_resolvable | netutils.dns.is_fqdn_resolvable |
| hostname_regex | netutils.hostname.hostname_regex |
| abbreviated_interface_name | netutils.interface.abbreviated_interface_name |
| abbreviated_interface_name_list | netutils.interface.abbreviated_interface_name_list |
| canonical_interface_name | netutils.interface.canonical_interface_name |
Expand Down Expand Up @@ -70,6 +69,11 @@
| encrypt_type7 | netutils.password.encrypt_type7 |
| get_hash_salt | netutils.password.get_hash_salt |
| tcp_ping | netutils.ping.tcp_ping |
| regex_findall | netutils.regex.regex_findall |
| regex_match | netutils.regex.regex_match |
| regex_search | netutils.regex.regex_search |
| regex_split | netutils.regex.regex_split |
| regex_sub | netutils.regex.regex_sub |
| longest_prefix_match | netutils.route.longest_prefix_match |
| uptime_seconds_to_string | netutils.time.uptime_seconds_to_string |
| uptime_string_to_seconds | netutils.time.uptime_string_to_seconds |
Expand Down
72 changes: 72 additions & 0 deletions docs/user/lib_use_cases_jinja_filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,78 @@ When you run `jinja2_environment.py` the output will be:
The version of 192.168.0.1/24 is IPv4.
```

## regex Convenience Functions

When adding the netutils functions to your jinja2 environment, you also gain access to the built-in `re` python library using these Jinja2 filters.

```python
"regex_findall": "regex.regex_findall",
"regex_match": "regex.regex_match",
"regex_search": "regex.regex_search",
"regex_split": "regex.regex_split",
"regex_sub": "regex.regex_sub",
```

These functions will always return a json serializable object and not a complex object like `re.Match` or simialr to better serve the primary use case of functions to be used as Jinja2 filters. After all, they are simply small wrappers around Python `re` functions, which should be preferred when not using Jinja2 or similar templating language.

Below is a code in you can drop into your Python shell to help bring to life how these regex functions can be used.

```python
from jinja2 import Environment, BaseLoader
from netutils.utils import jinja2_convenience_function

env = Environment(loader=BaseLoader())
env.filters.update(jinja2_convenience_function())

DATA = {
"device": "USSCAMS07",
"comma_seperated_devices": "NYC-RT01,NYC-RT02,SFO-SW01,SFO-RT01"
}

TEMPLATE_STRING = """
{% set device_details = '([A-Z]{2})([A-Z]{2})([A-Z]{3})(\d*)' | regex_match(device) %}
Country: {{ ('^([A-Z]{2})([A-Z]{2})([A-Z]{3})(\d*)' | regex_search(device))[0] }}
STATE: {{ device_details[1] }}
FUNCTION: {{ device_details[2] }}
ALL DEVICES:
{% for router in ',' | regex_split(comma_seperated_devices) -%}
- {{ router }}
{% endfor %}
ONLY ROUTERS:
{% for router in ',' | regex_split(comma_seperated_devices) -%}
{% if '-RT' | regex_search(router) -%}
- {{ router }}
{% endif -%}
{% endfor %}
"""

template = env.from_string(TEMPLATE_STRING, DATA)
result = template.render()
print(result)
```

Which would result in the following output.

```text
Country: US
STATE: SC
FUNCTION: AMS
ALL DEVICES:
- NYC-RT01
- NYC-RT02
- SFO-SW01
- SFO-RT01
ONLY ROUTERS:
- NYC-RT01
- NYC-RT02
- SFO-RT01
```

## Netutils to Jinja2 Filters List


Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ nav:
- Password: "dev/code_reference/password.md"
- Ping: "dev/code_reference/ping.md"
- Protocol Mapper: "dev/code_reference/protocol_mapper.md"
- Regex: "dev/code_reference/regex.md"
- Route: "dev/code_reference/route.md"
- Time: "dev/code_reference/time.md"
- Utils: "dev/code_reference/utils.md"
Expand Down
35 changes: 0 additions & 35 deletions netutils/hostname.py

This file was deleted.

158 changes: 158 additions & 0 deletions netutils/regex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Utilities to expose regex functions, primarily for Jinja filters."""

import re
import typing as t


def _match_object(match: t.Optional[t.Match[str]]) -> t.Union[t.List[str], str, None]:
"""Helper method to better 'serialize' a re.Match object."""
if not match:
return None
if match.groups():
results = []
for group in match.groups():
results.append(group)
return results
return str(match.group())


def regex_findall(pattern: str, string: str) -> t.List[str]:
r"""Given a regex pattern and string, return all non-overlapping matches of pattern in string, as a list of strings matches.
The main purpose of this function is provide a Jinja2 filter as this is simply a wrapper around `re.findall`.
Args:
pattern: Regex string to match against.
string: String to check against.
Returns:
List of matches, when there is no match the list will be empty.
Examples:
>>> from netutils.regex import regex_findall
>>> match = regex_findall("\w\w\w-RT\d\d", "NYC-RT01,NYC-RT02,SFO-SW01,SFO-RT01")
>>> len(match)
3
>>> match[0]
'NYC-RT01'
"""
return re.findall(pattern, string)


def regex_match(pattern: str, string: str) -> t.Union[t.List[str], str, None]:
r"""Given a regex pattern and string, return `None` if there is no matching `re.Match.groups()` if using capture groups or regex match via `re.Match.group()` on start of string.
This is useful in the following use cases:
1. Truthy conditional check that a string matches a given regex.
2. Returning regex capture groups from the string.
3. Matching for the start of a string, see `regex_search` when you do not want only start of string matching.
The main purpose of this function is provide a Jinja2 filter as this is simply a wrapper around `re.match`.
Args:
pattern: Regex string to match against.
string: String to check against.
Returns:
List of matches, match, or None no match found
Examples:
>>> from netutils.regex import regex_match
>>> print("South Carolina" if regex_match(".+SC.+\d\d", "USSCAMS07") else "Not South Carolina")
South Carolina
>>>
>>> match = regex_match("([A-Z]{2})([A-Z]{2})([A-Z]{3})(\d*)", "USSCAMS07")
>>> match[0]
'US'
>>> match[1]
'SC'
>>> match[2]
'AMS'
>>> match[3]
'07'
"""
return _match_object(re.match(pattern, string))


def regex_search(pattern: str, string: str) -> t.Union[t.List[str], str, None]:
r"""Given a regex pattern and string, return `None` if there is no matching `re.Match.groups()` if using capture groups or regex match via `re.Match.group()`.
The main purpose of this function is provide a Jinja2 filter as this is simply a wrapper around `re.search`.
Args:
pattern: Regex string to match against.
string: String to check against.
Returns:
List of matches, match, or None no match found.
Examples:
>>> from netutils.regex import regex_search
>>> print("South Carolina" if regex_search(".+SC.+\d\d", "USSCAMS07") else "Not South Carolina")
South Carolina
>>>
>>> match = regex_search("^([A-Z]{2})([A-Z]{2})([A-Z]{3})(\d*)", "USSCAMS07")
>>> match[0]
'US'
>>> match[1]
'SC'
>>> match[2]
'AMS'
>>> match[3]
'07'
"""
return _match_object(re.search(pattern, string))


def regex_split(pattern: str, string: str, maxsplit: int = 0) -> t.List[str]:
"""Given a regex pattern and string, return the split the object based on the patern a single element or single element of original value if there is no match.
The main purpose of this function is provide a Jinja2 filter as this is simply a wrapper around `re.split`.
Args:
pattern: Regex string to match against.
string: String to check against.
maxsplit: The maximum time to split.
Returns:
List of string of the match or single element list of original value if no match
Examples:
>>> from netutils.regex import regex_split
>>> match = regex_split(",", "NYC-RT01,NYC-RT02,SFO-SW01,SFO-RT01")
>>> match[0]
'NYC-RT01'
>>> match[3]
'SFO-RT01'
"""
return re.split(pattern, string, maxsplit)


def regex_sub(pattern: str, repl: str, string: str, count: int = 0) -> str:
"""Given a regex pattern, replacement, and string replace the pattern within the string and return.
The main purpose of this function is provide a Jinja2 filter as this is simply a wrapper around `re.sub`.
Args:
pattern: Regex string to match against.
repl: Replacement characters that were matched in the pattern.
string: String to check against.
count: The maximum time to replace.
Returns:
List of string of the match or single element list of original value if no match
Examples:
>>> from netutils.regex import regex_sub
>>> match = regex_sub(",", " ", "NYC-RT01,NYC-RT02,SFO-SW01,SFO-RT01")
>>> match
'NYC-RT01 NYC-RT02 SFO-SW01 SFO-RT01'
>>> match = regex_sub("(ROUTER|RTR)", "RT", "NYC-ROUTER01,NYC-ROUTER02,NYC-RTR03")
>>> match
'NYC-RT01,NYC-RT02,NYC-RT03'
"""
return re.sub(pattern, repl, string, count)
6 changes: 5 additions & 1 deletion netutils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
"section_config": "config.compliance.section_config",
"fqdn_to_ip": "dns.fqdn_to_ip",
"is_fqdn_resolvable": "dns.is_fqdn_resolvable",
"hostname_regex": "hostname.hostname_regex",
"interface_range_expansion": "interface.interface_range_expansion",
"interface_range_compress": "interface.interface_range_compress",
"split_interface": "interface.split_interface",
Expand Down Expand Up @@ -69,6 +68,11 @@
"encrypt_juniper_type9": "password.encrypt_juniper_type9",
"get_hash_salt": "password.get_hash_salt",
"tcp_ping": "ping.tcp_ping",
"regex_findall": "regex.regex_findall",
"regex_match": "regex.regex_match",
"regex_search": "regex.regex_search",
"regex_split": "regex.regex_split",
"regex_sub": "regex.regex_sub",
"longest_prefix_match": "route.longest_prefix_match",
"vlanlist_to_config": "vlan.vlanlist_to_config",
"vlanconfig_to_list": "vlan.vlanconfig_to_list",
Expand Down
23 changes: 0 additions & 23 deletions tests/unit/test_hostname.py

This file was deleted.

Loading

0 comments on commit 5b621f6

Please sign in to comment.