onvif-python
Advanced tools
| # onvif/services/events/pausable_subscription.py | ||
| from ...operator import ONVIFOperator | ||
| from ...utils import ONVIFWSDL, ONVIFService | ||
| class PausableSubscription(ONVIFService): | ||
| def __init__(self, xaddr=None, **kwargs): | ||
| # References: | ||
| # - PausableSubscriptionManagerBinding (ver10/events/wsdl/event-vs.wsdl) | ||
| # - Operations: https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl | ||
| definition = ONVIFWSDL.get_definition("pausable_subscription") | ||
| self.operator = ONVIFOperator( | ||
| definition["path"], | ||
| binding=f"{{{definition['namespace']}}}{definition['binding']}", | ||
| xaddr=xaddr, | ||
| **kwargs, | ||
| ) | ||
| def Renew(self, TerminationTime=None): | ||
| return self.operator.call("Renew", TerminationTime=TerminationTime) | ||
| def Unsubscribe(self): | ||
| return self.operator.call("Unsubscribe") | ||
| def PauseSubscription(self): | ||
| return self.operator.call("PauseSubscription") | ||
| def ResumeSubscription(self): | ||
| return self.operator.call("ResumeSubscription") |
| Metadata-Version: 2.4 | ||
| Name: onvif-python | ||
| Version: 0.2.3 | ||
| Version: 0.2.4 | ||
| Summary: A modern Python library for ONVIF-compliant devices | ||
@@ -47,10 +47,9 @@ Author-email: Nirsimetri Technologies® <open@nirsimetri.com> | ||
| <div align="center"> | ||
| [](https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) | ||
| [](https://deepwiki.com/nirsimetri/onvif-python) | ||
| [](https://pypi.org/project/onvif-python/) | ||
| [](https://clickpy.clickhouse.com/dashboard/onvif-python) | ||
| <img alt="Codacy grade" src="https://img.shields.io/codacy/grade/bff08a94e4d447b690cea49c6594826d?label=Code%20Quality&logo=codacy" href="https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"> | ||
| <img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/nirsimetri/onvif-python"> | ||
| <img alt="PyPI Version" src="https://img.shields.io/badge/PyPI-0.2.4-orange?logo=archive&color=yellow" href="https://pypi.org/project/onvif-python/"> | ||
| <img alt="Pepy Total Downloads" src="https://img.shields.io/pepy/dt/onvif-python?label=Downloads&color=red" href="https://pepy.tech/projects/onvif-python"> | ||
| <br> | ||
| [](https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml) | ||
| [](https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml) | ||
| <img alt="Build" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml/badge.svg?branch=main" href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml"> | ||
| <img alt="Upload Python Package" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml/badge.svg" href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml"> | ||
| </div> | ||
@@ -360,3 +359,3 @@ | ||
| ONVIF Terminal Client — v0.2.3 | ||
| ONVIF Terminal Client — v0.2.4 | ||
| https://github.com/nirsimetri/onvif-python | ||
@@ -434,3 +433,3 @@ | ||
| ```bash | ||
| ONVIF Interactive Shell — v0.2.3 | ||
| ONVIF Interactive Shell — v0.2.4 | ||
| https://github.com/nirsimetri/onvif-python | ||
@@ -1034,3 +1033,3 @@ | ||
| | Device Management | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [device.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/device/wsdl/devicemgmt.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ✅ Complete | | ||
| | Events | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [event.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ⚠️ Partial | | ||
| | Events | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [event.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ✅ Complete | | ||
| | Access Control | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/AccessControl.xml) | [accesscontrol.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/pacs/accesscontrol.wsdl) | [types.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/pacs/types.xsd) | ✅ Complete | | ||
@@ -1057,3 +1056,2 @@ | Access Rules | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/AccessRules.xml) | [accessrules.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/accessrules/wsdl/accessrules.wsdl) | - | ✅ Complete | | ||
| | Replay Control | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Replay.xml) | [replay.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/replay.wsdl) | - | ✅ Complete | | ||
| | Resource Query | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/ResourceQuery.xml) | - | | ❌ Any idea? | | ||
| | Schedule | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Schedule.xml) | [schedule.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schedule/wsdl/schedule.wsdl) | - | ✅ Complete | | ||
@@ -1063,3 +1061,2 @@ | Security | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Security.xml) | [advancedsecurity.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl) | - | ✅ Complete | | ||
| | Uplink | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Uplink.xml) | [uplink.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/uplink/wsdl/uplink.wsdl) | - | ✅ Complete | | ||
| | WebRTC | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/WebRTC.xml) | - | - | ❌ Any idea? | | ||
@@ -1178,15 +1175,6 @@ </details> | ||
| ## Alternatives | ||
| If you are looking for other ONVIF Python libraries, here are some alternatives: | ||
| - [python-onvif-zeep](https://github.com/FalkTannhaeuser/python-onvif-zeep): | ||
| A synchronous ONVIF client library for Python, using Zeep for SOAP communication. Focuses on compatibility and ease of use for standard ONVIF device operations. Good for scripts and applications where async is not required. | ||
| - [python-onvif-zeep-async](https://github.com/openvideolibs/python-onvif-zeep-async): | ||
| An asynchronous ONVIF client library for Python, based on Zeep and asyncio. Suitable for applications requiring non-blocking operations and concurrent device communication. Supports many ONVIF services and is actively maintained. | ||
| ## References | ||
| - [ONVIF Official Specifications](https://www.onvif.org/profiles/specifications/specification-history/) | ||
| - [ONVIF Official Specs Repository](https://github.com/onvif/specs) | ||
| - [ONVIF Application Programmer's Guide](https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_WG-APG-Application_Programmers_Guide-1.pdf) | ||
| - [ONVIF 2.0 Service Operation Index](https://www.onvif.org/onvif/ver20/util/operationIndex.html) | ||
@@ -1193,0 +1181,0 @@ - [Usage Examples](./examples/) |
@@ -44,2 +44,3 @@ LICENSE.md | ||
| onvif/services/events/notification.py | ||
| onvif/services/events/pausable_subscription.py | ||
| onvif/services/events/pullpoint.py | ||
@@ -131,3 +132,2 @@ onvif/services/events/subscription.py | ||
| tests/test_error_handlers.py | ||
| tests/test_exceptions.py | ||
| tests/test_wsdl_map.py | ||
| tests/test_exceptions.py |
@@ -197,3 +197,3 @@ # onvif/cli/interactive.py | ||
| "/ /_/ / /| / | |/ // // __/ ", | ||
| "\\____/_/ |_/ |___/___/_/ v0.2.3", | ||
| "\\____/_/ |_/ |___/___/_/ v0.2.4", | ||
| " ", | ||
@@ -1404,3 +1404,3 @@ ] | ||
| help_text = f""" | ||
| {colorize('ONVIF Interactive Shell — v0.2.3', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')} | ||
| {colorize('ONVIF Interactive Shell — v0.2.4', 'cyan')}\n{colorize('https://github.com/nirsimetri/onvif-python', 'white')} | ||
@@ -1407,0 +1407,0 @@ {colorize('Basic Commands:', 'yellow')} |
+286
-286
@@ -24,3 +24,3 @@ # onvif/cli/main.py | ||
| prog="onvif", | ||
| description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.3\nhttps://github.com/nirsimetri/onvif-python", | ||
| description=f"{colorize('ONVIF Terminal Client', 'yellow')} — v0.2.4\nhttps://github.com/nirsimetri/onvif-python", | ||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||
@@ -165,286 +165,2 @@ epilog=f""" | ||
| def search_products(search_term: str, page: int = 1, per_page: int = 20) -> None: | ||
| """Search ONVIF products database and display results in table format with pagination. | ||
| Args: | ||
| search_term: Search term to match against model, post_title, and company_name fields | ||
| page: Page number (1-based) | ||
| per_page: Number of results per page | ||
| """ | ||
| # Get the database path relative to the script location | ||
| current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| db_path = os.path.join(current_dir, "..", "db", "products.db") | ||
| db_path = os.path.normpath(db_path) | ||
| if not os.path.exists(db_path): | ||
| print(f"{colorize('Error:', 'red')} Products database not found at {db_path}") | ||
| sys.exit(1) | ||
| # Validate pagination parameters | ||
| if page < 1: | ||
| print(f"{colorize('Error:', 'red')} Page number must be 1 or greater") | ||
| sys.exit(1) | ||
| if per_page < 1 or per_page > 100: | ||
| print(f"{colorize('Error:', 'red')} Per-page must be between 1 and 100") | ||
| sys.exit(1) | ||
| try: | ||
| conn = sqlite3.connect(db_path) | ||
| cursor = conn.cursor() | ||
| # First, get total count for pagination info | ||
| count_query = """ | ||
| SELECT COUNT(*) | ||
| FROM onvif_products | ||
| WHERE LOWER(model) LIKE LOWER(?) | ||
| OR LOWER(post_title) LIKE LOWER(?) | ||
| OR LOWER(company_name) LIKE LOWER(?) | ||
| OR LOWER(product_category) LIKE LOWER(?) | ||
| """ | ||
| search_pattern = f"%{search_term}%" | ||
| cursor.execute( | ||
| count_query, | ||
| (search_pattern, search_pattern, search_pattern, search_pattern), | ||
| ) | ||
| total_count = cursor.fetchone()[0] | ||
| if total_count == 0: | ||
| print( | ||
| f"{colorize('No products found matching:', 'yellow')} {colorize(search_term, 'white')}" | ||
| ) | ||
| return | ||
| # Calculate pagination | ||
| total_pages = (total_count + per_page - 1) // per_page # Ceiling division | ||
| if page > total_pages: | ||
| print( | ||
| f"{colorize('Error:', 'red')} Page {page} does not exist. Total pages: {total_pages}" | ||
| ) | ||
| return | ||
| offset = (page - 1) * per_page | ||
| # Search query with pagination | ||
| query = """ | ||
| SELECT ID, test_date, post_title, product_firmware_version, | ||
| product_profiles, product_category, type, | ||
| company_name | ||
| FROM onvif_products | ||
| WHERE LOWER(model) LIKE LOWER(?) | ||
| OR LOWER(post_title) LIKE LOWER(?) | ||
| OR LOWER(company_name) LIKE LOWER(?) | ||
| OR LOWER(product_category) LIKE LOWER(?) | ||
| ORDER BY test_date DESC | ||
| LIMIT ? OFFSET ? | ||
| """ | ||
| cursor.execute( | ||
| query, | ||
| ( | ||
| search_pattern, | ||
| search_pattern, | ||
| search_pattern, | ||
| search_pattern, | ||
| per_page, | ||
| offset, | ||
| ), | ||
| ) | ||
| results = cursor.fetchall() | ||
| # Display results in table format | ||
| start_result = offset + 1 | ||
| end_result = min(offset + per_page, total_count) | ||
| print( | ||
| f"\n{colorize(f'Found {total_count} product(s) matching:', 'green')} {colorize(search_term, 'white')}" | ||
| ) | ||
| print( | ||
| f"{colorize(f'Showing {start_result}-{end_result} of {total_count} results', 'cyan')}" | ||
| ) | ||
| print() | ||
| # Table headers | ||
| headers = [ | ||
| "ID", | ||
| "Test Date", | ||
| "Model", | ||
| "Firmware", | ||
| "Profiles", | ||
| "Category", | ||
| "Type", | ||
| "Company", | ||
| ] | ||
| # Get terminal width for adaptive formatting | ||
| try: | ||
| terminal_width = shutil.get_terminal_size().columns | ||
| except Exception: | ||
| terminal_width = 120 # fallback width | ||
| # Calculate minimum column widths | ||
| min_col_widths = [max(len(str(header)), 8) for header in headers] | ||
| # Calculate actual content widths (without truncation first) | ||
| actual_widths = [0] * len(headers) | ||
| for row in results: | ||
| for i, value in enumerate(row): | ||
| if value: | ||
| if i == 1: # Date column - calculate formatted date width | ||
| str_value = str(value) | ||
| if "T" in str_value: | ||
| date_part = str_value.split("T")[0] | ||
| time_part = str_value.split("T")[1].split(".")[0] | ||
| if "+" in time_part: | ||
| time_part = time_part.split("+")[0] | ||
| elif "Z" in time_part: | ||
| time_part = time_part.replace("Z", "") | ||
| formatted_date = f"{date_part} {time_part}" | ||
| actual_widths[i] = max( | ||
| actual_widths[i], len(formatted_date) | ||
| ) | ||
| else: | ||
| actual_widths[i] = max(actual_widths[i], len(str_value)) | ||
| else: | ||
| actual_widths[i] = max(actual_widths[i], len(str(value))) | ||
| # Combine minimum widths with actual content widths | ||
| col_widths = [ | ||
| max(min_col_widths[i], actual_widths[i]) for i in range(len(headers)) | ||
| ] | ||
| # Calculate space needed for separators (3 chars per separator: " | ") | ||
| separator_space = (len(headers) - 1) * 3 | ||
| total_content_width = sum(col_widths) | ||
| total_needed_width = total_content_width + separator_space | ||
| # If table is too wide for terminal, apply smart truncation | ||
| if total_needed_width > terminal_width: | ||
| available_width = terminal_width - separator_space | ||
| # Priority columns that should not be truncated (ID, Date) | ||
| protected_cols = {0, 1} # ID and Date columns | ||
| protected_width = sum(col_widths[i] for i in protected_cols) | ||
| # Width available for other columns | ||
| remaining_width = available_width - protected_width | ||
| # Columns that can be truncated | ||
| truncatable_cols = [ | ||
| i for i in range(len(headers)) if i not in protected_cols | ||
| ] | ||
| if remaining_width > 0 and truncatable_cols: | ||
| # Calculate proportional allocation for truncatable columns | ||
| current_truncatable_width = sum(col_widths[i] for i in truncatable_cols) | ||
| for i in truncatable_cols: | ||
| if current_truncatable_width > 0: | ||
| # Proportional allocation | ||
| proportion = col_widths[i] / current_truncatable_width | ||
| new_width = int(remaining_width * proportion) | ||
| # Ensure minimum width | ||
| col_widths[i] = max(new_width, min_col_widths[i]) | ||
| else: | ||
| col_widths[i] = min_col_widths[i] | ||
| # Print header | ||
| header_line = " | ".join( | ||
| header.ljust(col_widths[i]) for i, header in enumerate(headers) | ||
| ) | ||
| print(colorize(header_line, "yellow")) | ||
| print(colorize("-" * len(header_line), "white")) | ||
| # Print data rows | ||
| for row in results: | ||
| formatted_row = [] | ||
| for i, value in enumerate(row): | ||
| if value is None: | ||
| formatted_value = "" | ||
| else: | ||
| str_value = str(value) | ||
| # Special formatting for date column (index 1) | ||
| if i == 1 and value: # Date column | ||
| try: | ||
| # Handle ISO format with timezone | ||
| if "T" in str_value: | ||
| # Parse ISO format: 2024-08-15T17:53:12.9154121+08:00 | ||
| # Extract just the date and time part before timezone | ||
| date_part = str_value.split("T")[0] | ||
| time_part = str_value.split("T")[1].split(".")[ | ||
| 0 | ||
| ] # Remove microseconds | ||
| if "+" in time_part: | ||
| time_part = time_part.split("+")[0] | ||
| elif "Z" in time_part: | ||
| time_part = time_part.replace("Z", "") | ||
| formatted_value = f"{date_part} {time_part}" | ||
| elif ( | ||
| len(str_value) == 19 and " " in str_value | ||
| ): # Already in correct format | ||
| formatted_value = str_value | ||
| elif len(str_value) == 10: # Just date, add time | ||
| formatted_value = f"{str_value} 00:00:00" | ||
| else: | ||
| # Try to parse common formats | ||
| try: | ||
| parsed_date = datetime.strptime( | ||
| str_value, "%Y-%m-%d %H:%M:%S" | ||
| ) | ||
| formatted_value = parsed_date.strftime( | ||
| "%Y-%m-%d %H:%M:%S" | ||
| ) | ||
| except ValueError: | ||
| try: | ||
| parsed_date = datetime.strptime( | ||
| str_value, "%Y-%m-%d" | ||
| ) | ||
| formatted_value = parsed_date.strftime( | ||
| "%Y-%m-%d 00:00:00" | ||
| ) | ||
| except ValueError: | ||
| formatted_value = ( | ||
| str_value # Keep original if parsing fails | ||
| ) | ||
| except Exception: | ||
| formatted_value = str_value # Keep original if any error | ||
| else: | ||
| # Apply truncation based on calculated column width | ||
| max_width = col_widths[i] | ||
| if len(str_value) > max_width: | ||
| formatted_value = str_value[: max_width - 3] + "..." | ||
| else: | ||
| formatted_value = str_value | ||
| formatted_row.append(formatted_value.ljust(col_widths[i])) | ||
| print(" | ".join(formatted_row)) | ||
| # Display pagination information | ||
| print() | ||
| newline = "\n" if total_pages == 1 else "" | ||
| print(f"{colorize(f'Page {page} of {total_pages}', 'cyan')} {newline}") | ||
| # Show navigation hints | ||
| nav_hints = [] | ||
| if page > 1: | ||
| nav_hints.append(f"Previous: --page {page - 1}") | ||
| if page < total_pages: | ||
| nav_hints.append(f"Next: --page {page + 1}") | ||
| if nav_hints: | ||
| print(f"{colorize('Navigation:', 'white')} {' | '.join(nav_hints)}\n") | ||
| conn.close() | ||
| except sqlite3.Error as e: | ||
| print(f"{colorize('Database error:', 'red')} {e}") | ||
| sys.exit(1) | ||
| except Exception as e: | ||
| print(f"{colorize('Error:', 'red')} {e}") | ||
| sys.exit(1) | ||
| def main(): | ||
@@ -466,3 +182,3 @@ """Main CLI entry point""" | ||
| if args.version: | ||
| print(colorize("0.2.3", "yellow")) | ||
| print(colorize("0.2.4", "yellow")) | ||
| sys.exit(0) | ||
@@ -741,2 +457,286 @@ | ||
| def search_products(search_term: str, page: int = 1, per_page: int = 20) -> None: | ||
| """Search ONVIF products database and display results in table format with pagination. | ||
| Args: | ||
| search_term: Search term to match against model, post_title, and company_name fields | ||
| page: Page number (1-based) | ||
| per_page: Number of results per page | ||
| """ | ||
| # Get the database path relative to the script location | ||
| current_dir = os.path.dirname(os.path.abspath(__file__)) | ||
| db_path = os.path.join(current_dir, "..", "db", "products.db") | ||
| db_path = os.path.normpath(db_path) | ||
| if not os.path.exists(db_path): | ||
| print(f"{colorize('Error:', 'red')} Products database not found at {db_path}") | ||
| sys.exit(1) | ||
| # Validate pagination parameters | ||
| if page < 1: | ||
| print(f"{colorize('Error:', 'red')} Page number must be 1 or greater") | ||
| sys.exit(1) | ||
| if per_page < 1 or per_page > 100: | ||
| print(f"{colorize('Error:', 'red')} Per-page must be between 1 and 100") | ||
| sys.exit(1) | ||
| try: | ||
| conn = sqlite3.connect(db_path) | ||
| cursor = conn.cursor() | ||
| # First, get total count for pagination info | ||
| count_query = """ | ||
| SELECT COUNT(*) | ||
| FROM onvif_products | ||
| WHERE LOWER(model) LIKE LOWER(?) | ||
| OR LOWER(post_title) LIKE LOWER(?) | ||
| OR LOWER(company_name) LIKE LOWER(?) | ||
| OR LOWER(product_category) LIKE LOWER(?) | ||
| """ | ||
| search_pattern = f"%{search_term}%" | ||
| cursor.execute( | ||
| count_query, | ||
| (search_pattern, search_pattern, search_pattern, search_pattern), | ||
| ) | ||
| total_count = cursor.fetchone()[0] | ||
| if total_count == 0: | ||
| print( | ||
| f"{colorize('No products found matching:', 'yellow')} {colorize(search_term, 'white')}" | ||
| ) | ||
| return | ||
| # Calculate pagination | ||
| total_pages = (total_count + per_page - 1) // per_page # Ceiling division | ||
| if page > total_pages: | ||
| print( | ||
| f"{colorize('Error:', 'red')} Page {page} does not exist. Total pages: {total_pages}" | ||
| ) | ||
| return | ||
| offset = (page - 1) * per_page | ||
| # Search query with pagination | ||
| query = """ | ||
| SELECT ID, test_date, post_title, product_firmware_version, | ||
| product_profiles, product_category, type, | ||
| company_name | ||
| FROM onvif_products | ||
| WHERE LOWER(model) LIKE LOWER(?) | ||
| OR LOWER(post_title) LIKE LOWER(?) | ||
| OR LOWER(company_name) LIKE LOWER(?) | ||
| OR LOWER(product_category) LIKE LOWER(?) | ||
| ORDER BY test_date DESC | ||
| LIMIT ? OFFSET ? | ||
| """ | ||
| cursor.execute( | ||
| query, | ||
| ( | ||
| search_pattern, | ||
| search_pattern, | ||
| search_pattern, | ||
| search_pattern, | ||
| per_page, | ||
| offset, | ||
| ), | ||
| ) | ||
| results = cursor.fetchall() | ||
| # Display results in table format | ||
| start_result = offset + 1 | ||
| end_result = min(offset + per_page, total_count) | ||
| print( | ||
| f"\n{colorize(f'Found {total_count} product(s) matching:', 'green')} {colorize(search_term, 'white')}" | ||
| ) | ||
| print( | ||
| f"{colorize(f'Showing {start_result}-{end_result} of {total_count} results', 'cyan')}" | ||
| ) | ||
| print() | ||
| # Table headers | ||
| headers = [ | ||
| "ID", | ||
| "Test Date", | ||
| "Model", | ||
| "Firmware", | ||
| "Profiles", | ||
| "Category", | ||
| "Type", | ||
| "Company", | ||
| ] | ||
| # Get terminal width for adaptive formatting | ||
| try: | ||
| terminal_width = shutil.get_terminal_size().columns | ||
| except Exception: | ||
| terminal_width = 120 # fallback width | ||
| # Calculate minimum column widths | ||
| min_col_widths = [max(len(str(header)), 8) for header in headers] | ||
| # Calculate actual content widths (without truncation first) | ||
| actual_widths = [0] * len(headers) | ||
| for row in results: | ||
| for i, value in enumerate(row): | ||
| if value: | ||
| if i == 1: # Date column - calculate formatted date width | ||
| str_value = str(value) | ||
| if "T" in str_value: | ||
| date_part = str_value.split("T")[0] | ||
| time_part = str_value.split("T")[1].split(".")[0] | ||
| if "+" in time_part: | ||
| time_part = time_part.split("+")[0] | ||
| elif "Z" in time_part: | ||
| time_part = time_part.replace("Z", "") | ||
| formatted_date = f"{date_part} {time_part}" | ||
| actual_widths[i] = max( | ||
| actual_widths[i], len(formatted_date) | ||
| ) | ||
| else: | ||
| actual_widths[i] = max(actual_widths[i], len(str_value)) | ||
| else: | ||
| actual_widths[i] = max(actual_widths[i], len(str(value))) | ||
| # Combine minimum widths with actual content widths | ||
| col_widths = [ | ||
| max(min_col_widths[i], actual_widths[i]) for i in range(len(headers)) | ||
| ] | ||
| # Calculate space needed for separators (3 chars per separator: " | ") | ||
| separator_space = (len(headers) - 1) * 3 | ||
| total_content_width = sum(col_widths) | ||
| total_needed_width = total_content_width + separator_space | ||
| # If table is too wide for terminal, apply smart truncation | ||
| if total_needed_width > terminal_width: | ||
| available_width = terminal_width - separator_space | ||
| # Priority columns that should not be truncated (ID, Date) | ||
| protected_cols = {0, 1} # ID and Date columns | ||
| protected_width = sum(col_widths[i] for i in protected_cols) | ||
| # Width available for other columns | ||
| remaining_width = available_width - protected_width | ||
| # Columns that can be truncated | ||
| truncatable_cols = [ | ||
| i for i in range(len(headers)) if i not in protected_cols | ||
| ] | ||
| if remaining_width > 0 and truncatable_cols: | ||
| # Calculate proportional allocation for truncatable columns | ||
| current_truncatable_width = sum(col_widths[i] for i in truncatable_cols) | ||
| for i in truncatable_cols: | ||
| if current_truncatable_width > 0: | ||
| # Proportional allocation | ||
| proportion = col_widths[i] / current_truncatable_width | ||
| new_width = int(remaining_width * proportion) | ||
| # Ensure minimum width | ||
| col_widths[i] = max(new_width, min_col_widths[i]) | ||
| else: | ||
| col_widths[i] = min_col_widths[i] | ||
| # Print header | ||
| header_line = " | ".join( | ||
| header.ljust(col_widths[i]) for i, header in enumerate(headers) | ||
| ) | ||
| print(colorize(header_line, "yellow")) | ||
| print(colorize("-" * len(header_line), "white")) | ||
| # Print data rows | ||
| for row in results: | ||
| formatted_row = [] | ||
| for i, value in enumerate(row): | ||
| if value is None: | ||
| formatted_value = "" | ||
| else: | ||
| str_value = str(value) | ||
| # Special formatting for date column (index 1) | ||
| if i == 1 and value: # Date column | ||
| try: | ||
| # Handle ISO format with timezone | ||
| if "T" in str_value: | ||
| # Parse ISO format: 2024-08-15T17:53:12.9154121+08:00 | ||
| # Extract just the date and time part before timezone | ||
| date_part = str_value.split("T")[0] | ||
| time_part = str_value.split("T")[1].split(".")[ | ||
| 0 | ||
| ] # Remove microseconds | ||
| if "+" in time_part: | ||
| time_part = time_part.split("+")[0] | ||
| elif "Z" in time_part: | ||
| time_part = time_part.replace("Z", "") | ||
| formatted_value = f"{date_part} {time_part}" | ||
| elif ( | ||
| len(str_value) == 19 and " " in str_value | ||
| ): # Already in correct format | ||
| formatted_value = str_value | ||
| elif len(str_value) == 10: # Just date, add time | ||
| formatted_value = f"{str_value} 00:00:00" | ||
| else: | ||
| # Try to parse common formats | ||
| try: | ||
| parsed_date = datetime.strptime( | ||
| str_value, "%Y-%m-%d %H:%M:%S" | ||
| ) | ||
| formatted_value = parsed_date.strftime( | ||
| "%Y-%m-%d %H:%M:%S" | ||
| ) | ||
| except ValueError: | ||
| try: | ||
| parsed_date = datetime.strptime( | ||
| str_value, "%Y-%m-%d" | ||
| ) | ||
| formatted_value = parsed_date.strftime( | ||
| "%Y-%m-%d 00:00:00" | ||
| ) | ||
| except ValueError: | ||
| formatted_value = ( | ||
| str_value # Keep original if parsing fails | ||
| ) | ||
| except Exception: | ||
| formatted_value = str_value # Keep original if any error | ||
| else: | ||
| # Apply truncation based on calculated column width | ||
| max_width = col_widths[i] | ||
| if len(str_value) > max_width: | ||
| formatted_value = str_value[: max_width - 3] + "..." | ||
| else: | ||
| formatted_value = str_value | ||
| formatted_row.append(formatted_value.ljust(col_widths[i])) | ||
| print(" | ".join(formatted_row)) | ||
| # Display pagination information | ||
| print() | ||
| newline = "\n" if total_pages == 1 else "" | ||
| print(f"{colorize(f'Page {page} of {total_pages}', 'cyan')} {newline}") | ||
| # Show navigation hints | ||
| nav_hints = [] | ||
| if page > 1: | ||
| nav_hints.append(f"Previous: --page {page - 1}") | ||
| if page < total_pages: | ||
| nav_hints.append(f"Next: --page {page + 1}") | ||
| if nav_hints: | ||
| print(f"{colorize('Navigation:', 'white')} {' | '.join(nav_hints)}\n") | ||
| conn.close() | ||
| except sqlite3.Error as e: | ||
| print(f"{colorize('Database error:', 'red')} {e}") | ||
| sys.exit(1) | ||
| except Exception as e: | ||
| print(f"{colorize('Error:', 'red')} {e}") | ||
| sys.exit(1) | ||
| def setup_warning_format(): | ||
@@ -743,0 +743,0 @@ """Setup custom warning format to show clean, concise warnings""" |
+49
-22
@@ -72,2 +72,11 @@ # onvif/cli/utils.py | ||
| def _is_valid_json(s: str) -> bool: | ||
| """Check if a string is valid JSON without raising exceptions.""" | ||
| try: | ||
| json.loads(s) | ||
| except ValueError: | ||
| return False | ||
| return True | ||
| def parse_json_params(params_str: str) -> Dict[str, Any]: | ||
@@ -84,6 +93,4 @@ """Parse parameters from a JSON string or key=value pairs into a dict. | ||
| # If the whole string is valid JSON, return it directly | ||
| try: | ||
| return json.loads(params_str) | ||
| except Exception: | ||
| pass | ||
| if _is_valid_json(params_str): | ||
| return json.loads(params_str) # We know it's valid, so this should not fail | ||
@@ -356,5 +363,3 @@ # Otherwise parse key=value pairs but allow JSON values for the right-hand side | ||
| return None | ||
| def colorize(text: str, color: str) -> str: | ||
@@ -383,4 +388,8 @@ """Add color to text for terminal output""" | ||
| ) | ||
| except Exception: | ||
| pass # Fallback to no colors if error | ||
| except (ImportError, AttributeError, OSError): | ||
| # Specific exceptions that can occur: | ||
| # - ImportError: ctypes not available | ||
| # - AttributeError: Windows API functions not available | ||
| # - OSError: Console mode setting failed | ||
| colorize._colors_enabled = False | ||
@@ -917,4 +926,8 @@ colors = { | ||
| ) | ||
| except Exception: | ||
| pass # Skip if can't parse | ||
| except (etree.XMLSyntaxError, OSError, PermissionError): | ||
| # Skip files that can't be parsed or accessed: | ||
| # - XMLSyntaxError: malformed XML | ||
| # - OSError: file access issues | ||
| # - PermissionError: insufficient permissions | ||
| continue | ||
@@ -954,4 +967,8 @@ # Process includes | ||
| ) | ||
| except Exception: | ||
| pass # Skip if can't parse | ||
| except (etree.XMLSyntaxError, OSError, PermissionError): | ||
| # Skip files that can't be parsed or accessed: | ||
| # - XMLSyntaxError: malformed XML | ||
| # - OSError: file access issues | ||
| # - PermissionError: insufficient permissions | ||
| continue | ||
@@ -1269,4 +1286,6 @@ | ||
| attr_doc_elem = attr.find("xs:annotation/xs:documentation", namespaces) | ||
| if attr_doc_elem is not None and attr_doc_elem.text: | ||
| attr_doc = clean_documentation_html(attr_doc_elem.text.strip()) | ||
| if attr_doc_elem is not None: | ||
| full_text = extract_documentation_text(attr_doc_elem) | ||
| if full_text: | ||
| attr_doc = clean_documentation_html(full_text) | ||
@@ -1302,4 +1321,6 @@ attr_info = { | ||
| doc_elem = elem.find("xs:annotation/xs:documentation", namespaces) | ||
| if doc_elem is not None and doc_elem.text: | ||
| child_doc = clean_documentation_html(doc_elem.text.strip()) | ||
| if doc_elem is not None: | ||
| full_text = extract_documentation_text(doc_elem) | ||
| if full_text: | ||
| child_doc = clean_documentation_html(full_text) | ||
@@ -1394,4 +1415,6 @@ child_info = { | ||
| attr_doc_elem = attr.find("xs:annotation/xs:documentation", namespaces) | ||
| if attr_doc_elem is not None and attr_doc_elem.text: | ||
| attr_doc = clean_documentation_html(attr_doc_elem.text.strip()) | ||
| if attr_doc_elem is not None: | ||
| full_text = extract_documentation_text(attr_doc_elem) | ||
| if full_text: | ||
| attr_doc = clean_documentation_html(full_text) | ||
@@ -1444,4 +1467,6 @@ attr_info = { | ||
| attr_doc_elem = attr.find("xs:annotation/xs:documentation", namespaces) | ||
| if attr_doc_elem is not None and attr_doc_elem.text: | ||
| attr_doc = clean_documentation_html(attr_doc_elem.text.strip()) | ||
| if attr_doc_elem is not None: | ||
| full_text = extract_documentation_text(attr_doc_elem) | ||
| if full_text: | ||
| attr_doc = clean_documentation_html(full_text) | ||
@@ -1477,4 +1502,6 @@ attr_info = { | ||
| doc_elem = elem.find("xs:annotation/xs:documentation", namespaces) | ||
| if doc_elem is not None and doc_elem.text: | ||
| child_doc = clean_documentation_html(doc_elem.text.strip()) | ||
| if doc_elem is not None: | ||
| full_text = extract_documentation_text(doc_elem) | ||
| if full_text: | ||
| child_doc = clean_documentation_html(full_text) | ||
@@ -1481,0 +1508,0 @@ child_info = { |
+28
-0
@@ -13,2 +13,3 @@ # onvif/client.py | ||
| Subscription, | ||
| PausableSubscription, | ||
| Imaging, | ||
@@ -217,2 +218,5 @@ Media, | ||
| self._subscriptions = {} # Dictionary for multiple Subscription instances | ||
| self._pausable_subscriptions = ( | ||
| {} | ||
| ) # Dictionary for multiple PausableSubscription instances | ||
@@ -468,2 +472,26 @@ self._imaging = None | ||
| @service | ||
| def pausable_subscription(self, SubscriptionRef): | ||
| logger.debug("Initializing PausableSubscription service") | ||
| xaddr = None | ||
| addr_obj = SubscriptionRef["SubscriptionReference"]["Address"] | ||
| if isinstance(addr_obj, dict) and "_value_1" in addr_obj: | ||
| xaddr = addr_obj["_value_1"] | ||
| elif hasattr(addr_obj, "_value_1"): | ||
| xaddr = addr_obj._value_1 | ||
| xaddr = self._rewrite_xaddr_if_needed(xaddr) | ||
| if not xaddr: | ||
| raise RuntimeError( | ||
| "SubscriptionReference.Address missing in subscription response" | ||
| ) | ||
| if xaddr not in self._pausable_subscriptions: | ||
| self._pausable_subscriptions[xaddr] = PausableSubscription( | ||
| xaddr=xaddr, **self.common_args | ||
| ) | ||
| return self._pausable_subscriptions[xaddr] | ||
| # Imaging | ||
@@ -470,0 +498,0 @@ |
@@ -8,2 +8,3 @@ # onvif/services/__init__.py | ||
| from .events.subscription import Subscription | ||
| from .events.pausable_subscription import PausableSubscription | ||
| from .imaging import Imaging | ||
@@ -47,2 +48,3 @@ from .media import Media | ||
| "Subscription", | ||
| "PausableSubscription", | ||
| "Imaging", | ||
@@ -49,0 +51,0 @@ "Media", |
@@ -258,4 +258,6 @@ # onvif/utils/service.py | ||
| doc_text = None | ||
| except Exception: | ||
| pass # Documentation is optional | ||
| except Exception as e: | ||
| logger.warning( | ||
| f"Could not retrieve documentation for {method_name}: {e}" | ||
| ) # Documentation is optional | ||
@@ -262,0 +264,0 @@ return { |
+14
-0
@@ -245,2 +245,16 @@ # onvif/utils/wsdl.py | ||
| }, | ||
| "pausable_subscription": { | ||
| "ver10": { | ||
| "path": os.path.join( | ||
| base_dir, | ||
| ( | ||
| "event-vs.wsdl" | ||
| if use_flat | ||
| else "ver10/events/wsdl/event-vs.wsdl" | ||
| ), | ||
| ), | ||
| "binding": "PausableSubscriptionManagerBinding", | ||
| "namespace": "http://www.onvif.org/ver10/events/wsdl", | ||
| } | ||
| }, | ||
| "accesscontrol": { | ||
@@ -247,0 +261,0 @@ "ver10": { |
+11
-23
| Metadata-Version: 2.4 | ||
| Name: onvif-python | ||
| Version: 0.2.3 | ||
| Version: 0.2.4 | ||
| Summary: A modern Python library for ONVIF-compliant devices | ||
@@ -47,10 +47,9 @@ Author-email: Nirsimetri Technologies® <open@nirsimetri.com> | ||
| <div align="center"> | ||
| [](https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) | ||
| [](https://deepwiki.com/nirsimetri/onvif-python) | ||
| [](https://pypi.org/project/onvif-python/) | ||
| [](https://clickpy.clickhouse.com/dashboard/onvif-python) | ||
| <img alt="Codacy grade" src="https://img.shields.io/codacy/grade/bff08a94e4d447b690cea49c6594826d?label=Code%20Quality&logo=codacy" href="https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"> | ||
| <img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/nirsimetri/onvif-python"> | ||
| <img alt="PyPI Version" src="https://img.shields.io/badge/PyPI-0.2.4-orange?logo=archive&color=yellow" href="https://pypi.org/project/onvif-python/"> | ||
| <img alt="Pepy Total Downloads" src="https://img.shields.io/pepy/dt/onvif-python?label=Downloads&color=red" href="https://pepy.tech/projects/onvif-python"> | ||
| <br> | ||
| [](https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml) | ||
| [](https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml) | ||
| <img alt="Build" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml/badge.svg?branch=main" href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml"> | ||
| <img alt="Upload Python Package" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml/badge.svg" href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml"> | ||
| </div> | ||
@@ -360,3 +359,3 @@ | ||
| ONVIF Terminal Client — v0.2.3 | ||
| ONVIF Terminal Client — v0.2.4 | ||
| https://github.com/nirsimetri/onvif-python | ||
@@ -434,3 +433,3 @@ | ||
| ```bash | ||
| ONVIF Interactive Shell — v0.2.3 | ||
| ONVIF Interactive Shell — v0.2.4 | ||
| https://github.com/nirsimetri/onvif-python | ||
@@ -1034,3 +1033,3 @@ | ||
| | Device Management | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [device.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/device/wsdl/devicemgmt.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ✅ Complete | | ||
| | Events | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [event.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ⚠️ Partial | | ||
| | Events | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [event.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ✅ Complete | | ||
| | Access Control | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/AccessControl.xml) | [accesscontrol.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/pacs/accesscontrol.wsdl) | [types.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/pacs/types.xsd) | ✅ Complete | | ||
@@ -1057,3 +1056,2 @@ | Access Rules | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/AccessRules.xml) | [accessrules.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/accessrules/wsdl/accessrules.wsdl) | - | ✅ Complete | | ||
| | Replay Control | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Replay.xml) | [replay.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/replay.wsdl) | - | ✅ Complete | | ||
| | Resource Query | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/ResourceQuery.xml) | - | | ❌ Any idea? | | ||
| | Schedule | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Schedule.xml) | [schedule.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schedule/wsdl/schedule.wsdl) | - | ✅ Complete | | ||
@@ -1063,3 +1061,2 @@ | Security | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Security.xml) | [advancedsecurity.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl) | - | ✅ Complete | | ||
| | Uplink | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Uplink.xml) | [uplink.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/uplink/wsdl/uplink.wsdl) | - | ✅ Complete | | ||
| | WebRTC | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/WebRTC.xml) | - | - | ❌ Any idea? | | ||
@@ -1178,15 +1175,6 @@ </details> | ||
| ## Alternatives | ||
| If you are looking for other ONVIF Python libraries, here are some alternatives: | ||
| - [python-onvif-zeep](https://github.com/FalkTannhaeuser/python-onvif-zeep): | ||
| A synchronous ONVIF client library for Python, using Zeep for SOAP communication. Focuses on compatibility and ease of use for standard ONVIF device operations. Good for scripts and applications where async is not required. | ||
| - [python-onvif-zeep-async](https://github.com/openvideolibs/python-onvif-zeep-async): | ||
| An asynchronous ONVIF client library for Python, based on Zeep and asyncio. Suitable for applications requiring non-blocking operations and concurrent device communication. Supports many ONVIF services and is actively maintained. | ||
| ## References | ||
| - [ONVIF Official Specifications](https://www.onvif.org/profiles/specifications/specification-history/) | ||
| - [ONVIF Official Specs Repository](https://github.com/onvif/specs) | ||
| - [ONVIF Application Programmer's Guide](https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_WG-APG-Application_Programmers_Guide-1.pdf) | ||
| - [ONVIF 2.0 Service Operation Index](https://www.onvif.org/onvif/ver20/util/operationIndex.html) | ||
@@ -1193,0 +1181,0 @@ - [Usage Examples](./examples/) |
+1
-1
@@ -7,3 +7,3 @@ [build-system] | ||
| name = "onvif-python" | ||
| version = "0.2.3" | ||
| version = "0.2.4" | ||
| description = "A modern Python library for ONVIF-compliant devices" | ||
@@ -10,0 +10,0 @@ readme = "README.md" |
+10
-22
| <h1 align="center">ONVIF Python</h1> | ||
| <div align="center"> | ||
| [](https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) | ||
| [](https://deepwiki.com/nirsimetri/onvif-python) | ||
| [](https://pypi.org/project/onvif-python/) | ||
| [](https://clickpy.clickhouse.com/dashboard/onvif-python) | ||
| <img alt="Codacy grade" src="https://img.shields.io/codacy/grade/bff08a94e4d447b690cea49c6594826d?label=Code%20Quality&logo=codacy" href="https://app.codacy.com/gh/nirsimetri/onvif-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade"> | ||
| <img alt="Ask DeepWiki" src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/nirsimetri/onvif-python"> | ||
| <img alt="PyPI Version" src="https://img.shields.io/badge/PyPI-0.2.4-orange?logo=archive&color=yellow" href="https://pypi.org/project/onvif-python/"> | ||
| <img alt="Pepy Total Downloads" src="https://img.shields.io/pepy/dt/onvif-python?label=Downloads&color=red" href="https://pepy.tech/projects/onvif-python"> | ||
| <br> | ||
| [](https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml) | ||
| [](https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml) | ||
| <img alt="Build" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml/badge.svg?branch=main" href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-app.yml"> | ||
| <img alt="Upload Python Package" src="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml/badge.svg" href="https://github.com/nirsimetri/onvif-python/actions/workflows/python-publish.yml"> | ||
| </div> | ||
@@ -316,3 +315,3 @@ | ||
| ONVIF Terminal Client — v0.2.3 | ||
| ONVIF Terminal Client — v0.2.4 | ||
| https://github.com/nirsimetri/onvif-python | ||
@@ -390,3 +389,3 @@ | ||
| ```bash | ||
| ONVIF Interactive Shell — v0.2.3 | ||
| ONVIF Interactive Shell — v0.2.4 | ||
| https://github.com/nirsimetri/onvif-python | ||
@@ -990,3 +989,3 @@ | ||
| | Device Management | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [device.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/device/wsdl/devicemgmt.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ✅ Complete | | ||
| | Events | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [event.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ⚠️ Partial | | ||
| | Events | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Core.xml) | [event.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/events/wsdl/event.wsdl) | [onvif.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/onvif.xsd) <br> [common.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schema/common.xsd) | ✅ Complete | | ||
| | Access Control | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/AccessControl.xml) | [accesscontrol.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/pacs/accesscontrol.wsdl) | [types.xsd](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/pacs/types.xsd) | ✅ Complete | | ||
@@ -1013,3 +1012,2 @@ | Access Rules | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/AccessRules.xml) | [accessrules.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/accessrules/wsdl/accessrules.wsdl) | - | ✅ Complete | | ||
| | Replay Control | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Replay.xml) | [replay.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/replay.wsdl) | - | ✅ Complete | | ||
| | Resource Query | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/ResourceQuery.xml) | - | | ❌ Any idea? | | ||
| | Schedule | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Schedule.xml) | [schedule.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/schedule/wsdl/schedule.wsdl) | - | ✅ Complete | | ||
@@ -1019,3 +1017,2 @@ | Security | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Security.xml) | [advancedsecurity.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/advancedsecurity/wsdl/advancedsecurity.wsdl) | - | ✅ Complete | | ||
| | Uplink | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/Uplink.xml) | [uplink.wsdl](https://developer.onvif.org/pub/specs/branches/development/wsdl/ver10/uplink/wsdl/uplink.wsdl) | - | ✅ Complete | | ||
| | WebRTC | [Document](https://developer.onvif.org/pub/specs/branches/development/doc/WebRTC.xml) | - | - | ❌ Any idea? | | ||
@@ -1134,15 +1131,6 @@ </details> | ||
| ## Alternatives | ||
| If you are looking for other ONVIF Python libraries, here are some alternatives: | ||
| - [python-onvif-zeep](https://github.com/FalkTannhaeuser/python-onvif-zeep): | ||
| A synchronous ONVIF client library for Python, using Zeep for SOAP communication. Focuses on compatibility and ease of use for standard ONVIF device operations. Good for scripts and applications where async is not required. | ||
| - [python-onvif-zeep-async](https://github.com/openvideolibs/python-onvif-zeep-async): | ||
| An asynchronous ONVIF client library for Python, based on Zeep and asyncio. Suitable for applications requiring non-blocking operations and concurrent device communication. Supports many ONVIF services and is actively maintained. | ||
| ## References | ||
| - [ONVIF Official Specifications](https://www.onvif.org/profiles/specifications/specification-history/) | ||
| - [ONVIF Official Specs Repository](https://github.com/onvif/specs) | ||
| - [ONVIF Application Programmer's Guide](https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_WG-APG-Application_Programmers_Guide-1.pdf) | ||
| - [ONVIF 2.0 Service Operation Index](https://www.onvif.org/onvif/ver20/util/operationIndex.html) | ||
@@ -1149,0 +1137,0 @@ - [Usage Examples](./examples/) |
| import os | ||
| import pytest | ||
| from onvif import ONVIFWSDL | ||
| def test_get_wsdl_definition_valid(): | ||
| """Test valid WSDL definition retrieval""" | ||
| definition = ONVIFWSDL.get_definition("devicemgmt", "ver10") | ||
| assert isinstance(definition, dict) | ||
| assert "path" in definition | ||
| assert "binding" in definition | ||
| assert "namespace" in definition | ||
| assert os.path.exists( | ||
| definition["path"] | ||
| ), f"WSDL path not found: {definition['path']}" | ||
| def test_get_wsdl_definition_invalid_service(): | ||
| """Test invalid service name""" | ||
| with pytest.raises(ValueError) as excinfo: | ||
| ONVIFWSDL.get_definition("invalid_service", "ver10") | ||
| assert "Unknown service" in str(excinfo.value) | ||
| def test_get_wsdl_definition_invalid_version(): | ||
| """Test invalid version for existing service""" | ||
| with pytest.raises(ValueError) as excinfo: | ||
| ONVIFWSDL.get_definition("devicemgmt", "ver99") | ||
| assert "not available" in str(excinfo.value) | ||
| @pytest.mark.parametrize("service", ["devicemgmt", "media"]) | ||
| @pytest.mark.parametrize("version", ["ver10"]) | ||
| def test_get_wsdl_definition_combinations(service, version): | ||
| """Test various service and version combinations""" | ||
| definition = ONVIFWSDL.get_definition(service, version) | ||
| assert isinstance(definition, dict) | ||
| assert "path" in definition | ||
| assert os.path.exists( | ||
| definition["path"] | ||
| ), f"{service} {version} missing: {definition['path']}" | ||
| # Verify binding and namespace are present | ||
| assert definition["binding"] is not None | ||
| assert definition["namespace"] is not None |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
85014747
010755
0.5%