diff --git a/.gitignore b/.gitignore index e883298..f023b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ venv.bak/ .eggs/ dist/ build/ +uv.lock # AI .claude diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4b25ad6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,20 @@ +# AGENTS.md + +## Commands +- **Install**: `uv sync --all-extras` +- **Test all**: `uv run pytest` +- **Test single**: `uv run pytest tests/test_models.py::TestKeyPart::test_valid_key` +- **Build docs**: `uv run mkdocs build` +- **Serve docs**: `uv run mkdocs serve` + +## Code Style +- **Python**: 3.12+, use `uv` for dependency management +- **Imports**: Group stdlib, third-party, local imports; use `from pathlib import Path` for paths +- **Types**: Full type hints required, use Pydantic models with discriminated unions +- **Naming**: + - Classes: PascalCase + - Functions/variables: snake_case + - Constants: UPPER_SNAKE_CASE + - Keys: end with `_ID`, tables: lowercase_with_underscores, value sets: end with `_set` +- **Error handling**: Use Pydantic validators, raise ValueError with descriptive messages +- **Models**: Use frozen ConfigDict for immutable data, populate_by_name for alias support \ No newline at end of file diff --git a/LICENSE b/LICENSE index f0870fd..2f244ac 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,395 @@ -MIT License - -Copyright (c) 2025 modelEAU - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index a965834..1e7812f 100644 --- a/README.md +++ b/README.md @@ -62,4 +62,4 @@ uv pip install -e ".[dev]" ```uv run mkdocs serve``` ## License -dat*EAU*base is published under the MIT license. +dat*EAU*base is published under the CC-BY 4.0 license. diff --git a/docs/assets/erd_interactive.html b/docs/assets/erd_interactive.html new file mode 100644 index 0000000..49b9cac --- /dev/null +++ b/docs/assets/erd_interactive.html @@ -0,0 +1,2420 @@ + + + + + + datEAUbase ERD + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/docs/assets/erd_simple.html b/docs/assets/erd_simple.html new file mode 100644 index 0000000..49b9cac --- /dev/null +++ b/docs/assets/erd_simple.html @@ -0,0 +1,2420 @@ + + + + + + datEAUbase ERD + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/docs/contributing/dictionary.md b/docs/contributing/dictionary.md new file mode 100644 index 0000000..a224a84 --- /dev/null +++ b/docs/contributing/dictionary.md @@ -0,0 +1,464 @@ +# The Dictionary: A Self-Documenting Database Schema + +## Purpose + +The dictionary (stored as `dictionary.json` at the project root) serves as a comprehensive metadata repository that defines every component of the dat*EAU*base data model. It acts as a single source of truth from which you can generate SQL schemas, documentation, and entity-relationship diagrams. + +**Key Principle**: Each unique field concept gets exactly ONE entry in the dictionary, even if that field appears in multiple tables. The `table_presence` object indicates where each field appears and in what role. + +The dictionary is self-referential: it contains the definitions needed to describe itself, making it bootstrapped and internally consistent. + +## Understanding the Structure + +### JSON Format + +The dictionary uses a hierarchical JSON structure that eliminates sparse columns. Each part has only the metadata it needs: + +```json +{ + "parts": [ + { + "Part_ID": "Contact_ID", + "Label": "Contact ID", + "Description": "Identifier for contacts", + "Part_type": "key", + "SQL_data_type": "int", + "table_presence": { + "contact": { + "role": "key", + "required": true, + "order": 1 + }, + "project_has_contact": { + "role": "compositeKeySecond", + "required": false, + "order": 2 + } + } + } + ] +} +``` + +### Core Fields + +Every part has these core metadata fields: + +- **Part_ID**: Unique identifier for this field/table/value +- **Label**: Human-readable name +- **Description**: Detailed explanation of what this part represents +- **Part_type**: Classification (`table`, `key`, `property`, `compositeKeyFirst`, `compositeKeySecond`, `parentKey`, `valueSet`, `valueSetMember`) +- **Value_set_part_ID**: If this property is constrained by a value set, which set (optional) +- **Member_of_set_part_ID**: If this is a value set member, which set it belongs to (required for valueSetMember) +- **Ancestor_part_ID**: For `parentKey` type, the Part_ID of the ancestor being referenced (enables hierarchical relationships within the same table) +- **SQL_data_type**: SQL data type (e.g., `int`, `nvarchar(100)`, `datetime`) (optional) +- **Is_required**: Whether this field is mandatory (NOT NULL) (optional) +- **Default_value**: Default value for the field (optional) +- **Sort_order**: Display order for documentation/UI (optional) + +### Table Presence Object + +For fields (keys and properties), the `table_presence` object maps table names to metadata about how the field appears: + +```json +"table_presence": { + "table_name": { + "role": "key|property|compositeKeyFirst|compositeKeySecond", + "required": true|false, + "order": 1 + } +} +``` + +**Roles**: + +- **`key`**: This field is the primary key in this table +- **`compositeKeyFirst`**: First part of a composite primary key +- **`compositeKeySecond`**: Second part of a composite primary key +- **`property`**: This field is a regular column in this table + +**Example**: `Equipment_ID` has: + +```json +{ + "Part_ID": "Equipment_ID", + "table_presence": { + "equipment": {"role": "key", "required": true, "order": 1}, + "metadata": {"role": "property", "required": false, "order": 5}, + "project_has_equipment": {"role": "compositeKeySecond", "required": false, "order": 2} + } +} +``` + +## Reading the Dictionary + +### Find acceptable values for a field + +To determine what values a field can accept, check if it references a valueSet: + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Get the value set for a field +field = mgr._find_part("Site_type") +if field and hasattr(field, 'value_set_part_id'): + value_set_id = field.value_set_part_id + print(f"Field 'Site_type' uses value set: {value_set_id}") +else: + print("Field 'Site_type' has no value set constraint") +``` + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Get all valid values for that set +members = mgr.get_value_set_members("Site_type") +for member in members: + print(f"{member['Part_ID']}: {member['Label']} - {member['Description']}") +``` + +### Get all columns in a table + +To retrieve all columns that appear in a specific table: + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Get all columns in the site table +columns = mgr.get_table_columns("site") +for col in columns: + print(f"{col['Part_ID']}: {col['Label']} ({col['SQL_data_type']}) - {col['Role']} - Required: {col['Is_required']}") +``` + +Or to see what role each field plays: + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Show role information for each field in the site table +columns = mgr.get_table_columns("site") +for col in columns: + print(f"{col['Part_ID']}: {col['Label']} - Role: {col['Role']}") +``` + +### Find which tables contain a specific field + +To see all tables where `Equipment_ID` appears: + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Find all tables where Equipment_ID appears +tables = mgr.get_field_tables("Equipment_ID") +for table_info in tables: + print(f"Table: {table_info['Table_ID']}, Role: {table_info['Role']}, Required: {table_info['Required']}, Order: {table_info['Order']}") +``` + +### Find all primary keys in the database + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Get all primary keys +primary_keys = mgr.get_primary_keys() +for pk in primary_keys: + print(f"{pk['Part_ID']}: {pk['Label']} ({pk['SQL_data_type']}) - Primary in: {pk['Primary_in_tables']}") +``` + +Or to see just the key names: + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Just the key names +primary_keys = mgr.get_primary_keys() +for pk in primary_keys: + print(f"{pk['Part_ID']}: {pk['Label']}") +``` + +### List all tables in the model + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# List all tables +tables = mgr.list_tables() +for table_id in tables: + table = mgr._find_part(table_id) + print(f"{table_id}: {table.label} - {table.description}") +``` + +### Find fields that appear in multiple tables + +```python exec="true" source="above" result="console" +from open_dateaubase.data_model.helpers import DictionaryManager +mgr = DictionaryManager.load("src/dictionary.json") + +# Find fields that appear in multiple tables +shared_fields = mgr.get_shared_fields() +for field in shared_fields[:10]: # Show first 10 + print(f"{field['Part_ID']}: {field['Label']} ({field['Part_type']}) - Used in {field['Table_count']} tables") + for table in field['Tables']: + print(f" - {table['Table_ID']}: {table['Role']}") + print() +``` + +## Editing the Dictionary + +The dictionary should be edited using the `DictionaryManager` helper class, which ensures validation and consistency. + +### Adding a New Value Set + +```python +from open_dateaubase.data_model.helpers import DictionaryManager + +mgr = DictionaryManager.load("src/dictionary.json") + +# Create a new value set +mgr.create_value_set("Status_set", "Status Values", "Valid status values for records") + +# Add members to the set +mgr.add_value_set_member("Status_set", "active", "Active", "Record is currently active", order=1) +mgr.add_value_set_member("Status_set", "inactive", "Inactive", "Record is currently inactive", order=2) +mgr.add_value_set_member("Status_set", "pending", "Pending", "Record is pending review", order=3) + +# Save the changes +mgr.save() +``` + +### Adding a New Table + +```python +from open_dateaubase.data_model.helpers import DictionaryManager + +mgr = DictionaryManager.load("src/dictionary.json") + +# Create the table +mgr.create_table("observation", "Observation", "Environmental observation records") + +# Add primary key +mgr.add_field_to_table( + table_id="observation", + field_id="Observation_ID", + label="Observation ID", + description="Primary key for observations", + role="key", + sql_data_type="int", + required=True, + order=1 +) + +# Add regular fields +mgr.add_field_to_table( + table_id="observation", + field_id="Observation_date", + label="Observation Date", + description="Date when observation was made", + role="property", + sql_data_type="datetime", + required=True, + order=2 +) + +mgr.add_field_to_table( + table_id="observation", + field_id="Value", + label="Value", + description="Observed value", + role="property", + sql_data_type="float", + required=False, + order=3 +) + +# Save the changes +mgr.save() +``` + +### Adding Fields to an Existing Table + +The `add_field_to_table()` method handles both new and existing fields automatically: + +- **If the field already exists** (like `Site_ID` used in multiple tables), it updates the field's `table_presence` to include this table +- **If the field doesn't exist**, it creates a new field part + +```python +from open_dateaubase.data_model.helpers import DictionaryManager + +mgr = DictionaryManager.load("src/dictionary.json") + +# Add a foreign key (Site_ID likely already exists in the dictionary) +mgr.add_field_to_table( + table_id="observation", + field_id="Site_ID", + label="Site ID", + description="Foreign key to site", + role="property", + sql_data_type="int", + required=True, + order=4 +) + +# Add a new field with value set constraint +mgr.add_field_to_table( + table_id="observation", + field_id="Status", + label="Status", + description="Current status of observation", + role="property", + sql_data_type="nvarchar(50)", + required=False, + value_set_id="Status_set", + order=5 +) + +# Add another new field +mgr.add_field_to_table( + table_id="observation", + field_id="Notes", + label="Notes", + description="Additional notes about observation", + role="property", + sql_data_type="nvarchar(500)", + required=False, + order=6 +) + +mgr.save() +``` + +### Adding a Hierarchical Relationship (Parent Key) + +For tables with parent-child relationships within the same table: + +```python +from open_dateaubase.data_model.helpers import DictionaryManager + +mgr = DictionaryManager.load("src/dictionary.json") + +# Add a parent key for hierarchical structure +mgr.add_parent_key( + table_id="site", + parent_key_id="Parent_Site_ID", + ancestor_key_id="Site_ID", + label="Parent Site ID", + description="Reference to parent site in hierarchy", + sql_data_type="int", + required=False, + order=10 +) + +mgr.save() +``` + +This creates: + +- A new `Parent_Site_ID` field of type `parentKey` +- With `Ancestor_part_ID` pointing to `Site_ID` +- Appearing in the `site` table as a `property` + +### Handling Name Collisions + +If a non-ID field name appears in multiple tables with different meanings (e.g., `Description`, `City`): + +- Create **separate Part_ID entries** with table prefixes +- Examples: `site_City`, `contact_City`, `purpose_Description`, `project_Description` +- Each gets its own part with `table_presence` set for only that table +- Labels can be the same or differentiated: "City", "City", etc. + +```python +from open_dateaubase.data_model.helpers import DictionaryManager + +mgr = DictionaryManager.load("src/dictionary.json") + +# Add site-specific description +mgr.add_field_to_table( + table_id="site", + field_id="site_Description", + label="Description", + description="Description of the site", + role="property", + sql_data_type="nvarchar(500)", + required=False, + order=6 +) + +# Add project-specific description (different content) +mgr.add_field_to_table( + table_id="project", + field_id="project_Description", + label="Description", + description="Description of the project", + role="property", + sql_data_type="nvarchar(500)", + required=False, + order=7 +) + +mgr.save() +``` + +**Exception**: ID fields (`*_ID`) always use the same Part_ID across tables and are tracked via `table_presence`. + +### Regenerating Documentation and SQL + +After editing the dictionary, regenerate all outputs: + +```bash +# Regenerate documentation +uv run python scripts/orchestrate_docs.py + +# Or regenerate specific components +uv run python scripts/generate_dictionary_reference.py dictionary.json docs/reference +uv run python scripts/generate_erd.py dictionary.json docs/reference +uv run python scripts/generate_sql.py dictionary.json sql_generation_scripts mssql +``` + +## Naming Conventions + +The following naming rules apply: + +### Part_IDs + +- **Tables**: Singular nouns, lowercase, words separated by underscores + - Examples: `watershed`, `sampling_point`, `equipment_model`, `weather_condition` + +- **Primary Keys**: `[Table_name]_ID` with capitalized first letters + - Examples: `Watershed_ID`, `Sampling_point_ID`, `Equipment_model_ID` + - Rule: These appear in multiple tables with the same Part_ID + +- **Foreign Keys**: Use the exact same Part_ID as the referenced primary key + - Example: `site` table references `watershed` via `Watershed_ID` + - The dictionary shows this with different roles in `table_presence` + +- **Regular Fields**: Descriptive names, mixed case with underscores + - Examples: `Site_name`, `Street_number`, `Latitude_GPS`, `Purchase_date` + +- **Table-Prefixed Fields**: When non-ID names collide across tables + - Format: `tablename_FieldName` + - Examples: `site_City`, `contact_City`, `purpose_Description` + +### Junction Tables (Many-to-Many) + +- Format: `[table1]_has_[table2]` where both tables are singular +- Examples: `project_has_equipment`, `project_has_contact`, `equipment_model_has_procedure` +- Primary keys are composite (two `compositeKey*` fields) + +### Value Sets and Members + +- **Value Sets**: descriptive name + `Set` or `_set` suffix + - Part_type: `valueSet` + - Examples: `Part_type_set`, `Site_type_set`, `StatusSet` + +- **Members**: short, descriptive identifiers + - Part_type: `valueSetMember` + - Member_of_set_part_ID: points to the set + - Examples: `table`, `key`, `property`, `valueSet`, `valueSetMember`, `active`, `inactive` diff --git a/docs/contributing/parts_table.md b/docs/contributing/parts_table.md deleted file mode 100644 index 6d20de3..0000000 --- a/docs/contributing/parts_table.md +++ /dev/null @@ -1,344 +0,0 @@ -# The Parts Table: A Self-Documenting Database Schema - -## Purpose - -The Parts table (stored as `dictionary.csv`) serves as a comprehensive metadata repository that defines every component of your database model. It acts as a single source of truth from which you can generate SQL schemas, documentation, and entity-relationship diagrams. - -**Key Principle**: Each unique field concept gets exactly ONE row in the dictionary, even if that field appears in multiple tables. The `TableName_present` columns indicate where each field appears and in what role. - -The Parts table is self-referential: it contains the definitions needed to describe itself, making it bootstrapped and internally consistent. - -## Understanding the Structure - -### Core Columns - -Every part has these core metadata columns: - -- **Part_ID**: Unique identifier for this field/table/value -- **Label**: Human-readable name -- **Description**: Detailed explanation of what this part represents -- **Part_type**: Classification (`table`, `key`, `property`, `compositeKeyFirst`, `compositeKeySecond`, `parentKey`, `valueSet`, `valueSetMember`) -- **Value_set_part_ID**: If this property is constrained by a value set, which set -- **Member_of_set_part_ID**: If this is a value set member, which set it belongs to -- **Ancestor_part_ID**: For `parentKey` type, the Part_ID of the ancestor being referenced (enables hierarchical relationships within the same table) -- **SQL_data_type**: SQL data type (e.g., `int`, `nvarchar(100)`, `ntext`) -- **Is_required**: Whether this field is mandatory (NOT NULL) -- **Default_value**: Default value for the field -- **Sort_order**: Display order for documentation/UI - -### Table Presence Columns - -For each table in the database, there are `TableName_present` columns that indicate if and how a field appears in that table: - -- **`key`**: This field is the primary key in this table -- **`compositeKeyFirst`**: First part of a composite primary key -- **`compositeKeySecond`**: Second part of a composite primary key -- **`property`**: This field is a regular column in this table -- **(empty)**: This field does not appear in this table - -**Example**: `Equipment_ID` has a single row with: - -- `equipment_present = key` (primary key in equipment table) -- `metadata_present = property` (foreign key in metadata table) -- `project_has_equipment_present = compositeKeySecond` (part of composite key) - -### Table Metadata Columns - -For tracking additional metadata, each table also has: - -- **TableName_required**: Whether this part is required in that table -- **TableName_order**: Display order of this part in that table - -## Reading the Dictionary - -### Find acceptable values for a field - -To determine what values a field can accept, check if it references a valueSet: - -```sql --- Get the valueSet for a field -SELECT Value_set_part_ID -FROM Parts -WHERE Part_ID = 'Site_type'; -``` - -```sql --- Get all valid values for that set -SELECT Part_ID, Label, Description -FROM Parts -WHERE Member_of_set_part_ID = 'Site_type_set' -ORDER BY Sort_order; -``` - -### Get all columns in a table - -To retrieve all columns that appear in a specific table: - -```sql -SELECT Part_ID, Label, SQL_data_type, Is_required -FROM Parts -WHERE site_present != '' -- Field appears in site table - AND Part_type IN ('key', 'property', 'compositeKeyFirst', 'compositeKeySecond') -ORDER BY site_order; -``` - -Or to see what role each field plays: - -```sql -SELECT Part_ID, Label, site_present AS role_in_site -FROM Parts -WHERE site_present != '' -ORDER BY site_order; -``` - -### Find which tables contain a specific field - -To see all tables where `Equipment_ID` appears: - -```sql -SELECT Part_ID, - CASE WHEN equipment_present != '' THEN 'equipment (' || equipment_present || ')' END, - CASE WHEN metadata_present != '' THEN 'metadata (' || metadata_present || ')' END, - CASE WHEN project_has_equipment_present != '' THEN 'project_has_equipment (' || project_has_equipment_present || ')' END -FROM Parts -WHERE Part_ID = 'Equipment_ID'; -``` - -### Find all primary keys in the database - -```sql -SELECT Part_ID, Label -FROM Parts -WHERE Part_type = 'key' -ORDER BY Part_ID; -``` - -Or to see which table each key belongs to (checking all `_present` columns): - -```sql -SELECT Part_ID, Label, SQL_data_type -FROM Parts -WHERE Part_type = 'key' - AND Parts_present IS NULL -- Exclude Parts table metadata fields -ORDER BY Part_ID; -``` - -### List all tables in the model - -```sql -SELECT Part_ID, Label, Description -FROM Parts -WHERE Part_type = 'table' -ORDER BY Label; -``` - -### Find fields that appear in multiple tables - -```sql --- This query identifies fields (especially ID fields) used across tables --- by counting non-empty _present columns -SELECT Part_ID, Label, Part_type -FROM Parts -WHERE Part_type IN ('key', 'property') - AND ( - -- Count number of tables where this field appears - -- (You'd need to list all _present columns) - (equipment_present != '') + - (metadata_present != '') + - (project_present != '') -- etc. - ) > 1; -``` - -## Editing the Dictionary - -### Adding a New Field to an Existing Table - -1. Check if a field with this name already exists (search for Part_ID) -2. If it exists and is an ID field, just update the appropriate `TableName_present` column -3. If it doesn't exist or is a different concept, add a new row: - - **Part_ID**: Field name (or `TableName_FieldName` if name collision) - - **Label**: Human-readable label - - **Description**: What the field represents - - **Part_type**: Usually `property`, `key` for primary keys - - **SQL_data_type**: The SQL data type - - **Is_required**: TRUE if NOT NULL - - **Sort_order**: Position in table definition - - **TableName_present**: Set to `key`, `property`, `compositeKeyFirst`, or `compositeKeySecond` - -### Adding a New Table - -1. Add a table definition row: - - **Part_ID**: Table name (lowercase with underscores) - - **Label**: Title case version - - **Part_type**: `table` - -2. Add the primary key field (or composite key fields) - -3. Add all property fields, setting `TableName_present` for each - -4. Add `TableName_present`, `TableName_required`, and `TableName_order` columns to the Parts table metadata - -### Handling Name Collisions - -If a non-ID field name appears in multiple tables (e.g., `Description`, `City`): - -- Create **separate Part_ID entries** with table prefixes -- Examples: `site_City`, `contact_City`, `purpose_Description`, `project_Description` -- Each gets its own row with `TableName_present` set for only that table -- Labels should include both parts: "Site City", "Contact City", etc. - -**Exception**: ID fields (`*_ID`) always use the same Part_ID across tables, with multiple `_present` columns filled in. - -### Handling Hierarchical Relationships (parentKey) - -When a table has a hierarchical structure (parent-child within the same table): - -- Create a **new Part_ID** for the parent reference (don't reuse the table's primary key Part_ID) -- Set **Part_type** to `parentKey` -- Set **Ancestor_part_ID** to the Part_ID of the field being referenced (usually the table's primary key) -- The field appears in the table as a regular `property` in the `TableName_present` column - -**Example**: For a hierarchical `site` table where sites can have parent sites: - -```csv -Part_ID,Part_type,Ancestor_part_ID,site_present,... -Site_ID,key,,key,... -Parent_site_ID,parentKey,Site_ID,property,... -``` - -This models: - -- `Site_ID` is the primary key -- `Parent_site_ID` is a semantically different field that references `Site_ID` -- The hierarchical relationship is explicit via `Ancestor_part_ID` - -### Regenerating from SQL Schema - -If you update the SQL schema file, you can regenerate the dictionary: - -```bash -uv run python generate_dictionary.py -``` - -This will create `dictionary_new.csv`. Review it and replace `dictionary.csv` if correct. - -## Naming Conventions - -The following naming rules apply: - -### Part_IDs - -- **Tables**: Singular nouns, lowercase, words separated by underscores - - Examples: `watershed`, `sampling_points`, `equipment_model`, `weather_condition` - -- **Primary Keys**: `[Table_name]_ID` with capitalized first letters - - Examples: `Watershed_ID`, `Sampling_point_ID`, `Equipment_model_ID` - - Rule: These appear in multiple tables with the same Part_ID - -- **Foreign Keys**: Use the exact same Part_ID as the referenced primary key - - Example: `site` table references `watershed` via `Watershed_ID` - - The dictionary shows this with `site_present = property` and `watershed_present = key` - -- **Regular Fields**: Descriptive names, mixed case with underscores - - Examples: `Site_name`, `Street_number`, `Latitude_GPS`, `Purchase_date` - -- **Table-Prefixed Fields**: When non-ID names collide across tables - - Format: `tablename_FieldName` - - Examples: `site_City`, `contact_City`, `purpose_Description` - -### Junction Tables (Many-to-Many) - -- Format: `[table1]_has_[table2]` where both tables are singular -- Examples: `project_has_equipment`, `project_has_contact`, `equipment_model_has_procedures` -- Primary keys are composite (two `compositeKey*` fields) - -### Value Sets and Members - -- **Value Sets**: descriptive name + `_set` suffix - - Part_type: `valueSet` - - Examples: `Part_type_set`, `Site_type_set` - -- **Members**: short, descriptive identifiers - - Part_type: `valueSetMember` - - Member_of_set_part_ID: points to the set - - Examples: `table`, `key`, `property`, `valueSet`, `valueSetMember` - -## Practical Examples - -### Example 1: Understanding Equipment_ID - -The `Equipment_ID` field has ONE row in the dictionary: - -| Column | Value | -|--------|-------| -| Part_ID | Equipment_ID | -| Label | Equipment ID | -| Description | Identifier for equipment, also used in 2 other table(s) | -| Part_type | key | -| SQL_data_type | int | -| equipment_present | key | -| metadata_present | property | -| project_has_equipment_present | compositeKeySecond | -| *(all other _present columns)* | *(empty)* | - -This tells us: - -- Equipment_ID is a primary key (`Part_type = key`) -- It's the primary key in the `equipment` table -- It appears as a foreign key in `metadata` -- It's part of a composite key in `project_has_equipment` - -### Example 2: Understanding Description Fields - -Because `Description` appears in multiple tables with different meanings, there are MULTIPLE rows: - -| Part_ID | Label | purpose_present | project_present | site_present | -|---------|-------|-----------------|-----------------|--------------| -| Description | Description | | | | -| purpose_Description | Purpose Description | property | | | -| project_Description | Project Description | | property | | -| site_Description | Site Description | | | property | - -Note: The plain `Description` is the Parts table's own Description field (with `Parts_present = property`). - -### Example 3: Adding a New Field - -To add a `Latitude` field to the `site` table: - -1. Check if `Latitude` already exists (it doesn't, but `Latitude_GPS` does in `sampling_points`) -2. Since the names are different, add a new row: - - ```csv - Part_ID,Label,Description,Part_type,SQL_data_type,Is_required,Sort_order,site_present - Latitude,Latitude,Latitude coordinate of site,property,real,False,15,property - ``` - -3. Save and validate the dictionary - -### Example 4: Adding an Existing Field to a New Table - -To add `Contact_ID` to a new `project_contact_history` table: - -1. Find the existing `Contact_ID` row -2. Add a new column `project_contact_history_present` -3. Set the value to `property` (or `compositeKeyFirst`/`compositeKeySecond` if it's part of the primary key) -4. No need to create a new row—just update the existing one! - -## Tips for Working with the Dictionary - -1. **Always search before adding**: Use your editor's find function to check if a Part_ID exists -2. **ID fields are shared**: If you see `_ID` at the end, it's likely used across multiple tables -3. **Use table prefixes for collisions**: When the same field name means different things in different tables -4. **Validate after changes**: Run `validate_dictionary.py` to check for duplicates and issues -5. **Keep it synchronized**: If you edit the SQL schema, regenerate the dictionary and merge changes carefully -6. **Document value sets**: When adding enumerations, create both the valueSet and all valueSetMembers - -## Self-Reference: The Parts Table Describes Itself - -The dictionary includes rows that describe its own structure. For example: - -- `Part_ID` (the field) has `Parts_present = key` -- `Label` has `Parts_present = property` -- `equipment_present` (one of the many `_present` columns) has `Parts_present = property` - -This self-referential structure means the dictionary is "bootstrapped"—it fully describes itself using its own format. diff --git a/docs/hooks/call_orchestrator.py b/docs/hooks/call_orchestrator.py new file mode 100644 index 0000000..2a92ab6 --- /dev/null +++ b/docs/hooks/call_orchestrator.py @@ -0,0 +1,52 @@ +import os +from pathlib import Path +from datetime import datetime +from importlib.metadata import version + +package_version = version("open-dateaubase") +TARGET_DBS = ["mssql"] + + +def on_pre_build(config): + """ + MkDocs hook that runs before build process. + Calls the orchestrator script to generate all documentation components. + """ + # Define paths + project_root = Path(config["config_file_path"]).parent + json_path = project_root / "src" / "dictionary.json" + docs_dir = Path(config["docs_dir"]) + output_path = docs_dir / "reference" + sql_path = project_root / "sql_generation_scripts" + assets_path = docs_dir / "assets" + + # Call orchestrator script + scripts_dir = project_root / "scripts" + orchestrator = scripts_dir / "orchestrate_docs.py" + + # Build command + cmd = [ + "uv", + "run", + "python", + str(orchestrator), + str(json_path), + str(output_path), + str(sql_path), + str(assets_path), + ] + + # Add target databases + cmd.extend(TARGET_DBS) + + print(f"Running documentation generation orchestrator...") + print(f"Command: {' '.join(cmd)}") + + # Run orchestrator + result = os.system(" ".join(cmd)) + + if result != 0: + print("Error: Documentation generation failed!") + return + + print("Documentation generation completed successfully!") diff --git a/docs/hooks/generate_docs.py b/docs/hooks/generate_docs.py deleted file mode 100644 index 94e88f4..0000000 --- a/docs/hooks/generate_docs.py +++ /dev/null @@ -1,571 +0,0 @@ -import csv -import os -from pathlib import Path -from datetime import datetime -from importlib.metadata import version - -package_version = version("open-dateaubase") -TARGET_DBS = ["mssql"] - -def on_pre_build(config): - """ - MkDocs hook that runs before the build process. - Reads Parts_table.csv and generates reference.md - """ - # Define paths - project_root = Path(config['config_file_path']).parent - csv_path = project_root / 'src/dictionary.csv' - docs_dir = Path(config['docs_dir']) - output_path = docs_dir / 'reference' - sql_path = project_root / 'sql_generation_scripts' - - # Read and parse CSV - parts_data = parse_parts_table(csv_path) - - # Generate markdown - image = generate_schema_image(parts_data) - tables = generate_tables_markdown(parts_data) - value_sets = generate_value_sets_markdown(parts_data) - - # Generate SQL schema(s) - generate_sql_schemas(parts_data, sql_path, TARGET_DBS) - - # Write to file - # TODO: Write the schema image to a file in the docs/assets directory - (output_path / "tables.md").write_text(tables, encoding='utf-8') - (output_path / "valuesets.md").write_text(value_sets, encoding='utf-8') - print(f"Generated {output_path}") - - - -def generate_sql_schemas(parts_data, path, db_list): - for target_db in db_list: - sql_schema = generate_sql_schema(parts_data, target_db=target_db) - version_str = package_version - filename = f"v{version_str}_as-designed_{target_db}.sql" - (path / filename).write_text(sql_schema, encoding='utf-8') - print(f"Generated SQL schema for {target_db} at {path / filename}") - - -def generate_schema_image(data): - return "Schema image generation not yet implemented!" - -def parse_parts_table(csv_path): - """ - Parse the Parts_table.csv into a structured format. - Returns a dict organized by tables and value sets. - - NEW FORMAT: Uses TableName_present columns instead of Table_part_ID - """ - data = { - 'tables': {}, - 'value_sets': {}, - 'metadata': {}, - 'id_field_locations': {} # Track where ID fields appear for FK detection - } - - with open(csv_path, 'r', encoding='utf-8') as f: - reader = csv.DictReader(f) - # Read all rows to ensure fieldnames is populated - rows = list(reader) - fieldnames = reader.fieldnames or [] - - # Find all *_present columns (excluding Parts_present which is self-reference) - present_columns = [col for col in fieldnames - if col.endswith('_present') and col != 'Parts_present'] - - # Find all *_order columns for sorting - order_columns = {col.replace('_order', ''): col - for col in fieldnames if col.endswith('_order')} - - for row in rows: - part_id = row['Part_ID'] - part_type = row['Part_type'] - - if part_type == 'table': - # Skip Parts table self-reference - if part_id != 'Parts': - data['tables'][part_id] = { - 'label': row['Label'], - 'description': row['Description'], - 'fields': [] - } - - elif part_type in ['key', 'property', 'compositeKeyFirst', 'compositeKeySecond', 'parentKey']: - # Check all *_present columns to see which tables this field appears in - for present_col in present_columns: - table_id = present_col.replace('_present', '') - role = row.get(present_col, '').strip() - - if role: # Field appears in this table - # Ensure table exists - if table_id not in data['tables']: - continue - - # Get sort order for this table - order_col = order_columns.get(table_id, '') - sort_order = int(row.get(order_col, '999')) if row.get(order_col) else 999 - - # Track ID field locations for FK detection - if part_id.endswith('_ID'): - if part_id not in data['id_field_locations']: - data['id_field_locations'][part_id] = {} - data['id_field_locations'][part_id][table_id] = role - - # For parentKey type, fk_to comes from Ancestor_part_ID - fk_to = '' - if part_type == 'parentKey': - fk_to = row.get('Ancestor_part_ID', '') - - field_info = { - 'part_id': part_id, - 'label': row['Label'], - 'description': row['Description'], - 'part_type': role, # Use role from _present column (key, property, etc.) - 'sql_data_type': row['SQL_data_type'], - 'is_required': row['Is_required'] == 'True', - 'default_value': row['Default_value'], - 'fk_to': fk_to, # Set for parentKey, otherwise determined later from ID patterns - 'value_set': row['Value_set_part_ID'], - 'sort_order': sort_order - } - data['tables'][table_id]['fields'].append(field_info) - - elif part_type == 'valueSet': - data['value_sets'][part_id] = { - 'label': row['Label'], - 'description': row['Description'], - 'members': [] - } - - elif part_type == 'valueSetMember': - value_set_id = row['Member_of_set_part_ID'] - if value_set_id and value_set_id in data['value_sets']: - member_info = { - 'part_id': part_id, - 'label': row['Label'], - 'description': row['Description'], - 'sort_order': int(row['Sort_order']) if row['Sort_order'] else 999 - } - data['value_sets'][value_set_id]['members'].append(member_info) - - # Derive foreign key relationships from ID field patterns - # Two cases: - # 1. An ID field that appears as 'key' in one table and 'property' in others is a FK - # 2. A field ending in _ID that references another table's primary key (e.g., TestTable_Parent_ID -> TestTable_ID) - - for id_field, locations in data['id_field_locations'].items(): - # Find the table where this is the primary key - pk_table = None - for table_id, role in locations.items(): - if role == 'key': - pk_table = table_id - break - - if pk_table: - # Mark all other occurrences as foreign keys - for table_id, role in locations.items(): - if table_id != pk_table and role == 'property': - # Find the field in this table and set fk_to - for field in data['tables'][table_id]['fields']: - if field['part_id'] == id_field: - field['fk_to'] = id_field - - # Also detect FK fields that reference other tables by name pattern - # E.g., TestTable_Parent_ID should reference TestTable_ID - for table_id, table_info in data['tables'].items(): - for field in table_info['fields']: - if field['part_id'].endswith('_ID') and not field['fk_to']: - # Try to find a matching primary key - # Extract potential table name from field name - # E.g., "TestTable_Parent_ID" -> look for "TestTable_ID" - parts = field['part_id'].rsplit('_', 1) # Split from right to get [..., 'ID'] - if len(parts) == 2: - prefix = parts[0] # E.g., "TestTable_Parent" - # Look for any table whose PK this might reference - # Check if prefix ends with a table name - for potential_table in data['tables'].keys(): - if prefix.startswith(potential_table + '_'): - # This might be a FK to potential_table - target_pk = potential_table + '_ID' - if target_pk in data['id_field_locations']: - field['fk_to'] = target_pk - break - - # Sort fields and members by sort_order - for table in data['tables'].values(): - table['fields'].sort(key=lambda x: x['sort_order']) - - for value_set in data['value_sets'].values(): - value_set['members'].sort(key=lambda x: x['sort_order']) - - return data - - -def generate_tables_markdown(data): - """ - Generate markdown documentation from parsed data. - """ - md = ["# Database Tables\n"] - md.append("This documentation is auto-generated from the Parts metadata table.\n") - - # Generate table documentation - md.append("\n## Tables\n") - - for table_id, table_info in sorted(data['tables'].items()): - # Anchor as invisible span, table name as regular heading - md.append(f'\n\n') - md.append(f"### {table_info['label']}\n") - md.append(f"{table_info['description']}\n") - - if table_info['fields']: - md.append("\n#### Fields\n") - md.append("| Field | SQL Type | Value Set | Required | Description | Constraints |") - md.append("|-------|----------|-----------|----------|-------------|-------------|") - - for field in table_info['fields']: - field_name = field['label'] - field_id = field['part_id'] - - # SQL Type column - sql_type = field['sql_data_type'] if field['sql_data_type'] else '-' - if field['part_type'] in ['key', 'compositeKeyFirst', 'compositeKeySecond']: - if field['part_type'] == 'key': - sql_type += ' **(PK)**' - elif field['part_type'] == "compositeKeyFirst": - sql_type += ' **(CK-1)**' - elif field['part_type'] == "compositeKeySecond": - sql_type += ' **(CK-2)**' - else: - raise ValueError(f"Found unknown part type: {field['part_type']}. Correct the parts table OR update the documentation generation code.") - - # Value Set column - link to the value set definition - value_set = f"[{field['value_set']}](valuesets.md#{field['value_set']})" if field['value_set'] else '-' - - required = '✓' if field['is_required'] else '' - - # Anchor the description with the Part_ID - description = f'{field["description"]}' - - # Build constraints column - constraints = [] - if field['fk_to']: - # Link to the FK target field - constraints.append(f"FK → [{field['fk_to']}](#{field['fk_to']})") - if field['default_value']: - constraints.append(f"Default: `{field['default_value']}`") - - constraints_str = '
'.join(constraints) if constraints else '-' - - md.append(f"| {field_name} | {sql_type} | {value_set} | {required} | {description} | {constraints_str} |") - - return '\n'.join(md) - - -def generate_value_sets_markdown(data): - """ - Generate value set documentation with proper anchoring. - """ - md = ["# Value Sets\n"] - md.append("Controlled vocabularies used throughout the database.\n") - - if data['value_sets']: - for value_set_id, value_set_info in sorted(data['value_sets'].items()): - # Anchor as invisible span, value set name as regular heading - md.append(f'\n\n') - md.append(f"## {value_set_info['label']}\n") - md.append(f"{value_set_info['description']}\n") - - if value_set_info['members']: - md.append("\n| Value | Description |") - md.append("|-------|-------------|") - - for member in value_set_info['members']: - member_id = member['part_id'] - # Anchor each member with its Part_ID - md.append(f"| `{member_id}` | {member['description']} |") - else: - md.append("No value sets currently appear in the dictionary.") - - return '\n'.join(md) - -def generate_sql_schema(data, target_db='mssql', include_timestamp=True): - """ - Generate SQL CREATE statements from parsed metadata. - - Args: - data: Parsed parts table data - target_db: Target database flavor ('mssql', 'postgres', 'mysql' - future) - include_timestamp: Whether to include generation timestamp (default: True) - - Returns: - SQL DDL as a string - - Raises: - ValueError: If circular foreign key dependencies detected - """ - # Validate no circular FK dependencies - validate_no_circular_fks(data) - - sql = ["-- Auto-generated SQL schema from Parts metadata table"] - sql.append(f"-- Target database: {target_db.upper()}") - if include_timestamp: - sql.append(f"-- Generated: {datetime.now().isoformat()}") - sql.append("\n") - - # Get DB-specific config - db_config = get_db_config(target_db) - - # First pass: Create all tables without foreign keys - for table_id, table_info in sorted(data['tables'].items()): - sql.append(f"\n-- {table_info['description']}") - sql.append(f"CREATE TABLE {db_config['quote'](table_id)} (") - - field_definitions = [] - pk_fields = [] - - for field in table_info['fields']: - field_def = generate_field_definition(field, data, db_config) - field_definitions.append(field_def) - - # Track primary key fields - if field['part_type'] in ['key', 'compositeKeyFirst', 'compositeKeySecond']: - field_name = extract_field_name(field['part_id']) - pk_fields.append(f"{db_config['quote'](field_name)}") - - # Add primary key constraint - if pk_fields: - pk_name = "PK_" + table_id - pk_constraint = f" CONSTRAINT {db_config['quote'](pk_name)} PRIMARY KEY ({', '.join(pk_fields)})" - field_definitions.append(pk_constraint) - - sql.append(",\n".join(field_definitions)) - sql.append(");\n") - - # Second pass: Add foreign key constraints - sql.append("\n-- Foreign Key Constraints\n") - for table_id, table_info in sorted(data['tables'].items()): - for field in table_info['fields']: - if field['fk_to']: - fk_sql = generate_foreign_key_constraint(table_id, field, db_config) - if fk_sql: - sql.append(fk_sql) - - return '\n'.join(sql) - - -def get_db_config(target_db): - """ - Get database-specific configuration. - - Args: - target_db: Database flavor string - - Returns: - Dict with DB-specific settings - """ - configs = { - 'mssql': { - 'quote_char': '[', - 'quote_char_end': ']', - 'type_mappings': { - 'nvarchar': 'nvarchar', - 'ntext': 'nvarchar(max)', # ntext deprecated in modern MSSQL - 'int': 'int', - 'float': 'float', - 'real': 'real', - 'numeric': 'numeric', - 'bit': 'bit' - }, - 'supports_check_constraints': True, - 'supports_deferred_constraints': False - }, - # Future: postgres, mysql, sqlite configs - } - - if target_db not in configs: - raise ValueError(f"Unsupported database: {target_db}. Supported: {list(configs.keys())}") - - config = configs[target_db] - - # Add convenience method for quoting identifiers - if config['quote_char_end']: - config['quote'] = lambda name: f"{config['quote_char']}{name}{config['quote_char_end']}" - else: - config['quote'] = lambda name: f"{config['quote_char']}{name}{config['quote_char']}" - - return config - - -def extract_field_name(part_id): - """ - Extract field name from Part_ID. - - NEW FORMAT handling: - - ID fields (e.g., 'Equipment_ID', 'Project_ID'): Use as-is (these are the actual SQL field names) - - Table-prefixed fields (e.g., 'site_City', 'purpose_Description'): Remove table prefix - - Non-prefixed fields: Use as-is - - Args: - part_id: Part_ID from dictionary - - Returns: - Field name to use in SQL - """ - # ID fields are used as-is in SQL - if part_id.endswith('_ID'): - return part_id - - # Table-prefixed non-ID fields: remove prefix - # Format is lowercase_table_MixedCaseField (e.g., 'site_City', 'contact_City') - if '_' in part_id: - # Check if first part looks like a table name (lowercase) - parts = part_id.split('_', 1) - if len(parts) == 2 and parts[0].islower(): - # This is likely a table-prefixed field, remove prefix - return parts[1] - - # Otherwise use as-is - return part_id - - -def validate_no_circular_fks(data): - """ - Check for circular foreign key dependencies between tables. - - Args: - data: Parsed parts table data - - Raises: - ValueError: If circular FK dependencies found - """ - # Build adjacency list of FK relationships - fk_graph = {table_id: set() for table_id in data['tables']} - - for table_id, table_info in data['tables'].items(): - for field in table_info['fields']: - if field['fk_to'] and '_' in field['fk_to']: - target_table = field['fk_to'].split('_', 1)[0] - if target_table in fk_graph: - fk_graph[table_id].add(target_table) - - # Check for bidirectional relationships (A->B and B->A) - circular_deps = [] - for table_a, targets in fk_graph.items(): - for table_b in targets: - if table_b == table_a: - # self-referential FKs are allowed - continue - if table_a in fk_graph.get(table_b, set()): - # Found circular dependency - pair = tuple(sorted([table_a, table_b])) - if pair not in circular_deps: - circular_deps.append(pair) - - if circular_deps: - error_msg = "Circular foreign key dependencies detected:\n" - for table_a, table_b in circular_deps: - error_msg += f" - {table_a} ↔ {table_b}\n" - error_msg += "\nEach pair of tables has FKs pointing to each other, which creates ambiguity in table creation order." - raise ValueError(error_msg) - - -def generate_field_definition(field, data, db_config): - """ - Generate SQL field definition with constraints. - - Args: - field: Field metadata dict - data: Full parsed data (for value set lookups) - db_config: Database-specific configuration - - Returns: - SQL field definition string - """ - field_name = extract_field_name(field['part_id']) - - quote = db_config['quote'] - - parts = [f" {quote(field_name)}"] - - # Data type with mapping - sql_type = field['sql_data_type'] if field['sql_data_type'] else 'nvarchar(255)' - # Apply type mapping for target DB - base_type = sql_type.split('(')[0] # Extract base type (e.g., 'nvarchar' from 'nvarchar(255)') - if base_type in db_config['type_mappings']: - # Preserve parameters if they exist - if '(' in sql_type: - params = sql_type[sql_type.index('('):] - sql_type = db_config['type_mappings'][base_type].split('(')[0] + params - else: - sql_type = db_config['type_mappings'][base_type] - - parts.append(sql_type) - - # NULL constraint - if field['is_required']: - parts.append("NOT NULL") - else: - parts.append("NULL") - - # Default value - if field['default_value']: - default_val = field['default_value'] - # Handle boolean defaults - if default_val in ['True', 'False']: - default_val = '1' if default_val == 'True' else '0' - # Handle numeric vs string defaults - if field['sql_data_type'] and field['sql_data_type'].split('(')[0] in ['int', 'float', 'real', 'numeric', 'bit']: - parts.append(f"DEFAULT {default_val}") - else: - parts.append(f"DEFAULT '{default_val}'") - - # Note: Value set CHECK constraints removed per requirement #3 - # Future: could add back conditionally based on target_db config - - return ' '.join(parts) - - -def generate_foreign_key_constraint(table_id, field, db_config): - """ - Generate ALTER TABLE statement for foreign key. - - NEW FORMAT: fk_to is the Part_ID of the target field (e.g., 'TestTable_ID') - We need to find which table has this field as a primary key. - - Args: - table_id: Source table ID - field: Field metadata with FK reference - db_config: Database-specific configuration - - Returns: - SQL ALTER TABLE statement or None - """ - if not field['fk_to']: - return None - - # fk_to is the Part_ID of the target (e.g., 'TestTable_ID') - # For ID fields, this is the actual field name - # We need to determine the target table from the naming - fk_target = field['fk_to'] - source_field = extract_field_name(field['part_id']) - - # For ID fields like 'TestTable_ID', the target table is the part before '_ID' - if fk_target.endswith('_ID'): - target_field = fk_target # e.g., 'TestTable_ID' - # Extract table name (everything before '_ID') - target_table = fk_target[:-3] # Remove '_ID' to get 'TestTable' - else: - # Non-ID FK (shouldn't happen in new format, but fallback) - return None - - quote = db_config['quote'] - constraint_name = f"FK_{table_id}_{source_field}" - - sql = f"""ALTER TABLE {quote(table_id)} - ADD CONSTRAINT {quote(constraint_name)} - FOREIGN KEY ({quote(source_field)}) - REFERENCES {quote(target_table)} ({quote(target_field)}); -""" - - return sql \ No newline at end of file diff --git a/docs/reference/erd.md b/docs/reference/erd.md new file mode 100644 index 0000000..809f346 --- /dev/null +++ b/docs/reference/erd.md @@ -0,0 +1,36 @@ +# Entity Relationship Diagram (ERD) + +This interactive diagram shows all tables and their relationships in datEAUbase schema. + +## Interactive ERD + +The interactive version allows you to: +- 🖱️ **Drag tables** to rearrange layout +- 🔍 **Zoom in/out** for better visibility +- 📐 **Auto-layout** to reorganize tables automatically +- 💾 **Export** diagram as PNG + + + +[Open in new window](../assets/erd_interactive.html){: target="_blank" .md-button .md-button--primary} + +## Legend + +### Field Markers +- **PK** badge: Primary Key - Unique identifier for each record +- **FK** badge: Foreign Key - Reference to another table's primary key +- **\*** Required field (NOT NULL) + +### Relationship Notation +Relationships use standard crow's foot notation: +- **Single line (|)**: "One" side of relationship +- **Crow's foot (⟨)**: "Many" side of relationship + +**Relationship Types:** +- **One-to-One**: Single line on both ends (e.g., watershed ↔ hydrological_characteristics) +- **One-to-Many**: Crow's foot on child side, single line on parent (e.g., site ↔ sampling_points) +- **Many-to-Many**: Crow's foot on both ends (via junction tables like project_has_contact) + +## Table Count + +The current schema contains **23** tables with **27** relationships. diff --git a/docs/reference/schema.md b/docs/reference/schema.md index e3ca853..71a906b 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -1,170 +1,77 @@ -Schema generation not yet implemented! +# datEAUbase Schema Documentation. +## 1. Overview -# 📘 datEAUbase Schema Documentation (AS-IS 2025) +### 1.1 Purpose -> **Version :** 2025-09-12 -> **Auteur :** Lala (documentation interne – datEAUbase) -> **Source :** Schéma Lucidchart “datEAUbase_AS-IS_2025.pdf” -> **Contexte :** Base de données centrale du SI pilEAUte / datEAUbase, interconnectée avec FactoryTalk, API Python et MQTT pour la gestion, l’ingestion et la validation de données hydrologiques, environnementales et opérationnelles. +datEAUbase is a relational database designed to: ---- - -## 1. Conventions et domaines fonctionnels - -| Couleur | Domaine | Description | -|----------|----------|-------------| -| 🟩 Vert | **Géospatiale et environnement** | Sites, bassins versants, caractéristiques urbaines et hydrologiques | -| 🟧 Orange | **Métadonnées et valeurs** | Données scientifiques et de mesure | -| 🟪 Rose | **Instrumentation & procédures** | Équipements, modèles, paramètres et procédures associées | -| 🟨 Jaune | **Projets & liaisons** | Relations projet-équipement-contact-points | -| 🟦 Bleu | **Référentiels de support** | Unités, statuts, types, sources et opérations | -| ⚙️ Gris | **Systèmes & contrôle** | Boucles de régulation, synchronisation, historisation | - ---- - -## 2. Structure générale et dépendances - -```text -value ─┬──▶ metadata ─┬──▶ parameter - │ ├──▶ equipment - │ ├──▶ project - │ ├──▶ sampling_points ─▶ site ─▶ watershed - │ ├──▶ purpose - │ ├──▶ condition (weather_condition) - │ └──▶ contact - │ - └──▶ comment -``` - -Relations secondaires : -- `equipment_model` ←→ `parameter` via `equipment_model_has_specification` -- `equipment_model` ←→ `procedures` via `equipment_model_has_procedures` -- `parameter` ←→ `procedures` via `parameter_has_procedures` -- `project` ←→ (`equipment`, `contact`, `sampling_points`) via tables d’association -- `source`, `operations`, `type_data`, `status` : nouveaux référentiels pour ingestion et contrôle qualité -- `control_loop` : lie `measurement`, `controller` et `actuator` - ---- - -## 3. Détail des domaines - -### 3.1 Métadonnées et valeurs - -| Table | Description | Clés | Relations | -|-------|--------------|------|------------| -| **value** | Données brutes et validées (mesures, résultats d’expériences, etc.) | `Value_ID (PK)` | `Metadata_ID → metadata`, `Comment_ID → comments` | -| **metadata** | Contexte complet d’une valeur : paramètre, unité, site, équipement, projet, condition météo, etc. | `Metadata_ID (PK)` | FK vers `parameter`, `unit`, `equipment`, `contact`, `project`, `sampling_points`, `weather_condition`, `purpose`, `type_data`, `source`, `status` | -| **purpose** | Objectif de la donnée (ex. suivi, calibration, simulation) | `Purpose_ID (PK)` | 1-N avec `metadata` | -| **unit** | Référentiel d’unités (mg/L, m³/s, °C…) | `Unit_ID (PK)` | Référencée par `parameter`, `metadata`, `equipment_model_has_specification` | -| **comments** | Notes descriptives ou remarques sur une valeur | `Comment_ID (PK)` | 1-N avec `value` | -| **status** | Référentiel qualité (raw, flagged, validated, replaced, rejected) | `Status_ID (PK)` | FK depuis `metadata` ou `value` | -| **type_data** | Catégorisation du type d’enregistrement (measurement, laboratory, control_signal…) | `Type_ID (PK)` | FK depuis `metadata` | - ---- - -### 3.2 Instrumentation et procédures +- Centralize water quality data from multiple sources (online sensors, laboratories, manual observations) +- Document measurements with comprehensive metadata (who, what, where, when, how, why) +- Ensure data traceability from physical sensor to final storage +- Keep track of data as it gets processed to improve its quality. +- Maintain historical records of equipment usage, research projects, and site evolution -| Table | Description | Clés | Relations | -|-------|--------------|------|------------| -| **equipment_model** | Modèle d’équipement (méthode, fonctions, fabricant, manuels) | `Equipment_model_ID (PK)` | Liée à `equipment`, `parameter`, `procedures` | -| **equipment** | Équipement individuel (identifiant, numéro de série, propriétaire, date d’achat, mise en service) | `Equipment_ID (PK)` | FK `Equipment_model_ID` | -| **parameter** | Variable mesurée (température, NH₄, débit, etc.) avec unité et description | `Parameter_ID (PK)` | FK `Unit_ID` | -| **procedures** | Procédures opératoires ou de maintenance | `Procedure_ID (PK)` | liées à `parameter` et `equipment_model` | -| **equipment_model_has_specification** | Table de correspondance (remplace l’ancienne `equipment_model_has_parameter`) | `Equipment_model_ID`, `Parameter_ID` (CK) | inclut champs `Range_min`, `Range_max`, `Resolution`, `Unit_ID` | -| **parameter_has_procedures** | Relation N-N entre paramètres et procédures | `Parameter_ID`, `Procedure_ID` (CK) | -| **equipment_model_has_procedures** | Relation N-N entre modèles et procédures | `Equipment_model_ID`, `Procedure_ID` (CK) | +## 2. Functional Domains ---- - -### 3.3 Référentiels d’ingestion et d’opérations - -| Table | Description | Clés | Relations | -|-------|--------------|------|------------| -| **source** | Provenance du signal ou des fichiers (MQTT, API, OPC, CSV, manuel) | `Source_ID (PK)` | FK depuis `metadata` | -| **operations** | Seuils et paramètres opérationnels (NO3_min, NO3_max, alarmes) | `Operation_ID (PK)` | reliée à `source` | -| **syncdiagrams**, **maxtimestamp** | Tables internes de synchronisation et historique de timestamps | `AK`, `PK` divers | utilisées pour ingestion automatisée | -| **holiday** | Gestion des jours fériés pour planification | `Message_ID (PK)` | sans dépendances externes | - ---- - -### 3.4 Domaine géospatial et environnemental - -| Table | Description | Clés | Relations | -|-------|--------------|------|------------| -| **site** | Localisation physique d’un échantillonnage (adresse, ville, pays, type) | `Site_ID (PK)` | FK `Watershed_ID` | -| **sampling_points** | Points d’échantillonnage liés à un site, avec GPS et photos | `Sampling_point_ID (PK)` | FK `Site_ID` | -| **watershed** | Bassin versant associé au site | `Watershed_ID (PK)` | 1-N vers `site` | -| **urban_characteristics** | Surfaces urbaines, industrielles, agricoles, etc. | `Watershed_ID (FK)` | 1-1 avec `watershed` | -| **hydrological_characteristics** | Données hydrologiques détaillées (zones humides, forêts, prairies) | `Watershed_ID (FK)` | 1-1 avec `watershed` | -| **weather_condition** | Conditions météorologiques observées | `Condition_ID (PK)` | FK depuis `metadata` | - ---- +The database is organized into several color-coded functional domains: -### 3.5 Projets et associations - -| Table | Description | Clés | Relations | -|-------|--------------|------|------------| -| **project** | Projet de recherche ou d’exploitation lié à des sites et instruments | `Project_ID (PK)` | central | -| **project_has_equipment** | N-N entre projet et équipement | `(Project_ID, Equipment_ID)` (CK) | -| **project_has_contact** | N-N entre projet et contact | `(Project_ID, Contact_ID)` (CK) | -| **project_has_sampling_points** | N-N entre projet et points d’échantillonnage | `(Project_ID, Sampling_point_ID)` (CK) | -| **equipment_has_sampling_points** | N-N entre équipement et points d’échantillonnage | `(Equipment_ID, Sampling_point_ID)` (CK) | -| **contact** | Informations sur les personnes et organisations liées aux projets | `Contact_ID (PK)` | partagée entre projets, métadonnées, équipement_model | +| Domain | Color | Tables | Description | +|--------|-------|--------|-------------| +| **Metadata and Values** 3 | Core measurement data and context | +| **Instrumentation & Procedures**| 9 | Equipment, models, parameters, and SOPs | +| **Geospatial & Environmental** | 5 | Sites, watersheds, land use characteristics | +| **Projects & Associations** | 8 | Research projects and contacts | --- -### 3.6 Contrôle, automatisation et validation +## 3. Database Structure -| Table | Description | Clés | Relations | -|-------|--------------|------|------------| -| **control_loop** | Décrit les boucles de régulation automatiques (capteur-contrôleur-actionneur) | `Measurement (FK)`, `Controller (FK)`, `Actuator (FK)` | intégrée avec les flux en temps réel | -| **value_before_12_04_2025**, **value_test_hedi** | Tables d’historisation ou de test (migration & validation) | `Value_ID (PK)` | même structure que `value` | +### 3.1 Core Data Flow ---- +```text +value ──▶ metadata (Central Hub linking the value with their specific context) + │ │ + │ ├──▶ parameter ──▶ unit + │ ├──▶ equipment ──▶ equipment_model + │ ├──▶ sampling_points ──▶ site ──▶ watershed + │ ├──▶ contact + │ ├──▶ project + │ ├──▶ purpose + │ ├──▶ procedures + │ ├──▶ weather_condition + │ ├──▶ type_data + │ ├──▶ status + │ ├──▶ operations + │ └──▶ source + │ + └──▶ comments +``` -## 4. Contraintes clés et intégrité référentielle +### 3.2 Key Relationships -- **PK :** toutes les tables principales utilisent un `INT` auto-increment (SQL Server IDENTITY). -- **FK :** contraints en cascade `ON UPDATE CASCADE` / `ON DELETE NO ACTION` pour la plupart. -- **CK :** relations N-N avec `compositeKeyFirst`, `compositeKeySecond`. -- **Indexes :** `IX_Metadata_Parameter`, `IX_Value_Timestamp`, `IX_Site_Watershed`. -- **FK notables :** - - `value.Metadata_ID → metadata.Metadata_ID` - - `metadata.Parameter_ID → parameter.Parameter_ID` - - `metadata.Equipment_ID → equipment.Equipment_ID` - - `equipment.Equipment_model_ID → equipment_model.Equipment_model_ID` - - `site.Watershed_ID → watershed.Watershed_ID` + +**One-to-One (1:1)** +- watershed ↔ urban_characteristics +- watershed ↔ hydrological_characteristics ---- +**One-to-Many (1:N)** +- equipment_model → equipment +- site → sampling_points +- project → metadata -## 5. Interconnexions externes (AS-IS) - -| Source | Type | Description | -|--------|------|-------------| -| **FactoryTalk Historian** | OPC/CSV | Extraction automatique vers table `source` | -| **Python API (pilEAUte)** | REST | Insertion contrôlée vers `value` et `metadata` | -| **MQTT Broker** | Temps réel | Publication de `value` vers `control_loop` | -| **Grafana** | Visualisation | Lecture sur `value`, `metadata`, `status` | -| **Power BI / CSV Export** | Reporting | Requêtes consolidées multi-projets | +**Many-to-Many (M:N) via junction tables** +- equipment_model ↔ parameter (via equipment_model_has_specification) +- equipment_model ↔ procedures (via equipment_model_has_procedures) +- parameter ↔ procedures (via parameter_has_procedures) +- project ↔ equipment (via project_has_equipment) +- project ↔ contact (via project_has_contact) +- project ↔ sampling_points (via project_has_sampling_points) +- equipment ↔ sampling_points (via equipment_has_sampling_points) --- -## 6. Évolution prévisible (TO-BE 2025+) - -- Uniformisation du modèle vers une architecture **Docker + PostgreSQL + API REST** -- Ajout d’une couche **data lineage** (audit, tracking de corrections) -- Simplification des relations N-N (`project_has_*`) via vues logiques -- Dépréciation des tables `value_before_12_04_2025` et `value_test_hedi` -- Extension du domaine “operations” vers les boucles de contrôle prédictives (IA embarquée) - ---- -## 7. Références croisées +### 4. Key Publications -| Fichier | Usage | -|----------|-------| -| `tables.md` | Détail des champs, types SQL, descriptions | -| `valuesets.md` | Vocabulaire contrôlé (status, type_data, source_protocol, etc.) | -| `schema.md` | Vue d’ensemble du modèle relationnel | -| `architecture.md` *(à venir)* | Flux de données et interconnexions (API, MQTT, Historian) | +1. Plana, Q., et al. (2018). "Towards a water quality database for raw and validated data with emphasis on structured metadata." *Water Quality Research Journal*, 54(1), 1-9. diff --git a/docs/reference/tables.md b/docs/reference/tables.md index 449eca1..327198c 100644 --- a/docs/reference/tables.md +++ b/docs/reference/tables.md @@ -1,6 +1,6 @@ # Database Tables -This documentation is auto-generated from the Parts metadata table. +This documentation is auto-generated from dictionary.json. ## Tables @@ -10,395 +10,395 @@ This documentation is auto-generated from the Parts metadata table. ### Comments -Table for Comments +Stores any additional textual comments, notes, or observations related to a specific measured value #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Comment | ntext(1073741823) | - | | Comment in comments table | - | -| Comment ID | int **(PK)** | - | | Identifier for comments, also used in 1 other table(s) | - | +| Comment | ntext(1073741823) | - | | Comment on the data in the Value table | - | +| Comment ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | ### Contact -Table for Contact +Stores detailed personal and professional information for people involved in projects (e.g., name, affiliation, function, e-mail, phone) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Company | ntext(1073741823) | - | | Company in contact table | - | -| Contact ID | int **(PK)** | - | | Identifier for contact, also used in 2 other table(s) | - | -| Email | nvarchar(100) | - | | Email in contact table | - | -| First Name | nvarchar(255) | - | | First Name in contact table | - | -| Function | ntext(1073741823) | - | | Function in contact table | - | -| Last Name | nvarchar(100) | - | | Last Name in contact table | - | -| Linkedin | nvarchar(100) | - | | Linkedin in contact table | - | -| Office Number | nvarchar(100) | - | | Office Number in contact table | - | -| Phone | nvarchar(100) | - | | Phone in contact table | - | -| Skype Name | nvarchar(100) | - | | Skype Name in contact table | - | -| Status | nvarchar(255) | - | | Status in contact table | - | -| Website | nvarchar(60) | - | | Website in contact table | - | -| Contact City | nvarchar(255) | - | | Contact City in contact table | - | -| Contact Country | nvarchar(255) | - | | Contact Country in contact table | - | -| Contact Street Name | nvarchar(100) | - | | Contact Street Name in contact table | - | -| Contact Street Number | nvarchar(100) | - | | Contact Street Number in contact table | - | -| Contact Zip Code | nvarchar(45) | - | | Contact Zip Code in contact table | - | +| Company | ntext(1073741823) | - | | Company name | - | +| Contact ID | int **(PK)** | - | | Link to the Contact table | - | +| Email | nvarchar(100) | - | | E-mail address | - | +| First Name | nvarchar(255) | - | | First name of the contact | - | +| Function | ntext(1073741823) | - | | More detailed description about the functions | - | +| Last Name | nvarchar(100) | - | | Last name of the contact | - | +| Linkedin | nvarchar(100) | - | | LinkedIn account | - | +| Office Number | nvarchar(100) | - | | Number of the office | - | +| Phone | nvarchar(100) | - | | Phone number | - | +| Skype Name | nvarchar(100) | - | | Skype name | - | +| Status | nvarchar(255) | - | | Status of the person. For example: "Master student", "Postdoc" or "Intern" | - | +| Website | nvarchar(60) | - | | Website URL of the contact or organization | - | +| Contact City | nvarchar(255) | - | | Address: name of the city | - | +| Contact Country | nvarchar(255) | - | | Address: name of the country | - | +| Contact Street Name | nvarchar(100) | - | | Address: name of the street | - | +| Contact Street Number | nvarchar(100) | - | | Address: number of the street | - | +| Contact Zip Code | nvarchar(45) | - | | Address: zip code | - | ### Equipment -Table for Equipment +Stores information about a specific, physical piece of equipment (e.g., serial number, owner, purchase date, storage location) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Equipment ID | int **(PK)** | - | | Identifier for equipment, also used in 2 other table(s) | - | -| Equipment IDentifier | nvarchar(100) | - | | Equipment IDentifier in equipment table | - | -| Equipment Model ID | int | - | | Identifier for equipment_model, also used in 3 other table(s) | FK → [Equipment_model_ID](#Equipment_model_ID) | -| Owner | ntext(1073741823) | - | | Owner in equipment table | - | -| Purchase Date | date | - | | Purchase Date in equipment table | - | -| Serial Number | nvarchar(100) | - | | Serial Number in equipment table | - | -| Storage Location | nvarchar(100) | - | | Storage Location in equipment table | - | +| Equipment ID | int **(PK)** | - | | Link to the Equipment table | - | +| Equipment IDentifier | nvarchar(100) | - | | Identification name of the equipments | - | +| Equipment Model ID | int | - | | Link to the Equipment model table | FK → [Equipment_model_ID](#Equipment_model_ID) | +| Owner | ntext(1073741823) | - | | Name of the owner of the equipment | - | +| Purchase Date | date | - | | Date when the equipment was bought: 'YYYY-MM-DD | - | +| Serial Number | nvarchar(100) | - | | Serial number of the equipment | - | +| Storage Location | nvarchar(100) | - | | Where is the procedure stored | - | ### Equipment Model -Table for Equipment Model +Stores detailed, non-redundant specifications for a specific sensor or instrument model (e.g., manufacturer, functions, method) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Equipment Model | nvarchar(100) | - | | Equipment Model in equipment_model table | - | -| Equipment Model ID | int **(PK)** | - | | Identifier for equipment_model, also used in 3 other table(s) | - | -| Functions | ntext(1073741823) | - | | Functions in equipment_model table | - | -| Manual Location | nvarchar(100) | - | | Manual Location in equipment_model table | - | -| Manufacturer | nvarchar(100) | - | | Manufacturer in equipment_model table | - | -| Method | nvarchar(100) | - | | Method in equipment_model table | - | +| Equipment Model | nvarchar(100) | - | | Name of the equipment model. For example: ammo::lyser | - | +| Equipment Model ID | int **(PK)** | - | | Link to the Equipment model table | - | +| Functions | ntext(1073741823) | - | | Description of the functions of the equipment | - | +| Manual Location | nvarchar(100) | - | | Location where the manual is stored | - | +| Manufacturer | nvarchar(100) | - | | Name of the manufacturer | - | +| Method | nvarchar(100) | - | | Method behind the equipment | - | ### Equipment Model Has Parameter -Table for Equipment Model Has Parameter +Links equipment models to the parameters they can measure #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Equipment Model ID | int **(CK-1)** | - | | Identifier for equipment_model, also used in 3 other table(s) | - | -| Parameter ID | int **(CK-2)** | - | | Identifier for parameter, also used in 3 other table(s) | - | +| Equipment Model ID | int **(CK-1)** | - | | Link to the Equipment model table | FK → [Equipment_model_ID](#Equipment_model_ID) | +| Parameter ID | int **(CK-2)** | - | | Link to the Parameter table | - | ### Equipment Model Has Procedures -Table for Equipment Model Has Procedures +Links equipment models to the relevant maintenance procedures #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Equipment Model ID | int **(CK-1)** | - | | Identifier for equipment_model, also used in 3 other table(s) | - | -| Procedure ID | int **(CK-2)** | - | | Identifier for procedures, also used in 3 other table(s) | - | +| Equipment Model ID | int **(CK-1)** | - | | Link to the Equipment model table | FK → [Equipment_model_ID](#Equipment_model_ID) | +| Procedure ID | int **(CK-2)** | - | | Link to the Procedures table | FK → [Procedure_ID](#Procedure_ID) | ### Hydrological Characteristics -Table for Hydrological Characteristics +Stores the hydrological land use percentages (e.g., forest, wetlands, cropland, grassland) within the watershed #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Cropland | real | - | | Cropland in hydrological_characteristics table | - | -| Forest | real | - | | Forest in hydrological_characteristics table | - | -| Grassland | real | - | | Grassland in hydrological_characteristics table | - | -| Meadow | real | - | | Meadow in hydrological_characteristics table | - | -| Urban Area | real | - | | Urban Area in hydrological_characteristics table | - | -| Watershed ID | int **(PK)** | - | | Identifier for hydrological_characteristics, also used in 3 other table(s) | - | -| Wetlands | real | - | | Wetlands in hydrological_characteristics table | - | +| Cropland | real | - | | Percentage [%] of croplands | - | +| Forest | real | - | | Percentage [%] of forest areas | - | +| Grassland | real | - | | Percentage [%] of grasslands | - | +| Meadow | real | - | | Percentage [%] of meadow areas | - | +| Urban Area | real | - | | Percentage [%] of urban areas | - | +| Watershed ID | int **(PK)** | - | | Linked to the Watershed table | FK → [Watershed_ID](#Watershed_ID) | +| Wetlands | real | - | | Percentage [%] of wetlands | - | ### Metadata -Table for Metadata +Contains a list of all existing unique metadata combinations (represented by a series of foreign keys/IDs) that describe a single measurement #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Condition ID | int | - | | Identifier for weather_condition, also used in 1 other table(s) | FK → [Condition_ID](#Condition_ID) | -| Contact ID | int | - | | Identifier for contact, also used in 2 other table(s) | FK → [Contact_ID](#Contact_ID) | -| Equipment ID | int | - | | Identifier for equipment, also used in 2 other table(s) | FK → [Equipment_ID](#Equipment_ID) | -| Metadata ID | int **(PK)** | - | | Identifier for metadata, also used in 1 other table(s) | - | -| Parameter ID | int | - | | Identifier for parameter, also used in 3 other table(s) | FK → [Parameter_ID](#Parameter_ID) | -| Procedure ID | int | - | | Identifier for procedures, also used in 3 other table(s) | FK → [Procedure_ID](#Procedure_ID) | -| Project ID | int | - | | Identifier for project, also used in 4 other table(s) | FK → [Project_ID](#Project_ID) | -| Purpose ID | int | - | | Identifier for purpose, also used in 1 other table(s) | FK → [Purpose_ID](#Purpose_ID) | -| Sampling Point ID | int | - | | Identifier for sampling_points, also used in 2 other table(s) | FK → [Sampling_point_ID](#Sampling_point_ID) | -| Unit ID | int | - | | Identifier for unit, also used in 2 other table(s) | FK → [Unit_ID](#Unit_ID) | +| Condition ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Condition_ID](#Condition_ID) | +| Contact ID | int | - | | Link to the Contact table | FK → [Contact_ID](#Contact_ID) | +| Equipment ID | int | - | | Link to the Equipment table | FK → [Equipment_ID](#Equipment_ID) | +| Metadata ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | +| Parameter ID | int | - | | Link to the Parameter table | FK → [Parameter_ID](#Parameter_ID) | +| Procedure ID | int | - | | Link to the Procedures table | FK → [Procedure_ID](#Procedure_ID) | +| Project ID | int | - | | Link to the Project table | FK → [Project_ID](#Project_ID) | +| Purpose ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Purpose_ID](#Purpose_ID) | +| Sampling Point ID | int | - | | Link to the Sampling_point table | FK → [Sampling_point_ID](#Sampling_point_ID) | +| Unit ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Unit_ID](#Unit_ID) | ### Parameter -Table for Parameter +Stores the different water quality or quantity parameters that are measured (e.g., pH, TSS, N-components) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Parameter | nvarchar(100) | - | | Parameter in parameter table | - | -| Parameter ID | int **(PK)** | - | | Identifier for parameter, also used in 3 other table(s) | - | -| Unit ID | int | - | | Identifier for unit, also used in 2 other table(s) | FK → [Unit_ID](#Unit_ID) | -| Parameter Description | ntext(1073741823) | - | | Parameter Description in parameter table | - | +| Parameter | nvarchar(100) | - | | Name of the parameter | - | +| Parameter ID | int **(PK)** | - | | Link to the Parameter table | - | +| Unit ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Unit_ID](#Unit_ID) | +| Parameter Description | ntext(1073741823) | - | | Description of the parameter | - | ### Parameter Has Procedures -Table for Parameter Has Procedures +Links parameters to the relevant measurement procedures #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Parameter ID | int **(CK-1)** | - | | Identifier for parameter, also used in 3 other table(s) | - | -| Procedure ID | int **(CK-2)** | - | | Identifier for procedures, also used in 3 other table(s) | - | +| Parameter ID | int **(CK-1)** | - | | Link to the Parameter table | - | +| Procedure ID | int **(CK-2)** | - | | Link to the Procedures table | FK → [Procedure_ID](#Procedure_ID) | ### Procedures -Table for Procedures +Stores details for different measurement procedures (e.g., calibration, validation, standard operating procedures, ISO methods) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Procedure ID | int **(PK)** | - | | Identifier for procedures, also used in 3 other table(s) | - | -| Procedure Location | nvarchar(100) | - | | Procedure Location in procedures table | - | -| Procedure Name | nvarchar(100) | - | | Procedure Name in procedures table | - | -| Procedure Type | nvarchar(255) | - | | Procedure Type in procedures table | - | -| Procedures Description | ntext(1073741823) | - | | Procedures Description in procedures table | - | +| Procedure ID | int **(PK)** | - | | Link to the Procedures table | - | +| Procedure Location | nvarchar(100) | - | | Where is the procedure stored | - | +| Procedure Name | nvarchar(100) | - | | Title name of the procedure | - | +| Procedure Type | nvarchar(255) | - | | Type of the procedure. For example, SOP | - | +| Procedures Description | ntext(1073741823) | - | | Description of the procedure | - | ### Project -Table for Project +Stores descriptive information about the research or monitoring project for which the data was collected #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Project ID | int **(PK)** | - | | Identifier for project, also used in 4 other table(s) | - | -| Project Name | nvarchar(100) | - | | Project Name in project table | - | -| Project Description | ntext(1073741823) | - | | Project Description in project table | - | +| Project ID | int **(PK)** | - | | Link to the Project table | - | +| Project Name | nvarchar(100) | - | | Name of the project | - | +| Project Description | ntext(1073741823) | - | | Description of the project | - | ### Project Has Contact -Table for Project Has Contact +Links projects to the personnel involved in them #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Contact ID | int **(CK-2)** | - | | Identifier for contact, also used in 2 other table(s) | - | -| Project ID | int **(CK-1)** | - | | Identifier for project, also used in 4 other table(s) | - | +| Contact ID | int **(CK-2)** | - | | Link to the Contact table | FK → [Contact_ID](#Contact_ID) | +| Project ID | int **(CK-1)** | - | | Link to the Project table | FK → [Project_ID](#Project_ID) | ### Project Has Equipment -Table for Project Has Equipment +Links projects to the specific equipment used within them #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Equipment ID | int **(CK-2)** | - | | Identifier for equipment, also used in 2 other table(s) | - | -| Project ID | int **(CK-1)** | - | | Identifier for project, also used in 4 other table(s) | - | +| Equipment ID | int **(CK-2)** | - | | Link to the Equipment table | FK → [Equipment_ID](#Equipment_ID) | +| Project ID | int **(CK-1)** | - | | Link to the Project table | FK → [Project_ID](#Project_ID) | ### Project Has Sampling Points -Table for Project Has Sampling Points +Links projects to the sampling points used within them #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Project ID | int **(CK-1)** | - | | Identifier for project, also used in 4 other table(s) | - | -| Sampling Point ID | int **(CK-2)** | - | | Identifier for sampling_points, also used in 2 other table(s) | - | +| Project ID | int **(CK-1)** | - | | Link to the Project table | FK → [Project_ID](#Project_ID) | +| Sampling Point ID | int **(CK-2)** | - | | Link to the Sampling_point table | FK → [Sampling_point_ID](#Sampling_point_ID) | ### Purpose -Table for Purpose +Stores information about the aim of the measurement (e.g., on-line measurement, laboratory analysis, calibration, validation, cleaning) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Purpose | nvarchar(100) | - | | Purpose in purpose table | - | -| Purpose ID | int **(PK)** | - | | Identifier for purpose, also used in 1 other table(s) | - | -| Purpose Description | ntext(1073741823) | - | | Purpose Description in purpose table | - | +| Purpose | nvarchar(100) | - | | Purpose of the data collection. For example, "Measurement", "Lab_analysis", "Calibration" and "Cleaning" | - | +| Purpose ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | +| Purpose Description | ntext(1073741823) | - | | Description of the purpose | - | ### Sampling Points -Table for Sampling Points +Stores the identification, specific geographical coordinates (Latitude/Longitude/GPS), and description of a particular spot where a sample or measurement is taken #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Latitude GPS | nvarchar(100) | - | | Latitude GPS in sampling_points table | - | -| Longitude GPS | nvarchar(100) | - | | Longitude GPS in sampling_points table | - | -| Pictures | BLOB | - | | Pictures in sampling_points table | - | -| Sampling Location | nvarchar(100) | - | | Sampling Location in sampling_points table | - | -| Sampling Point | nvarchar(100) | - | | Sampling Point in sampling_points table | - | -| Sampling Point ID | int **(PK)** | - | | Identifier for sampling_points, also used in 2 other table(s) | - | -| Site ID | int | - | | Identifier for site, also used in 1 other table(s) | FK → [Site_ID](#Site_ID) | -| Sampling Points Description | ntext(1073741823) | - | | Sampling Points Description in sampling_points table | - | +| Latitude GPS | nvarchar(100) | - | | GPS coordinates. For example: 47°54′25.103" | - | +| Longitude GPS | nvarchar(100) | - | | GPS coordinates. For example: $73^{\circ}47^{\prime}00.024^{\prime\prime}$ | - | +| Pictures | BLOB | - | | Picture of the site | - | +| Sampling Location | nvarchar(100) | - | | Where the sample was taken. For example: "Biofiltration", "Sewer 01" or "Retention Tank" | - | +| Sampling Point | nvarchar(100) | - | | Where the sample was taken. For example: "Inlet", "Outlet" or "Upstream" | - | +| Sampling Point ID | int **(PK)** | - | | Link to the Sampling_point table | - | +| Site ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Site_ID](#Site_ID) | +| Sampling Points Description | ntext(1073741823) | - | | Description of the sampling point | - | ### Site -Table for Site +Stores general site information, including address, site type, and a link to the associated watershed #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Picture | image(2147483647) | - | | Picture in site table | - | -| Province | nvarchar(255) | - | | Province in site table | - | -| Site ID | int **(PK)** | - | | Identifier for site, also used in 1 other table(s) | - | -| Site Name | nvarchar(100) | - | | Site Name in site table | - | -| Site Type | nvarchar(255) | - | | Site Type in site table | - | -| Watershed ID | int | - | | Identifier for hydrological_characteristics, also used in 3 other table(s) | FK → [Watershed_ID](#Watershed_ID) | -| Site City | nvarchar(255) | - | | Site City in site table | - | -| Site Country | nvarchar(255) | - | | Site Country in site table | - | -| Site Description | ntext(1073741823) | - | | Site Description in site table | - | -| Site Street Name | nvarchar(100) | - | | Site Street Name in site table | - | -| Site Street Number | nvarchar(100) | - | | Site Street Number in site table | - | -| Site Zip Code | nvarchar(100) | - | | Site Zip Code in site table | - | +| Picture | image(2147483647) | - | | Picture of the site | - | +| Province | nvarchar(255) | - | | Address: name of the province | - | +| Site ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | +| Site Name | nvarchar(100) | - | | Name of the site | - | +| Site Type | nvarchar(255) | - | | For example: "WWTP", "River" or "Sewer_system" | - | +| Watershed ID | int | - | | Linked to the Watershed table | FK → [Watershed_ID](#Watershed_ID) | +| Site City | nvarchar(255) | - | | Address: name of the city | - | +| Site Country | nvarchar(255) | - | | Address: name of the country | - | +| Site Description | ntext(1073741823) | - | | Description of the site | - | +| Site Street Name | nvarchar(100) | - | | Address: name of the street | - | +| Site Street Number | nvarchar(100) | - | | Address: number of the street | - | +| Site Zip Code | nvarchar(100) | - | | Address: zip code | - | ### Unit -Table for Unit +Stores the SI units of measurement (or other relevant units) corresponding to the parameters (e.g., mg/L, g/L, s) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Unit | nvarchar(100) | - | | Unit in unit table | - | -| Unit ID | int **(PK)** | - | | Identifier for unit, also used in 2 other table(s) | - | +| Unit | nvarchar(100) | - | | SI-units only | - | +| Unit ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | ### Urban Characteristics -Table for Urban Characteristics +Stores the urban land use percentages (e.g., commercial, residential, green spaces) within the watershed #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Agricultural | real | - | | Agricultural in urban_characteristics table | - | -| Commercial | real | - | | Commercial in urban_characteristics table | - | -| Green Spaces | real | - | | Green Spaces in urban_characteristics table | - | -| Industrial | real | - | | Industrial in urban_characteristics table | - | -| Institutional | real | - | | Institutional in urban_characteristics table | - | -| Recreational | real | - | | Recreational in urban_characteristics table | - | -| Residential | real | - | | Residential in urban_characteristics table | - | -| Watershed ID | int **(PK)** | - | | Identifier for hydrological_characteristics, also used in 3 other table(s) | - | +| Agricultural | real | - | | Percentage [%] of agricultural land use. For example farm land | - | +| Commercial | real | - | | Percentage [%] of commercial areas. For example stores or bank areas | - | +| Green Spaces | real | - | | Percentage [%] of green spaces | - | +| Industrial | real | - | | Percentage [%] of industrial areas. For example factories | - | +| Institutional | real | - | | Percentage [%] of institutional areas. For example schools, police stations or city hall | - | +| Recreational | real | - | | Percentage [%] of recreational areas. For example parks or sport fields | - | +| Residential | real | - | | Percentage [%] of residential areas. For example houses or apartment buildings | - | +| Watershed ID | int **(PK)** | - | | Linked to the Watershed table | FK → [Watershed_ID](#Watershed_ID) | ### Value -Table for Value +Stores each measured water quality or quantity value, its time stamp, replicate identification, and the link to its specific metadata set #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Comment ID | int | - | | Identifier for comments, also used in 1 other table(s) | FK → [Comment_ID](#Comment_ID) | -| Metadata ID | int | - | | Identifier for metadata, also used in 1 other table(s) | FK → [Metadata_ID](#Metadata_ID) | -| Number Of Experiment | numeric | - | | Number Of Experiment in value table | - | -| Timestamp | int | - | | Timestamp in value table | - | -| Value | float | - | | Value in value table | - | -| Value ID | int **(PK)** | - | | Unique identifier for value | - | +| Comment ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Comment_ID](#Comment_ID) | +| Metadata ID | int | - | | A unique ID is generated automatically by MySQL | FK → [Metadata_ID](#Metadata_ID) | +| Number Of Experiment | numeric | - | | Number of replica of an experiment | - | +| Timestamp | int | - | | Unix timestamp combining date and time of collected data | - | +| Value | float | - | | Value of collected data | - | +| Value ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | ### Watershed -Table for Watershed +Stores general information about the watershed area, including surface area, concentration time, and impervious surface percentage #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Concentration Time | int | - | | Concentration Time in watershed table | - | -| Impervious Surface | real | - | | Impervious Surface in watershed table | - | -| Surface Area | real | - | | Surface Area in watershed table | - | -| Watershed ID | int **(PK)** | - | | Identifier for hydrological_characteristics, also used in 3 other table(s) | - | -| Watershed Name | nvarchar(100) | - | | Watershed Name in watershed table | - | -| Watershed Description | ntext(1073741823) | - | | Watershed Description in watershed table | - | +| Concentration Time | int | - | | Concentration time in minutes [min] | - | +| Impervious Surface | real | - | | Percentage of the impervious surface of the watershed in percentage [%] | - | +| Surface Area | real | - | | Surface area of the watershed [ha] | - | +| Watershed ID | int **(PK)** | - | | Linked to the Watershed table | - | +| Watershed Name | nvarchar(100) | - | | Name of the watershed | - | +| Watershed Description | ntext(1073741823) | - | | Description of the watershed | - | ### Weather Condition -Table for Weather Condition +Stores descriptive information about the prevailing weather conditions when the measurement was taken (e.g., dry weather, wet weather, snow melt) #### Fields | Field | SQL Type | Value Set | Required | Description | Constraints | |-------|----------|-----------|----------|-------------|-------------| -| Condition ID | int **(PK)** | - | | Identifier for weather_condition, also used in 1 other table(s) | - | -| Weather Condition | nvarchar(100) | - | | Weather Condition in weather_condition table | - | -| Weather Condition Description | ntext(1073741823) | - | | Weather Condition Description in weather_condition table | - | \ No newline at end of file +| Condition ID | int **(PK)** | - | | A unique ID is generated automatically by MySQL | - | +| Weather Condition | nvarchar(100) | - | | Type of weather condition | - | +| Weather Condition Description | ntext(1073741823) | - | | Description of the condition | - | \ No newline at end of file diff --git a/docs/reference/valuesets.md b/docs/reference/valuesets.md index 7a9a99a..81b23dc 100644 --- a/docs/reference/valuesets.md +++ b/docs/reference/valuesets.md @@ -1,6 +1,6 @@ # Value Sets -Controlled vocabularies used throughout the database. +Controlled vocabularies used throughout database. diff --git a/mkdocs.yml b/mkdocs.yml index 384908e..55583bc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,22 +14,24 @@ theme: plugins: - search - + - markdown-exec hooks: - - docs/hooks/generate_docs.py + - docs/hooks/call_orchestrator.py nav: - Home: index.md - Contributing: - - The parts table: contributing/parts_table.md + - The Dictionary: contributing/dictionary.md - Reference: - Schema: reference/schema.md - Tables: reference/tables.md - Value Sets: reference/valuesets.md + - ERD Diagram: reference/erd.md markdown_extensions: - admonition - codehilite - tables - toc: - permalink: true \ No newline at end of file + permalink: true + - pymdownx.superfences \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 380436b..31407ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,9 +7,22 @@ requires-python = ">=3.12" dependencies = [ "mkdocs>=1.6.1", "mkdocs-material>=9.6.22", + "pydantic>=2.0.0", + "mkdocs-gen-files>=0.5.0", + "markdown-exec>=1.12.1", + "pymdown-extensions>=10.16.1", ] -[dependency-groups] +[project.optional-dependencies] dev = [ "pytest>=8.4.2", ] + +[tool.setuptools] +package-dir = { "" = "src" } + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +"" = ["*.json"] diff --git a/scripts/generate_dictionary_reference.py b/scripts/generate_dictionary_reference.py new file mode 100644 index 0000000..26f0968 --- /dev/null +++ b/scripts/generate_dictionary_reference.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +Generate dictionary reference documentation (tables and value sets). + +This script extracts table and value set generation logic from the main +generate_docs hook to create modular, testable components. + +Usage: + python generate_dictionary_reference.py +""" + +import sys +import json +from pathlib import Path + + +def parse_parts_json(json_path): + """ + Parse dictionary.json using Pydantic validation. + Returns same dict structure as parse_parts_table() for compatibility. + """ + # Add src to path to import models + project_root = Path(json_path).parent.parent + sys.path.insert(0, str(project_root / "src")) + + from open_dateaubase.data_model.models import Dictionary + + # Load and validate + with open(json_path, "r", encoding="utf-8") as f: + raw_data = json.load(f) + + # Pydantic validation + dictionary = Dictionary.model_validate(raw_data) + + # Transform to legacy format for generators + data = {"tables": {}, "value_sets": {}, "metadata": {}, "id_field_locations": {}} + + # Process tables + for part in dictionary.parts: + if part.part_type == "table": + data["tables"][part.part_id] = { + "label": part.label, + "description": part.description, + "fields": [], + } + + # Process fields + for part in dictionary.parts: + # Only process field parts that have table_presence + if hasattr(part, "table_presence") and part.part_type in [ + "key", + "property", + "compositeKeyFirst", + "compositeKeySecond", + "parentKey", + ]: + for table_name, presence in part.table_presence.items(): + if table_name not in data["tables"]: + continue + + # Track ID field locations + if part.part_id.endswith("_ID"): + if part.part_id not in data["id_field_locations"]: + data["id_field_locations"][part.part_id] = {} + data["id_field_locations"][part.part_id][table_name] = presence.role + + # Determine FK target and relationship type from explicit metadata + fk_to = "" + relationship_type = None + if part.part_type == "parentKey": + fk_to = part.ancestor_part_id + elif presence.relationship_type: + # Infer FK target from field name (field ending in _ID references same-named primary key) + if part.part_id.endswith("_ID"): + fk_to = part.part_id + relationship_type = presence.relationship_type + + field_info = { + "part_id": part.part_id, + "label": part.label, + "description": part.description, + "part_type": presence.role, + "sql_data_type": getattr(part, "sql_data_type", None) or "", + "is_required": presence.required, + "default_value": getattr(part, "default_value", None) or "", + "fk_to": fk_to, + "relationship_type": relationship_type, + "value_set": getattr(part, "value_set_part_id", None) or "", + "sort_order": presence.order, + } + data["tables"][table_name]["fields"].append(field_info) + + # Process value sets + for part in dictionary.parts: + if part.part_type == "valueSet": + data["value_sets"][part.part_id] = { + "label": part.label, + "description": part.description, + "members": [], + } + + for part in dictionary.parts: + if part.part_type == "valueSetMember": + value_set_id = part.member_of_set_part_id + if value_set_id in data["value_sets"]: + member_info = { + "part_id": part.part_id, + "label": part.label, + "description": part.description, + "sort_order": part.sort_order if part.sort_order else 999, + } + data["value_sets"][value_set_id]["members"].append(member_info) + + # Sort fields and members + for table in data["tables"].values(): + table["fields"].sort(key=lambda x: x["sort_order"]) + + for value_set in data["value_sets"].values(): + value_set["members"].sort(key=lambda x: x["sort_order"]) + + return data + + +def generate_tables_markdown(data): + """ + Generate markdown documentation from parsed data. + """ + md = ["# Database Tables\n"] + md.append("This documentation is auto-generated from dictionary.json.\n") + + # Generate table documentation + md.append("\n## Tables\n") + + for table_id, table_info in sorted(data["tables"].items()): + # Anchor as invisible span, table name as regular heading + md.append(f'\n\n') + md.append(f"### {table_info['label']}\n") + md.append(f"{table_info['description']}\n") + + if table_info["fields"]: + md.append("\n#### Fields\n") + md.append( + "| Field | SQL Type | Value Set | Required | Description | Constraints |" + ) + md.append( + "|-------|----------|-----------|----------|-------------|-------------|" + ) + + for field in table_info["fields"]: + field_name = field["label"] + field_id = field["part_id"] + + # SQL Type column + sql_type = field["sql_data_type"] if field["sql_data_type"] else "-" + if field["part_type"] in [ + "key", + "compositeKeyFirst", + "compositeKeySecond", + ]: + if field["part_type"] == "key": + sql_type += " **(PK)**" + elif field["part_type"] == "compositeKeyFirst": + sql_type += " **(CK-1)**" + elif field["part_type"] == "compositeKeySecond": + sql_type += " **(CK-2)**" + else: + raise ValueError( + f"Found unknown part type: {field['part_type']}. Correct dictionary OR update documentation generation code." + ) + + # Value Set column - link to value set definition + value_set = ( + f"[{field['value_set']}](valuesets.md#{field['value_set']})" + if field["value_set"] + else "-" + ) + + required = "✓" if field["is_required"] else "" + + # Anchor description with Part_ID + description = f'{field["description"]}' + + # Build constraints column + constraints = [] + if field["fk_to"]: + # Link to FK target field + constraints.append(f"FK → [{field['fk_to']}](#{field['fk_to']})") + if field["default_value"]: + constraints.append(f"Default: `{field['default_value']}`") + + constraints_str = "
".join(constraints) if constraints else "-" + + md.append( + f"| {field_name} | {sql_type} | {value_set} | {required} | {description} | {constraints_str} |" + ) + + return "\n".join(md) + + +def generate_value_sets_markdown(data): + """ + Generate value set documentation with proper anchoring. + """ + md = ["# Value Sets\n"] + md.append("Controlled vocabularies used throughout database.\n") + + if data["value_sets"]: + for value_set_id, value_set_info in sorted(data["value_sets"].items()): + # Anchor as invisible span, value set name as regular heading + md.append(f'\n\n') + md.append(f"## {value_set_info['label']}\n") + md.append(f"{value_set_info['description']}\n") + + if value_set_info["members"]: + md.append("\n| Value | Description |") + md.append("|-------|-------------|") + + for member in value_set_info["members"]: + member_id = member["part_id"] + # Anchor each member with its Part_ID + md.append( + f'| `{member_id}` | {member["description"]} |' + ) + else: + md.append("No value sets currently appear in dictionary.") + + return "\n".join(md) + + +def main(): + """Main entry point for script.""" + if len(sys.argv) != 3: + print("Usage: python generate_dictionary_reference.py ") + print("Example: python generate_dictionary_reference.py dictionary.json docs/reference") + sys.exit(1) + + json_path = Path(sys.argv[1]) + output_path = Path(sys.argv[2]) + + # Ensure output directory exists + output_path.mkdir(parents=True, exist_ok=True) + + # Parse JSON + parts_data = parse_parts_json(json_path) + + # Generate markdown + tables = generate_tables_markdown(parts_data) + value_sets = generate_value_sets_markdown(parts_data) + + # Write to files + (output_path / "tables.md").write_text(tables, encoding="utf-8") + (output_path / "valuesets.md").write_text(value_sets, encoding="utf-8") + + print(f"Generated dictionary reference documentation:") + print(f" Tables: {output_path / 'tables.md'}") + print(f" Value sets: {output_path / 'valuesets.md'}") + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_erd.py b/scripts/generate_erd.py new file mode 100644 index 0000000..cf99b5d --- /dev/null +++ b/scripts/generate_erd.py @@ -0,0 +1,1364 @@ +""" +Generate ERD (Entity-Relationship Diagram) from dictionary. + +This script extracts ERD generation logic from the main generate_docs hook +to create a modular, testable component. + +Usage: + python generate_erd.py +""" + +import sys +import json +from pathlib import Path +from typing import Dict, List, Any, Optional +from dataclasses import dataclass, asdict + +# Add src to path to import models +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "src")) + +from open_dateaubase.data_model.models import Dictionary + + +def parse_erd_json(json_path): + """ + Parse dictionary.json using Pydantic validation. + Returns same dict structure as parse_parts_table() for compatibility. + """ + # Load and validate + with open(json_path, "r", encoding="utf-8") as f: + raw_data = json.load(f) + + # Pydantic validation + dictionary = Dictionary.model_validate(raw_data) + + # Transform to legacy format for generators + data = {"tables": {}, "value_sets": {}, "metadata": {}, "id_field_locations": {}} + + # Process tables + for part in dictionary.parts: + if part.part_type == "table": + data["tables"][part.part_id] = { + "label": part.label, + "description": part.description, + "fields": [], + } + + # Process fields + for part in dictionary.parts: + # Only process field parts that have table_presence + if hasattr(part, "table_presence") and part.part_type in [ + "key", + "property", + "compositeKeyFirst", + "compositeKeySecond", + "parentKey", + ]: + for table_name, presence in part.table_presence.items(): + if table_name not in data["tables"]: + continue + + # Track ID field locations + if part.part_id.endswith("_ID"): + if part.part_id not in data["id_field_locations"]: + data["id_field_locations"][part.part_id] = {} + data["id_field_locations"][part.part_id][table_name] = presence.role + + # Determine FK target and relationship type from explicit metadata + fk_to = "" + relationship_type = None + if part.part_type == "parentKey": + fk_to = part.ancestor_part_id + elif presence.relationship_type: + # Infer FK target from field name (field ending in _ID references same-named primary key) + if part.part_id.endswith("_ID"): + fk_to = part.part_id + relationship_type = presence.relationship_type + + field_info = { + "part_id": part.part_id, + "label": part.label, + "description": part.description, + "part_type": presence.role, + "sql_data_type": getattr(part, "sql_data_type", None) or "", + "is_required": presence.required, + "default_value": getattr(part, "default_value", None) or "", + "fk_to": fk_to, + "relationship_type": relationship_type, + "value_set": getattr(part, "value_set_part_id", None) or "", + "sort_order": presence.order, + } + data["tables"][table_name]["fields"].append(field_info) + + # Process value sets + for part in dictionary.parts: + if part.part_type == "valueSet": + data["value_sets"][part.part_id] = { + "label": part.label, + "description": part.description, + "members": [], + } + + for part in dictionary.parts: + if part.part_type == "valueSetMember": + value_set_id = part.member_of_set_part_id + if value_set_id in data["value_sets"]: + member_info = { + "part_id": part.part_id, + "label": part.label, + "description": part.description, + "sort_order": part.sort_order if part.sort_order else 999, + } + data["value_sets"][value_set_id]["members"].append(member_info) + + # Sort fields and members + for table in data["tables"].values(): + table["fields"].sort(key=lambda x: x["sort_order"]) + + for value_set in data["value_sets"].values(): + value_set["members"].sort(key=lambda x: x["sort_order"]) + + return data + + +def generate_erd_files(parts_data, assets_path, output_path): + """ + Generate interactive ERD diagram. + + Args: + parts_data: Parsed dictionary data + assets_path: Path to docs/assets directory + output_path: Path to docs/reference directory + """ + + # Generate ERD data + erd_data = generate_erd_data(parts_data) + + # Create assets directory if it doesn't exist + assets_path.mkdir(parents=True, exist_ok=True) + + # Generate JointJS (interactive) version only + jointjs_path = assets_path / "erd_interactive.html" + + generate_erd_html(erd_data, jointjs_path, library="jointjs") + + print(f"Generated interactive ERD at {jointjs_path}") + + # Create ERD documentation page + erd_markdown = f"""# Entity Relationship Diagram (ERD) + +This interactive diagram shows all tables and their relationships in datEAUbase schema. + +## Interactive ERD + +The interactive version allows you to: +- 🖱️ **Drag tables** to rearrange layout +- 🔍 **Zoom in/out** for better visibility +- 📐 **Auto-layout** to reorganize tables automatically +- 💾 **Export** diagram as PNG + + + +[Open in new window](../assets/erd_interactive.html){{: target="_blank" .md-button .md-button--primary}} + +## Legend + +### Field Markers +- **PK** badge: Primary Key - Unique identifier for each record +- **FK** badge: Foreign Key - Reference to another table's primary key +- **\\*** Required field (NOT NULL) + +### Relationship Notation +Relationships use standard crow's foot notation: +- **Single line (|)**: "One" side of relationship +- **Crow's foot (⟨)**: "Many" side of relationship + +**Relationship Types:** +- **One-to-One**: Single line on both ends (e.g., watershed ↔ hydrological_characteristics) +- **One-to-Many**: Crow's foot on child side, single line on parent (e.g., site ↔ sampling_points) +- **Many-to-Many**: Crow's foot on both ends (via junction tables like project_has_contact) + +## Table Count + +The current schema contains **{len(parts_data["tables"])}** tables with **{len(erd_data["relationships"])}** relationships. +""" + + (output_path / "erd.md").write_text(erd_markdown, encoding="utf-8") + print(f"Generated ERD documentation page at {output_path / 'erd.md'}") + + +def main(): + """Main entry point for script.""" + if len(sys.argv) != 4: + print("Usage: python generate_erd.py ") + print("Example: python generate_erd.py dictionary.json docs/assets docs/reference") + sys.exit(1) + + json_path = Path(sys.argv[1]) + assets_path = Path(sys.argv[2]) + output_path = Path(sys.argv[3]) + + # Ensure directories exist + assets_path.mkdir(parents=True, exist_ok=True) + output_path.mkdir(parents=True, exist_ok=True) + + # Parse JSON + parts_data = parse_erd_json(json_path) + + # Generate ERD files + generate_erd_files(parts_data, assets_path, output_path) + + + + +@dataclass +class ERDField: + """Represents a field in a table for ERD visualization.""" + + name: str + sql_type: str + is_pk: bool = False + is_fk: bool = False + is_required: bool = False + fk_target: Optional[str] = None # Format: "table_name.field_name" + description: Optional[str] = None + + +@dataclass +class ERDTable: + """Represents a table for ERD visualization.""" + + id: str + label: str + description: str + fields: List[ERDField] + + +@dataclass +class ERDRelationship: + """Represents a foreign key relationship from child (FK) to parent (PK).""" + + from_table: str + to_table: str + from_field: str + to_field: str + # TODO: Add cardinality information in the future (one-to-one, one-to-many, many-to-many) + # This could be inferred from field.is_required and composite key patterns + relationship_type: str = ( + "many-to-one" # Placeholder for future cardinality implementation + ) + + +def generate_erd_data(parts_data: Dict[str, Any]) -> Dict[str, Any]: + """ + Transform parsed dictionary data into ERD-friendly format. + + Args: + parts_data: Parsed data from parse_parts_json() in generate_docs.py + + Returns: + Dict with 'tables' and 'relationships' for ERD rendering + """ + tables = [] + relationships = [] + + # Process each table + for table_id, table_info in parts_data["tables"].items(): + fields = [] + + for field in table_info["fields"]: + is_pk = field["part_type"] in [ + "key", + "compositeKeyFirst", + "compositeKeySecond", + ] + is_fk = bool(field.get("fk_to")) + + fk_target = None + if is_fk and field["fk_to"]: + # Extract target table from FK field (e.g., "Contact_ID" -> "contact") + fk_field = field["fk_to"] + if fk_field.endswith("_ID"): + # Convert to lowercase to match table_id format + target_table = fk_field[:-3].lower() + fk_target = f"{target_table}.{fk_field}" + + erd_field = ERDField( + name=field["label"], + sql_type=field["sql_data_type"] or "unknown", + is_pk=is_pk, + is_fk=is_fk, + is_required=field["is_required"], + fk_target=fk_target, + description=field["description"], + ) + fields.append(erd_field) + + erd_table = ERDTable( + id=table_id, + label=table_info["label"], + description=table_info["description"], + fields=fields, + ) + tables.append(erd_table) + + # Build a mapping of PK field IDs to their primary tables + # A field is a PRIMARY KEY in a table if part_type is 'key' (not compositeKeyFirst/Second) + # Composite keys in junction tables should NOT be treated as the primary definition + pk_id_to_table = {} + for tid, tinfo in parts_data["tables"].items(): + for f in tinfo["fields"]: + # Only consider 'key' as the primary definition of where this field is a PK + # compositeKeyFirst/Second means it's part of a composite key in a junction table + if f["part_type"] == "key": + pk_field_id = f["part_id"] + # Store the table where this field is the primary key + pk_id_to_table[pk_field_id] = (tid, f["label"]) + + # Extract relationships from foreign keys + # A relationship exists when a field is: + # - A property (FK) in the source table (indicated by fk_to being set) + # - A primary key in the target table (part_type='key') + for table_id, table_info in parts_data["tables"].items(): + for field in table_info["fields"]: + if field.get("fk_to"): + # fk_to contains the target field part_id (e.g., "Equipment_model_ID") + target_field_id = field["fk_to"] + + # Look up which table has this field as its primary key + target_info = pk_id_to_table.get(target_field_id) + + # Only create a relationship if we found a table with this as a primary key + if target_info: + target_table, target_pk_label = target_info + if target_table in parts_data["tables"]: + # Use explicit relationship_type from field metadata + rel_type = field.get("relationship_type", "one-to-many") + + relationship = ERDRelationship( + from_table=table_id, + to_table=target_table, + from_field=field["label"], + to_field=target_pk_label, + relationship_type=rel_type, + ) + relationships.append(relationship) + + return { + "tables": [asdict(t) for t in tables], + "relationships": [asdict(r) for r in relationships], + } + + +def generate_erd_html( + erd_data: Dict[str, Any], output_path: Path, library: str = "jointjs" +) -> None: + """ + Generate standalone HTML file with interactive ERD using JointJS. + + Args: + erd_data: ERD data from generate_erd_data() + output_path: Path to write HTML file + library: Deprecated parameter, kept for backward compatibility. Only 'jointjs' is supported. + """ + output_path.parent.mkdir(parents=True, exist_ok=True) + + if library != "jointjs": + raise ValueError( + f"Unsupported library. Only 'jointjs' library is supported. Got: {library}" + ) + + html_content = _generate_jointjs_html(erd_data) + output_path.write_text(html_content, encoding="utf-8") + + +def _generate_jointjs_html(erd_data: Dict[str, Any]) -> str: + """Generate HTML using JointJS library with custom HTML elements (Lucid-like).""" + + # Serialize ERD data as JSON for embedding + erd_json = json.dumps(erd_data, indent=2) + + html = f""" + + + + + datEAUbase ERD + + + + + + + + + + + + + + + + + + +
+ + + + + +
+ +
+ + + + + +""" + + return html + + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_sql.py b/scripts/generate_sql.py new file mode 100644 index 0000000..e065982 --- /dev/null +++ b/scripts/generate_sql.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python3 +""" +Generate SQL schema from dictionary. + +This script extracts SQL generation logic from the main generate_docs hook +to create a modular, testable component. + +Usage: + python generate_sql.py +""" + +import sys +import json +from pathlib import Path +from datetime import datetime +from importlib.metadata import version + +# Add src to path to import models +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "src")) + +from open_dateaubase.data_model.models import Dictionary + +package_version = version("open-dateaubase") + + +def parse_parts_json(json_path): + """ + Parse dictionary.json using Pydantic validation. + Returns same dict structure as parse_parts_table() for compatibility. + """ + # Load and validate + with open(json_path, "r", encoding="utf-8") as f: + raw_data = json.load(f) + + # Pydantic validation + dictionary = Dictionary.model_validate(raw_data) + + # Transform to legacy format for generators + data = {"tables": {}, "value_sets": {}, "metadata": {}, "id_field_locations": {}} + + # Process tables + for part in dictionary.parts: + if part.part_type == "table": + data["tables"][part.part_id] = { + "label": part.label, + "description": part.description, + "fields": [], + } + + # Process fields + for part in dictionary.parts: + # Only process field parts that have table_presence + if hasattr(part, "table_presence") and part.part_type in [ + "key", + "property", + "compositeKeyFirst", + "compositeKeySecond", + "parentKey", + ]: + for table_name, presence in part.table_presence.items(): + if table_name not in data["tables"]: + continue + + # Track ID field locations + if part.part_id.endswith("_ID"): + if part.part_id not in data["id_field_locations"]: + data["id_field_locations"][part.part_id] = {} + data["id_field_locations"][part.part_id][table_name] = presence.role + + # Determine FK target and relationship type from explicit metadata + fk_to = "" + relationship_type = None + if part.part_type == "parentKey": + fk_to = part.ancestor_part_id + elif presence.relationship_type: + # Infer FK target from field name (field ending in _ID references same-named primary key) + if part.part_id.endswith("_ID"): + fk_to = part.part_id + relationship_type = presence.relationship_type + + field_info = { + "part_id": part.part_id, + "label": part.label, + "description": part.description, + "part_type": presence.role, + "sql_data_type": getattr(part, "sql_data_type", None) or "", + "is_required": presence.required, + "default_value": getattr(part, "default_value", None) or "", + "fk_to": fk_to, + "relationship_type": relationship_type, + "value_set": getattr(part, "value_set_part_id", None) or "", + "sort_order": presence.order, + } + data["tables"][table_name]["fields"].append(field_info) + + # Process value sets + for part in dictionary.parts: + if part.part_type == "valueSet": + data["value_sets"][part.part_id] = { + "label": part.label, + "description": part.description, + "members": [], + } + + for part in dictionary.parts: + if part.part_type == "valueSetMember": + value_set_id = part.member_of_set_part_id + if value_set_id in data["value_sets"]: + member_info = { + "part_id": part.part_id, + "label": part.label, + "description": part.description, + "sort_order": part.sort_order if part.sort_order else 999, + } + data["value_sets"][value_set_id]["members"].append(member_info) + + # Sort fields and members + for table in data["tables"].values(): + table["fields"].sort(key=lambda x: x["sort_order"]) + + for value_set in data["value_sets"].values(): + value_set["members"].sort(key=lambda x: x["sort_order"]) + + return data + + +def generate_sql_schemas(parts_data, output_path, db_list): + """Generate SQL schemas for multiple database types.""" + for target_db in db_list: + sql_schema = generate_sql_schema(parts_data, target_db=target_db) + version_str = package_version + filename = f"v{version_str}_as-designed_{target_db}.sql" + (output_path / filename).write_text(sql_schema, encoding="utf-8") + print(f"Generated SQL schema for {target_db} at {output_path / filename}") + + +def generate_sql_schema(data, target_db="mssql", include_timestamp=True): + """ + Generate SQL CREATE statements from parsed metadata. + + Args: + data: Parsed parts table data + target_db: Target database flavor ('mssql', 'postgres', 'mysql' - future) + include_timestamp: Whether to include generation timestamp (default: True) + + Returns: + SQL DDL as a string + + Raises: + ValueError: If circular foreign key dependencies detected + """ + # Validate no circular FK dependencies + validate_no_circular_fks(data) + + sql = ["-- Auto-generated SQL schema from dictionary.json"] + sql.append(f"-- Target database: {target_db.upper()}") + if include_timestamp: + sql.append(f"-- Generated: {datetime.now().isoformat()}") + sql.append("\n") + + # Get DB-specific config + db_config = get_db_config(target_db) + + # First pass: Create all tables without foreign keys + for table_id, table_info in sorted(data["tables"].items()): + sql.append(f"\n-- {table_info['description']}") + sql.append(f"CREATE TABLE {db_config['quote'](table_id)} (") + + field_definitions = [] + pk_fields = [] + + for field in table_info["fields"]: + field_def = generate_field_definition(field, data, db_config) + field_definitions.append(field_def) + + # Track primary key fields + if field["part_type"] in ["key", "compositeKeyFirst", "compositeKeySecond"]: + field_name = extract_field_name(field["part_id"]) + pk_fields.append(f"{db_config['quote'](field_name)}") + + # Add primary key constraint + if pk_fields: + pk_name = "PK_" + table_id + pk_constraint = f" CONSTRAINT {db_config['quote'](pk_name)} PRIMARY KEY ({', '.join(pk_fields)})" + field_definitions.append(pk_constraint) + + sql.append(",\n".join(field_definitions)) + sql.append(");\n") + + # Second pass: Add foreign key constraints + sql.append("\n-- Foreign Key Constraints\n") + for table_id, table_info in sorted(data["tables"].items()): + for field in table_info["fields"]: + if field["fk_to"]: + fk_sql = generate_foreign_key_constraint(table_id, field, data, db_config) + if fk_sql: + sql.append(fk_sql) + + return "\n".join(sql) + + +def get_db_config(target_db): + """ + Get database-specific configuration. + + Args: + target_db: Database flavor string + + Returns: + Dict with DB-specific settings + """ + configs = { + "mssql": { + "quote_char": "[", + "quote_char_end": "]", + "type_mappings": { + "nvarchar": "nvarchar", + "ntext": "nvarchar(max)", # ntext deprecated in modern MSSQL + "int": "int", + "float": "float", + "real": "real", + "numeric": "numeric", + "bit": "bit", + }, + "supports_check_constraints": True, + "supports_deferred_constraints": False, + }, + # Future: postgres, mysql, sqlite configs + } + + if target_db not in configs: + raise ValueError( + f"Unsupported database: {target_db}. Supported: {list(configs.keys())}" + ) + + config = configs[target_db] + + # Add convenience method for quoting identifiers + if config["quote_char_end"]: + config["quote"] = ( + lambda name: f"{config['quote_char']}{name}{config['quote_char_end']}" + ) + else: + config["quote"] = ( + lambda name: f"{config['quote_char']}{name}{config['quote_char_end']}" + ) + + return config + + +def extract_field_name(part_id): + """ + Extract field name from Part_ID. + + NEW FORMAT handling: + - ID fields (e.g., 'Equipment_ID', 'Project_ID'): Use as-is (these are actual SQL field names) + - Table-prefixed fields (e.g., 'site_City', 'purpose_Description'): Remove table prefix + - Non-prefixed fields: Use as-is + + Args: + part_id: Part_ID from dictionary + + Returns: + Field name to use in SQL + """ + # ID fields are used as-is in SQL + if part_id.endswith("_ID"): + return part_id + + # Table-prefixed non-ID fields: remove prefix + # Format is lowercase_table_MixedCaseField (e.g., 'site_City', 'contact_City') + if "_" in part_id: + # Check if first part looks like a table name (lowercase) + parts = part_id.split("_", 1) + if len(parts) == 2 and parts[0].islower(): + # This is likely a table-prefixed field, remove prefix + return parts[1] + + # Otherwise use as-is + return part_id + + +def validate_no_circular_fks(data): + """ + Check for circular foreign key dependencies between tables. + + Args: + data: Parsed parts table data + + Raises: + ValueError: If circular FK dependencies found + """ + # Build adjacency list of FK relationships + fk_graph = {table_id: set() for table_id in data["tables"]} + + for table_id, table_info in data["tables"].items(): + for field in table_info["fields"]: + if field["fk_to"] and "_" in field["fk_to"]: + target_table = field["fk_to"].split("_", 1)[0] + if target_table in fk_graph: + fk_graph[table_id].add(target_table) + + # Check for bidirectional relationships (A->B and B->A) + circular_deps = [] + for table_a, targets in fk_graph.items(): + for table_b in targets: + if table_b == table_a: + # self-referential FKs are allowed + continue + if table_a in fk_graph.get(table_b, set()): + # Found circular dependency + pair = tuple(sorted([table_a, table_b])) + if pair not in circular_deps: + circular_deps.append(pair) + + if circular_deps: + error_msg = "Circular foreign key dependencies detected:\n" + for table_a, table_b in circular_deps: + error_msg += f" - {table_a} ↔ {table_b}\n" + error_msg += "\nEach pair of tables has FKs pointing to each other, which creates ambiguity in table creation order." + raise ValueError(error_msg) + + +def generate_field_definition(field, data, db_config): + """ + Generate SQL field definition with constraints. + + Args: + field: Field metadata dict + data: Full parsed data (for value set lookups) + db_config: Database-specific configuration + + Returns: + SQL field definition string + """ + field_name = extract_field_name(field["part_id"]) + + quote = db_config["quote"] + + parts = [f" {quote(field_name)}"] + + # Data type with mapping + sql_type = field["sql_data_type"] if field["sql_data_type"] else "nvarchar(255)" + # Apply type mapping for target DB + base_type = sql_type.split("(")[ + 0 + ] # Extract base type (e.g., 'nvarchar' from 'nvarchar(255)') + if base_type in db_config["type_mappings"]: + # Preserve parameters if they exist + if "(" in sql_type: + params = sql_type[sql_type.index("(") :] + sql_type = db_config["type_mappings"][base_type].split("(")[0] + params + else: + sql_type = db_config["type_mappings"][base_type] + + parts.append(sql_type) + + # NULL constraint + if field["is_required"]: + parts.append("NOT NULL") + else: + parts.append("NULL") + + # Default value + if field["default_value"]: + default_val = field["default_value"] + # Handle boolean defaults + if default_val in ["True", "False"]: + default_val = "1" if default_val == "True" else "0" + # Handle numeric vs string defaults + if field["sql_data_type"] and field["sql_data_type"].split("(")[0] in [ + "int", + "float", + "real", + "numeric", + "bit", + ]: + parts.append(f"DEFAULT {default_val}") + else: + parts.append(f"DEFAULT '{default_val}'") + + # Note: Value set CHECK constraints removed per requirement #3 + # Future: could add back conditionally based on target_db config + + return " ".join(parts) + + +def generate_foreign_key_constraint(table_id, field, data, db_config): + """ + Generate ALTER TABLE statement for foreign key. + + NEW FORMAT: fk_to is Part_ID of target field (e.g., 'TestTable_ID') + We need to find which table has this field as a primary key by looking up + the table Part_ID from the data model. + + Args: + table_id: Source table ID + field: Field metadata with FK reference + data: Full parsed data (for id_field_locations lookup) + db_config: Database-specific configuration + + Returns: + SQL ALTER TABLE statement or None + """ + if not field["fk_to"]: + return None + + # fk_to is Part_ID of target (e.g., 'TestTable_ID') + fk_target = field["fk_to"] + source_field = extract_field_name(field["part_id"]) + + # For ID fields like 'TestTable_ID', extract table name from FK field + # This preserves the capitalization pattern from the data model + if fk_target.endswith("_ID"): + target_field = fk_target # e.g., 'TestTable_ID' + # Extract table name from FK field name (e.g., 'Equipment_model_ID' -> 'Equipment_model') + # This matches the capitalization used in the original data model design + target_table = fk_target[:-3] # Remove '_ID' + else: + # Non-ID FK (shouldn't happen in new format, but fallback) + return None + + quote = db_config["quote"] + constraint_name = f"FK_{table_id}_{source_field}" + + sql = f"""ALTER TABLE {quote(table_id)} + ADD CONSTRAINT {quote(constraint_name)} + FOREIGN KEY ({quote(source_field)}) + REFERENCES {quote(target_table)} ({quote(target_field)}); +""" + + return sql + + +def main(): + """Main entry point for script.""" + if len(sys.argv) != 4: + print("Usage: python generate_sql.py ") + print( + "Example: python generate_sql.py dictionary.json sql_generation_scripts mssql" + ) + sys.exit(1) + + json_path = Path(sys.argv[1]) + output_path = Path(sys.argv[2]) + db_list = sys.argv[3].split(",") # Comma-separated list of databases + + # Ensure output directory exists + output_path.mkdir(parents=True, exist_ok=True) + + # Parse JSON + parts_data = parse_parts_json(json_path) + + # Generate SQL schemas + generate_sql_schemas(parts_data, output_path, db_list) + + +if __name__ == "__main__": + main() diff --git a/scripts/orchestrate_docs.py b/scripts/orchestrate_docs.py new file mode 100644 index 0000000..5890685 --- /dev/null +++ b/scripts/orchestrate_docs.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Documentation generation orchestrator. + +This script coordinates the generation of all documentation components: +- Dictionary reference (tables and value sets) +- ERD diagrams +- SQL schemas +- Asset copying + +Usage: + python generate_docs.py [target_dbs] +""" + +import sys +import subprocess +from pathlib import Path + +# Add scripts to path to import our modules +project_root = Path(__file__).parent +sys.path.insert(0, str(project_root)) + +from generate_dictionary_reference import ( + parse_parts_json, + generate_tables_markdown, + generate_value_sets_markdown, +) +from generate_erd import parse_erd_json, generate_erd_files +from generate_sql import parse_parts_json as parse_sql_json, generate_sql_schemas + + +def copy_generated_assets(assets_dir): + """Copy generated HTML assets to be served by MkDocs.""" + + # Files to copy - specifically the generated ERD files + generated_files = ["erd_interactive.html"] + + print(f"Checking for assets in {assets_dir.absolute()}") + + for filename in generated_files: + source_path = assets_dir / filename + if source_path.exists(): + # Target path in built site (assets/filename) + target_path = f"assets/{filename}" + + print(f"Copying {filename} -> {target_path}") + + # Use mkdocs_gen_files to register the file with MkDocs + try: + import mkdocs_gen_files + + content = source_path.read_text(encoding="utf-8") + with mkdocs_gen_files.open(target_path, "w") as f: + f.write(content) + except ImportError: + print( + "Warning: mkdocs_gen_files not available, assets may not be included in build" + ) + except Exception as e: + print(f"Error copying {filename}: {e}") + else: + print(f"Warning: Expected asset {filename} not found in {assets_dir}") + + +def main(): + """Main entry point for orchestrator.""" + if len(sys.argv) < 5: + print( + "Usage: python generate_docs.py [target_dbs]" + ) + print( + "Example: python generate_docs.py dictionary.json docs/reference sql_generation_scripts docs/assets mssql" + ) + sys.exit(1) + + json_path = Path(sys.argv[1]) + docs_dir = Path(sys.argv[2]) + sql_dir = Path(sys.argv[3]) + assets_dir = Path(sys.argv[4]) + + # Default to mssql if no databases specified + target_dbs = sys.argv[5].split(",") if len(sys.argv) > 5 else ["mssql"] + + # Ensure directories exist + docs_dir.mkdir(parents=True, exist_ok=True) + sql_dir.mkdir(parents=True, exist_ok=True) + assets_dir.mkdir(parents=True, exist_ok=True) + + print(f"Generating documentation from {json_path}") + print(f"Output directories:") + print(f" Docs: {docs_dir}") + print(f" SQL: {sql_dir}") + print(f" Assets: {assets_dir}") + print(f" Target databases: {target_dbs}") + + # Parse JSON once (each script can parse independently) + parts_data = parse_parts_json(json_path) + + # Generate dictionary reference + print("\n=== Generating Dictionary Reference ===") + tables = generate_tables_markdown(parts_data) + value_sets = generate_value_sets_markdown(parts_data) + + (docs_dir / "tables.md").write_text(tables, encoding="utf-8") + print(f"Generated tables documentation: {docs_dir / 'tables.md'}") + + (docs_dir / "valuesets.md").write_text(value_sets, encoding="utf-8") + print(f"Generated value sets documentation: {docs_dir / 'valuesets.md'}") + + # Generate ERD + print("\n=== Generating ERD ===") + erd_parts_data = parse_erd_json(json_path) + generate_erd_files(erd_parts_data, assets_dir, docs_dir) + + # Generate SQL schemas + print("\n=== Generating SQL Schemas ===") + sql_parts_data = parse_sql_json(json_path) + generate_sql_schemas(sql_parts_data, sql_dir, target_dbs) + + # Copy assets + print("\n=== Copying Assets ===") + copy_generated_assets(assets_dir) + + print("\n=== Documentation Generation Complete ===") + print("All components generated successfully!") + + +if __name__ == "__main__": + main() diff --git a/sql_generation_scripts/v0.1.0_as-designed_mssql.sql b/sql_generation_scripts/v0.1.0_as-designed_mssql.sql index 45eb0dd..fb7c6ff 100644 --- a/sql_generation_scripts/v0.1.0_as-designed_mssql.sql +++ b/sql_generation_scripts/v0.1.0_as-designed_mssql.sql @@ -1,10 +1,10 @@ --- Auto-generated SQL schema from Parts metadata table +-- Auto-generated SQL schema from dictionary.json -- Target database: MSSQL --- Generated: 2025-10-28T20:58:28.454637 +-- Generated: 2025-12-10T18:46:32.837716 --- Table for Comments +-- Stores any additional textual comments, notes, or observations related to a specific measured value CREATE TABLE [comments] ( [Comment] nvarchar(1073741823) NULL, [Comment_ID] int NULL, @@ -12,7 +12,7 @@ CREATE TABLE [comments] ( ); --- Table for Contact +-- Stores detailed personal and professional information for people involved in projects (e.g., name, affiliation, function, e-mail, phone) CREATE TABLE [contact] ( [Company] nvarchar(1073741823) NULL, [Contact_ID] int NULL, @@ -35,7 +35,7 @@ CREATE TABLE [contact] ( ); --- Table for Equipment +-- Stores information about a specific, physical piece of equipment (e.g., serial number, owner, purchase date, storage location) CREATE TABLE [equipment] ( [Equipment_ID] int NULL, [Equipment_identifier] nvarchar(100) NULL, @@ -48,7 +48,7 @@ CREATE TABLE [equipment] ( ); --- Table for Equipment Model +-- Stores detailed, non-redundant specifications for a specific sensor or instrument model (e.g., manufacturer, functions, method) CREATE TABLE [equipment_model] ( [Equipment_model] nvarchar(100) NULL, [Equipment_model_ID] int NULL, @@ -60,7 +60,7 @@ CREATE TABLE [equipment_model] ( ); --- Table for Equipment Model Has Parameter +-- Links equipment models to the parameters they can measure CREATE TABLE [equipment_model_has_Parameter] ( [Equipment_model_ID] int NULL, [Parameter_ID] int NULL, @@ -68,7 +68,7 @@ CREATE TABLE [equipment_model_has_Parameter] ( ); --- Table for Equipment Model Has Procedures +-- Links equipment models to the relevant maintenance procedures CREATE TABLE [equipment_model_has_procedures] ( [Equipment_model_ID] int NULL, [Procedure_ID] int NULL, @@ -76,7 +76,7 @@ CREATE TABLE [equipment_model_has_procedures] ( ); --- Table for Hydrological Characteristics +-- Stores the hydrological land use percentages (e.g., forest, wetlands, cropland, grassland) within the watershed CREATE TABLE [hydrological_characteristics] ( [Cropland] real NULL, [Forest] real NULL, @@ -89,7 +89,7 @@ CREATE TABLE [hydrological_characteristics] ( ); --- Table for Metadata +-- Contains a list of all existing unique metadata combinations (represented by a series of foreign keys/IDs) that describe a single measurement CREATE TABLE [metadata] ( [Condition_ID] int NULL, [Contact_ID] int NULL, @@ -105,7 +105,7 @@ CREATE TABLE [metadata] ( ); --- Table for Parameter +-- Stores the different water quality or quantity parameters that are measured (e.g., pH, TSS, N-components) CREATE TABLE [parameter] ( [Parameter] nvarchar(100) NULL, [Parameter_ID] int NULL, @@ -115,7 +115,7 @@ CREATE TABLE [parameter] ( ); --- Table for Parameter Has Procedures +-- Links parameters to the relevant measurement procedures CREATE TABLE [parameter_has_procedures] ( [Parameter_ID] int NULL, [Procedure_ID] int NULL, @@ -123,7 +123,7 @@ CREATE TABLE [parameter_has_procedures] ( ); --- Table for Procedures +-- Stores details for different measurement procedures (e.g., calibration, validation, standard operating procedures, ISO methods) CREATE TABLE [procedures] ( [Procedure_ID] int NULL, [Procedure_location] nvarchar(100) NULL, @@ -134,7 +134,7 @@ CREATE TABLE [procedures] ( ); --- Table for Project +-- Stores descriptive information about the research or monitoring project for which the data was collected CREATE TABLE [project] ( [Project_ID] int NULL, [Project_name] nvarchar(100) NULL, @@ -143,7 +143,7 @@ CREATE TABLE [project] ( ); --- Table for Project Has Contact +-- Links projects to the personnel involved in them CREATE TABLE [project_has_contact] ( [Contact_ID] int NULL, [Project_ID] int NULL, @@ -151,7 +151,7 @@ CREATE TABLE [project_has_contact] ( ); --- Table for Project Has Equipment +-- Links projects to the specific equipment used within them CREATE TABLE [project_has_equipment] ( [Equipment_ID] int NULL, [Project_ID] int NULL, @@ -159,7 +159,7 @@ CREATE TABLE [project_has_equipment] ( ); --- Table for Project Has Sampling Points +-- Links projects to the sampling points used within them CREATE TABLE [project_has_sampling_points] ( [Project_ID] int NULL, [Sampling_point_ID] int NULL, @@ -167,7 +167,7 @@ CREATE TABLE [project_has_sampling_points] ( ); --- Table for Purpose +-- Stores information about the aim of the measurement (e.g., on-line measurement, laboratory analysis, calibration, validation, cleaning) CREATE TABLE [purpose] ( [Purpose] nvarchar(100) NULL, [Purpose_ID] int NULL, @@ -176,7 +176,7 @@ CREATE TABLE [purpose] ( ); --- Table for Sampling Points +-- Stores the identification, specific geographical coordinates (Latitude/Longitude/GPS), and description of a particular spot where a sample or measurement is taken CREATE TABLE [sampling_points] ( [Latitude_GPS] nvarchar(100) NULL, [Longitude_GPS] nvarchar(100) NULL, @@ -190,7 +190,7 @@ CREATE TABLE [sampling_points] ( ); --- Table for Site +-- Stores general site information, including address, site type, and a link to the associated watershed CREATE TABLE [site] ( [Picture] image(2147483647) NULL, [Province] nvarchar(255) NULL, @@ -208,7 +208,7 @@ CREATE TABLE [site] ( ); --- Table for Unit +-- Stores the SI units of measurement (or other relevant units) corresponding to the parameters (e.g., mg/L, g/L, s) CREATE TABLE [unit] ( [Unit] nvarchar(100) NULL, [Unit_ID] int NULL, @@ -216,7 +216,7 @@ CREATE TABLE [unit] ( ); --- Table for Urban Characteristics +-- Stores the urban land use percentages (e.g., commercial, residential, green spaces) within the watershed CREATE TABLE [urban_characteristics] ( [Agricultural] real NULL, [Commercial] real NULL, @@ -230,7 +230,7 @@ CREATE TABLE [urban_characteristics] ( ); --- Table for Value +-- Stores each measured water quality or quantity value, its time stamp, replicate identification, and the link to its specific metadata set CREATE TABLE [value] ( [Comment_ID] int NULL, [Metadata_ID] int NULL, @@ -242,7 +242,7 @@ CREATE TABLE [value] ( ); --- Table for Watershed +-- Stores general information about the watershed area, including surface area, concentration time, and impervious surface percentage CREATE TABLE [watershed] ( [Concentration_time] int NULL, [Impervious_surface] real NULL, @@ -254,7 +254,7 @@ CREATE TABLE [watershed] ( ); --- Table for Weather Condition +-- Stores descriptive information about the prevailing weather conditions when the measurement was taken (e.g., dry weather, wet weather, snow melt) CREATE TABLE [weather_condition] ( [Condition_ID] int NULL, [Weather_condition] nvarchar(100) NULL, @@ -270,6 +270,26 @@ ALTER TABLE [equipment] FOREIGN KEY ([Equipment_model_ID]) REFERENCES [Equipment_model] ([Equipment_model_ID]); +ALTER TABLE [equipment_model_has_Parameter] + ADD CONSTRAINT [FK_equipment_model_has_Parameter_Equipment_model_ID] + FOREIGN KEY ([Equipment_model_ID]) + REFERENCES [Equipment_model] ([Equipment_model_ID]); + +ALTER TABLE [equipment_model_has_procedures] + ADD CONSTRAINT [FK_equipment_model_has_procedures_Equipment_model_ID] + FOREIGN KEY ([Equipment_model_ID]) + REFERENCES [Equipment_model] ([Equipment_model_ID]); + +ALTER TABLE [equipment_model_has_procedures] + ADD CONSTRAINT [FK_equipment_model_has_procedures_Procedure_ID] + FOREIGN KEY ([Procedure_ID]) + REFERENCES [Procedure] ([Procedure_ID]); + +ALTER TABLE [hydrological_characteristics] + ADD CONSTRAINT [FK_hydrological_characteristics_Watershed_ID] + FOREIGN KEY ([Watershed_ID]) + REFERENCES [Watershed] ([Watershed_ID]); + ALTER TABLE [metadata] ADD CONSTRAINT [FK_metadata_Condition_ID] FOREIGN KEY ([Condition_ID]) @@ -320,6 +340,41 @@ ALTER TABLE [parameter] FOREIGN KEY ([Unit_ID]) REFERENCES [Unit] ([Unit_ID]); +ALTER TABLE [parameter_has_procedures] + ADD CONSTRAINT [FK_parameter_has_procedures_Procedure_ID] + FOREIGN KEY ([Procedure_ID]) + REFERENCES [Procedure] ([Procedure_ID]); + +ALTER TABLE [project_has_contact] + ADD CONSTRAINT [FK_project_has_contact_Contact_ID] + FOREIGN KEY ([Contact_ID]) + REFERENCES [Contact] ([Contact_ID]); + +ALTER TABLE [project_has_contact] + ADD CONSTRAINT [FK_project_has_contact_Project_ID] + FOREIGN KEY ([Project_ID]) + REFERENCES [Project] ([Project_ID]); + +ALTER TABLE [project_has_equipment] + ADD CONSTRAINT [FK_project_has_equipment_Equipment_ID] + FOREIGN KEY ([Equipment_ID]) + REFERENCES [Equipment] ([Equipment_ID]); + +ALTER TABLE [project_has_equipment] + ADD CONSTRAINT [FK_project_has_equipment_Project_ID] + FOREIGN KEY ([Project_ID]) + REFERENCES [Project] ([Project_ID]); + +ALTER TABLE [project_has_sampling_points] + ADD CONSTRAINT [FK_project_has_sampling_points_Project_ID] + FOREIGN KEY ([Project_ID]) + REFERENCES [Project] ([Project_ID]); + +ALTER TABLE [project_has_sampling_points] + ADD CONSTRAINT [FK_project_has_sampling_points_Sampling_point_ID] + FOREIGN KEY ([Sampling_point_ID]) + REFERENCES [Sampling_point] ([Sampling_point_ID]); + ALTER TABLE [sampling_points] ADD CONSTRAINT [FK_sampling_points_Site_ID] FOREIGN KEY ([Site_ID]) @@ -330,6 +385,11 @@ ALTER TABLE [site] FOREIGN KEY ([Watershed_ID]) REFERENCES [Watershed] ([Watershed_ID]); +ALTER TABLE [urban_characteristics] + ADD CONSTRAINT [FK_urban_characteristics_Watershed_ID] + FOREIGN KEY ([Watershed_ID]) + REFERENCES [Watershed] ([Watershed_ID]); + ALTER TABLE [value] ADD CONSTRAINT [FK_value_Comment_ID] FOREIGN KEY ([Comment_ID]) diff --git a/src/dictionary.csv b/src/dictionary.csv deleted file mode 100644 index 2d3b6aa..0000000 --- a/src/dictionary.csv +++ /dev/null @@ -1,206 +0,0 @@ -Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,Ancestor_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,Parts_present,comments_present,contact_present,equipment_present,equipment_model_present,equipment_model_has_Parameter_present,equipment_model_has_procedures_present,hydrological_characteristics_present,metadata_present,parameter_present,parameter_has_procedures_present,procedures_present,project_present,project_has_contact_present,project_has_equipment_present,project_has_sampling_points_present,purpose_present,sampling_points_present,site_present,unit_present,urban_characteristics_present,value_present,watershed_present,weather_condition_present,Parts_required,Parts_order,comments_required,comments_order,contact_required,contact_order,equipment_required,equipment_order,equipment_model_required,equipment_model_order,equipment_model_has_Parameter_required,equipment_model_has_Parameter_order,equipment_model_has_procedures_required,equipment_model_has_procedures_order,hydrological_characteristics_required,hydrological_characteristics_order,metadata_required,metadata_order,parameter_required,parameter_order,parameter_has_procedures_required,parameter_has_procedures_order,procedures_required,procedures_order,project_required,project_order,project_has_contact_required,project_has_contact_order,project_has_equipment_required,project_has_equipment_order,project_has_sampling_points_required,project_has_sampling_points_order,purpose_required,purpose_order,sampling_points_required,sampling_points_order,site_required,site_order,unit_required,unit_order,urban_characteristics_required,urban_characteristics_order,value_required,value_order,watershed_required,watershed_order,weather_condition_required,weather_condition_order -Parts,Parts,Metadata table defining all components of the database model,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Part_ID,Part ID,Unique identifier for each part in the data model,key,,,,nvarchar(255),True,,1,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Label,Label,Human-readable label for the part,property,,,,nvarchar(255),True,,2,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Description,Description,Detailed description of what the part represents,property,,,,ntext,True,,3,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Part_type,Part Type,Classification of the database component,property,,,,nvarchar(50),True,,4,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Value_set_part_ID,Value Set Part ID,Which value set constrains this property,property,,,,nvarchar(255),False,,5,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Member_of_set_part_ID,Member Of Set Part ID,Which value set this part is a member of,property,,,,nvarchar(255),False,,6,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Ancestor_part_ID,Ancestor Part ID,"For parentKey type, the Part_ID of the ancestor being referenced",property,,,,nvarchar(255),False,,7,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -SQL_data_type,SQL Data Type,SQL data type of the column,property,,,,nvarchar(100),False,,8,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Is_required,Is Required,Whether this field is mandatory,property,,,,bit,False,,9,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Default_value,Default Value,Default value for the field,property,,,,ntext,False,,10,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Sort_order,Sort Order,Display order for documentation and UI,property,,,,int,False,,11,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -comments_present,Comments Present,Indicates whether and how a field appears in the comments table,property,,,,nvarchar(50),False,,11,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -comments_required,Comments Required,Whether this part is required in the comments table,property,,,,bit,False,,12,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -comments_order,Comments Order,Display order of this part in the comments table,property,,,,int,False,,13,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_present,Contact Present,Indicates whether and how a field appears in the contact table,property,,,,nvarchar(50),False,,14,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_required,Contact Required,Whether this part is required in the contact table,property,,,,bit,False,,15,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_order,Contact Order,Display order of this part in the contact table,property,,,,int,False,,16,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_present,Equipment Present,Indicates whether and how a field appears in the equipment table,property,,,,nvarchar(50),False,,17,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_required,Equipment Required,Whether this part is required in the equipment table,property,,,,bit,False,,18,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_order,Equipment Order,Display order of this part in the equipment table,property,,,,int,False,,19,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_present,Equipment Model Present,Indicates whether and how a field appears in the equipment_model table,property,,,,nvarchar(50),False,,20,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_required,Equipment Model Required,Whether this part is required in the equipment_model table,property,,,,bit,False,,21,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_order,Equipment Model Order,Display order of this part in the equipment_model table,property,,,,int,False,,22,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_Parameter_present,Equipment Model Has Parameter Present,Indicates whether and how a field appears in the equipment_model_has_Parameter table,property,,,,nvarchar(50),False,,23,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_Parameter_required,Equipment Model Has Parameter Required,Whether this part is required in the equipment_model_has_Parameter table,property,,,,bit,False,,24,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_Parameter_order,Equipment Model Has Parameter Order,Display order of this part in the equipment_model_has_Parameter table,property,,,,int,False,,25,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_procedures_present,Equipment Model Has Procedures Present,Indicates whether and how a field appears in the equipment_model_has_procedures table,property,,,,nvarchar(50),False,,26,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_procedures_required,Equipment Model Has Procedures Required,Whether this part is required in the equipment_model_has_procedures table,property,,,,bit,False,,27,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_procedures_order,Equipment Model Has Procedures Order,Display order of this part in the equipment_model_has_procedures table,property,,,,int,False,,28,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -hydrological_characteristics_present,Hydrological Characteristics Present,Indicates whether and how a field appears in the hydrological_characteristics table,property,,,,nvarchar(50),False,,29,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -hydrological_characteristics_required,Hydrological Characteristics Required,Whether this part is required in the hydrological_characteristics table,property,,,,bit,False,,30,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -hydrological_characteristics_order,Hydrological Characteristics Order,Display order of this part in the hydrological_characteristics table,property,,,,int,False,,31,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -metadata_present,Metadata Present,Indicates whether and how a field appears in the metadata table,property,,,,nvarchar(50),False,,32,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -metadata_required,Metadata Required,Whether this part is required in the metadata table,property,,,,bit,False,,33,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -metadata_order,Metadata Order,Display order of this part in the metadata table,property,,,,int,False,,34,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_present,Parameter Present,Indicates whether and how a field appears in the parameter table,property,,,,nvarchar(50),False,,35,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_required,Parameter Required,Whether this part is required in the parameter table,property,,,,bit,False,,36,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_order,Parameter Order,Display order of this part in the parameter table,property,,,,int,False,,37,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_has_procedures_present,Parameter Has Procedures Present,Indicates whether and how a field appears in the parameter_has_procedures table,property,,,,nvarchar(50),False,,38,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_has_procedures_required,Parameter Has Procedures Required,Whether this part is required in the parameter_has_procedures table,property,,,,bit,False,,39,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_has_procedures_order,Parameter Has Procedures Order,Display order of this part in the parameter_has_procedures table,property,,,,int,False,,40,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -procedures_present,Procedures Present,Indicates whether and how a field appears in the procedures table,property,,,,nvarchar(50),False,,41,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -procedures_required,Procedures Required,Whether this part is required in the procedures table,property,,,,bit,False,,42,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -procedures_order,Procedures Order,Display order of this part in the procedures table,property,,,,int,False,,43,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_present,Project Present,Indicates whether and how a field appears in the project table,property,,,,nvarchar(50),False,,44,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_required,Project Required,Whether this part is required in the project table,property,,,,bit,False,,45,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_order,Project Order,Display order of this part in the project table,property,,,,int,False,,46,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_contact_present,Project Has Contact Present,Indicates whether and how a field appears in the project_has_contact table,property,,,,nvarchar(50),False,,47,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_contact_required,Project Has Contact Required,Whether this part is required in the project_has_contact table,property,,,,bit,False,,48,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_contact_order,Project Has Contact Order,Display order of this part in the project_has_contact table,property,,,,int,False,,49,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_equipment_present,Project Has Equipment Present,Indicates whether and how a field appears in the project_has_equipment table,property,,,,nvarchar(50),False,,50,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_equipment_required,Project Has Equipment Required,Whether this part is required in the project_has_equipment table,property,,,,bit,False,,51,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_equipment_order,Project Has Equipment Order,Display order of this part in the project_has_equipment table,property,,,,int,False,,52,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_sampling_points_present,Project Has Sampling Points Present,Indicates whether and how a field appears in the project_has_sampling_points table,property,,,,nvarchar(50),False,,53,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_sampling_points_required,Project Has Sampling Points Required,Whether this part is required in the project_has_sampling_points table,property,,,,bit,False,,54,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_sampling_points_order,Project Has Sampling Points Order,Display order of this part in the project_has_sampling_points table,property,,,,int,False,,55,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -purpose_present,Purpose Present,Indicates whether and how a field appears in the purpose table,property,,,,nvarchar(50),False,,56,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -purpose_required,Purpose Required,Whether this part is required in the purpose table,property,,,,bit,False,,57,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -purpose_order,Purpose Order,Display order of this part in the purpose table,property,,,,int,False,,58,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -sampling_points_present,Sampling Points Present,Indicates whether and how a field appears in the sampling_points table,property,,,,nvarchar(50),False,,59,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -sampling_points_required,Sampling Points Required,Whether this part is required in the sampling_points table,property,,,,bit,False,,60,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -sampling_points_order,Sampling Points Order,Display order of this part in the sampling_points table,property,,,,int,False,,61,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_present,Site Present,Indicates whether and how a field appears in the site table,property,,,,nvarchar(50),False,,62,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_required,Site Required,Whether this part is required in the site table,property,,,,bit,False,,63,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_order,Site Order,Display order of this part in the site table,property,,,,int,False,,64,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -unit_present,Unit Present,Indicates whether and how a field appears in the unit table,property,,,,nvarchar(50),False,,65,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -unit_required,Unit Required,Whether this part is required in the unit table,property,,,,bit,False,,66,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -unit_order,Unit Order,Display order of this part in the unit table,property,,,,int,False,,67,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -urban_characteristics_present,Urban Characteristics Present,Indicates whether and how a field appears in the urban_characteristics table,property,,,,nvarchar(50),False,,68,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -urban_characteristics_required,Urban Characteristics Required,Whether this part is required in the urban_characteristics table,property,,,,bit,False,,69,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -urban_characteristics_order,Urban Characteristics Order,Display order of this part in the urban_characteristics table,property,,,,int,False,,70,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -value_present,Value Present,Indicates whether and how a field appears in the value table,property,,,,nvarchar(50),False,,71,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -value_required,Value Required,Whether this part is required in the value table,property,,,,bit,False,,72,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -value_order,Value Order,Display order of this part in the value table,property,,,,int,False,,73,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -watershed_present,Watershed Present,Indicates whether and how a field appears in the watershed table,property,,,,nvarchar(50),False,,74,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -watershed_required,Watershed Required,Whether this part is required in the watershed table,property,,,,bit,False,,75,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -watershed_order,Watershed Order,Display order of this part in the watershed table,property,,,,int,False,,76,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -weather_condition_present,Weather Condition Present,Indicates whether and how a field appears in the weather_condition table,property,,,,nvarchar(50),False,,77,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -weather_condition_required,Weather Condition Required,Whether this part is required in the weather_condition table,property,,,,bit,False,,78,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -weather_condition_order,Weather Condition Order,Display order of this part in the weather_condition table,property,,,,int,False,,79,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Part_type_set,Part Type Set,Valid values for part types,valueSet,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -table,Table,Represents a database table,valueSetMember,,Part_type_set,,nvarchar(50),,,1,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -key,Key,Represents a primary key,valueSetMember,,Part_type_set,,nvarchar(50),,,2,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -property,Property,Represents a column/field in a table,valueSetMember,,Part_type_set,,nvarchar(50),,,3,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -compositeKeyFirst,Composite Key First,First component of a composite primary key,valueSetMember,,Part_type_set,,nvarchar(50),,,4,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -compositeKeySecond,Composite Key Second,Second component of a composite primary key,valueSetMember,,Part_type_set,,nvarchar(50),,,5,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parentKey,Parent Key,Hierarchical reference to parent record in same table,valueSetMember,,Part_type_set,,nvarchar(50),,,6,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -valueSet,Value Set,Represents an enumeration or controlled vocabulary,valueSetMember,,Part_type_set,,nvarchar(50),,,7,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -valueSetMember,Value Set Member,Individual value within a value set,valueSetMember,,Part_type_set,,nvarchar(50),,,8,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -comments,Comments,Table for Comments,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact,Contact,Table for Contact,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment,Equipment,Table for Equipment,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model,Equipment Model,Table for Equipment Model,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_Parameter,Equipment Model Has Parameter,Table for Equipment Model Has Parameter,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -equipment_model_has_procedures,Equipment Model Has Procedures,Table for Equipment Model Has Procedures,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -hydrological_characteristics,Hydrological Characteristics,Table for Hydrological Characteristics,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -metadata,Metadata,Table for Metadata,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter,Parameter,Table for Parameter,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_has_procedures,Parameter Has Procedures,Table for Parameter Has Procedures,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -procedures,Procedures,Table for Procedures,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project,Project,Table for Project,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_contact,Project Has Contact,Table for Project Has Contact,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_equipment,Project Has Equipment,Table for Project Has Equipment,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_has_sampling_points,Project Has Sampling Points,Table for Project Has Sampling Points,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -purpose,Purpose,Table for Purpose,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -sampling_points,Sampling Points,Table for Sampling Points,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site,Site,Table for Site,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -unit,Unit,Table for Unit,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -urban_characteristics,Urban Characteristics,Table for Urban Characteristics,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -value,Value,Table for Value,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -watershed,Watershed,Table for Watershed,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -weather_condition,Weather Condition,Table for Weather Condition,table,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Agricultural,Agricultural,Agricultural in urban_characteristics table,property,,,,real,False,,7,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Comment,Comment,Comment in comments table,property,,,,ntext(1073741823),False,,2,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Comment_ID,Comment ID,"Identifier for comments, also used in 1 other table(s)",key,,,,int,False,,1,,key,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Commercial,Commercial,Commercial in urban_characteristics table,property,,,,real,False,,2,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Company,Company,Company in contact table,property,,,,ntext(1073741823),False,,4,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Concentration_time,Concentration Time,Concentration Time in watershed table,property,,,,int,False,,5,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Condition_ID,Condition ID,"Identifier for weather_condition, also used in 1 other table(s)",key,,,,int,False,,1,,,,,,,,,property,,,,,,,,,,,,,,,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Contact_ID,Contact ID,"Identifier for contact, also used in 2 other table(s)",key,,,,int,False,,1,,,key,,,,,,property,,,,,compositeKeySecond,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Cropland,Cropland,Cropland in hydrological_characteristics table,property,,,,real,False,,5,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Email,Email,Email in contact table,property,,,,nvarchar(100),False,,8,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Equipment_ID,Equipment ID,"Identifier for equipment, also used in 2 other table(s)",key,,,,int,False,,1,,,,key,,,,,property,,,,,,compositeKeySecond,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Equipment_identifier,Equipment IDentifier,Equipment IDentifier in equipment table,property,,,,nvarchar(100),False,,2,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Equipment_model,Equipment Model,Equipment Model in equipment_model table,property,,,,nvarchar(100),False,,2,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Equipment_model_ID,Equipment Model ID,"Identifier for equipment_model, also used in 3 other table(s)",key,,,,int,False,,1,,,,property,key,compositeKeyFirst,compositeKeyFirst,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -First_name,First Name,First Name in contact table,property,,,,nvarchar(255),False,,3,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Forest,Forest,Forest in hydrological_characteristics table,property,,,,real,False,,3,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Function,Function,Function in contact table,property,,,,ntext(1073741823),False,,6,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Functions,Functions,Functions in equipment_model table,property,,,,ntext(1073741823),False,,4,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Grassland,Grassland,Grassland in hydrological_characteristics table,property,,,,real,False,,7,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Green_spaces,Green Spaces,Green Spaces in urban_characteristics table,property,,,,real,False,,3,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Impervious_surface,Impervious Surface,Impervious Surface in watershed table,property,,,,real,False,,6,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Industrial,Industrial,Industrial in urban_characteristics table,property,,,,real,False,,4,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Institutional,Institutional,Institutional in urban_characteristics table,property,,,,real,False,,5,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Last_name,Last Name,Last Name in contact table,property,,,,nvarchar(100),False,,2,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Latitude_GPS,Latitude GPS,Latitude GPS in sampling_points table,property,,,,nvarchar(100),False,,5,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Linkedin,Linkedin,Linkedin in contact table,property,,,,nvarchar(100),False,,11,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Longitude_GPS,Longitude GPS,Longitude GPS in sampling_points table,property,,,,nvarchar(100),False,,6,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Manual_location,Manual Location,Manual Location in equipment_model table,property,,,,nvarchar(100),False,,6,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Manufacturer,Manufacturer,Manufacturer in equipment_model table,property,,,,nvarchar(100),False,,5,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Meadow,Meadow,Meadow in hydrological_characteristics table,property,,,,real,False,,6,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Metadata_ID,Metadata ID,"Identifier for metadata, also used in 1 other table(s)",key,,,,int,False,,1,,,,,,,,,key,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Method,Method,Method in equipment_model table,property,,,,nvarchar(100),False,,3,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Number_of_experiment,Number Of Experiment,Number Of Experiment in value table,property,,,,numeric,False,,3,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Office_number,Office Number,Office Number in contact table,property,,,,nvarchar(100),False,,7,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Owner,Owner,Owner in equipment table,property,,,,ntext(1073741823),False,,4,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Parameter,Parameter,Parameter in parameter table,property,,,,nvarchar(100),False,,2,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Parameter_ID,Parameter ID,"Identifier for parameter, also used in 3 other table(s)",compositeKeySecond,,,,int,False,,2,,,,,,compositeKeySecond,,,property,key,compositeKeyFirst,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Phone,Phone,Phone in contact table,property,,,,nvarchar(100),False,,9,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Picture,Picture,Picture in site table,property,,,,image(2147483647),False,,6,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Pictures,Pictures,Pictures in sampling_points table,property,,,,BLOB,False,,8,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Procedure_ID,Procedure ID,"Identifier for procedures, also used in 3 other table(s)",key,,,,int,False,,1,,,,,,,compositeKeySecond,,property,,compositeKeySecond,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Procedure_location,Procedure Location,Procedure Location in procedures table,property,,,,nvarchar(100),False,,5,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Procedure_name,Procedure Name,Procedure Name in procedures table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Procedure_type,Procedure Type,Procedure Type in procedures table,property,,,,nvarchar(255),False,,3,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Project_ID,Project ID,"Identifier for project, also used in 4 other table(s)",key,,,,int,False,,1,,,,,,,,,property,,,,key,compositeKeyFirst,compositeKeyFirst,compositeKeyFirst,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Project_name,Project Name,Project Name in project table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Province,Province,Province in site table,property,,,,nvarchar(255),False,,11,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Purchase_date,Purchase Date,Purchase Date in equipment table,property,,,,date,False,,6,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Purpose,Purpose,Purpose in purpose table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Purpose_ID,Purpose ID,"Identifier for purpose, also used in 1 other table(s)",key,,,,int,False,,1,,,,,,,,,property,,,,,,,,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Recreational,Recreational,Recreational in urban_characteristics table,property,,,,real,False,,8,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Residential,Residential,Residential in urban_characteristics table,property,,,,real,False,,6,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Sampling_location,Sampling Location,Sampling Location in sampling_points table,property,,,,nvarchar(100),False,,3,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Sampling_point,Sampling Point,Sampling Point in sampling_points table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Sampling_point_ID,Sampling Point ID,"Identifier for sampling_points, also used in 2 other table(s)",key,,,,int,False,,1,,,,,,,,,property,,,,,,,compositeKeySecond,,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Serial_number,Serial Number,Serial Number in equipment table,property,,,,nvarchar(100),False,,3,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Site_ID,Site ID,"Identifier for site, also used in 1 other table(s)",key,,,,int,False,,1,,,,,,,,,,,,,,,,,,property,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Site_name,Site Name,Site Name in site table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Site_type,Site Type,Site Type in site table,property,,,,nvarchar(255),False,,3,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Skype_name,Skype Name,Skype Name in contact table,property,,,,nvarchar(100),False,,10,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Status,Status,Status in contact table,property,,,,nvarchar(255),False,,5,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Storage_location,Storage Location,Storage Location in equipment table,property,,,,nvarchar(100),False,,5,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Surface_area,Surface Area,Surface Area in watershed table,property,,,,real,False,,4,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Timestamp,Timestamp,Timestamp in value table,property,,,,int,False,,6,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Unit,Unit,Unit in unit table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Unit_ID,Unit ID,"Identifier for unit, also used in 2 other table(s)",key,,,,int,False,,1,,,,,,,,,property,property,,,,,,,,,,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Urban_area,Urban Area,Urban Area in hydrological_characteristics table,property,,,,real,False,,2,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Value,Value,Value in value table,property,,,,float,False,,2,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Value_ID,Value ID,Unique identifier for value,key,,,,int,False,,1,,,,,,,,,,,,,,,,,,,,,,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Watershed_ID,Watershed ID,"Identifier for hydrological_characteristics, also used in 3 other table(s)",key,,,,int,False,,1,,,,,,,,key,,,,,,,,,,,property,,key,,key,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Watershed_name,Watershed Name,Watershed Name in watershed table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Weather_condition,Weather Condition,Weather Condition in weather_condition table,property,,,,nvarchar(100),False,,2,,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Website,Website,Website in contact table,property,,,,nvarchar(60),False,,17,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -Wetlands,Wetlands,Wetlands in hydrological_characteristics table,property,,,,real,False,,4,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_City,Contact City,Contact City in contact table,property,,,,nvarchar(255),False,,14,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_Country,Contact Country,Contact Country in contact table,property,,,,nvarchar(255),False,,16,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_Street_name,Contact Street Name,Contact Street Name in contact table,property,,,,nvarchar(100),False,,13,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_Street_number,Contact Street Number,Contact Street Number in contact table,property,,,,nvarchar(100),False,,12,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -contact_Zip_code,Contact Zip Code,Contact Zip Code in contact table,property,,,,nvarchar(45),False,,15,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -parameter_Description,Parameter Description,Parameter Description in parameter table,property,,,,ntext(1073741823),False,,4,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -procedures_Description,Procedures Description,Procedures Description in procedures table,property,,,,ntext(1073741823),False,,4,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -project_Description,Project Description,Project Description in project table,property,,,,ntext(1073741823),False,,3,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -purpose_Description,Purpose Description,Purpose Description in purpose table,property,,,,ntext(1073741823),False,,3,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -sampling_points_Description,Sampling Points Description,Sampling Points Description in sampling_points table,property,,,,ntext(1073741823),False,,7,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_City,Site City,Site City in site table,property,,,,nvarchar(255),False,,9,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_Country,Site Country,Site Country in site table,property,,,,nvarchar(255),False,,12,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_Description,Site Description,Site Description in site table,property,,,,ntext(1073741823),False,,5,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_Street_name,Site Street Name,Site Street Name in site table,property,,,,nvarchar(100),False,,8,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_Street_number,Site Street Number,Site Street Number in site table,property,,,,nvarchar(100),False,,7,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -site_Zip_code,Site Zip Code,Site Zip Code in site table,property,,,,nvarchar(100),False,,10,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -watershed_Description,Watershed Description,Watershed Description in watershed table,property,,,,ntext(1073741823),False,,3,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, -weather_condition_Description,Weather Condition Description,Weather Condition Description in weather_condition table,property,,,,ntext(1073741823),False,,3,,,,,,,,,,,,,,,,,,,,,,,,property,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/src/dictionary.json b/src/dictionary.json new file mode 100644 index 0000000..6d69667 --- /dev/null +++ b/src/dictionary.json @@ -0,0 +1,2064 @@ +{ + "parts": [ + { + "Part_ID": "Part_type_set", + "Label": "Part Type Set", + "Description": "Valid values for part types", + "Part_type": "valueSet", + "Sort_order": null + }, + { + "Part_ID": "table", + "Label": "Table", + "Description": "Represents a database table", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 1 + }, + { + "Part_ID": "key", + "Label": "Key", + "Description": "Represents a primary key", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 2 + }, + { + "Part_ID": "property", + "Label": "Property", + "Description": "Represents a column/field in a table", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 3 + }, + { + "Part_ID": "compositeKeyFirst", + "Label": "Composite Key First", + "Description": "First component of a composite primary key", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 4 + }, + { + "Part_ID": "compositeKeySecond", + "Label": "Composite Key Second", + "Description": "Second component of a composite primary key", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 5 + }, + { + "Part_ID": "parentKey", + "Label": "Parent Key", + "Description": "Hierarchical reference to parent record in same table", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 6 + }, + { + "Part_ID": "valueSet", + "Label": "Value Set", + "Description": "Represents an enumeration or controlled vocabulary", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 7 + }, + { + "Part_ID": "valueSetMember", + "Label": "Value Set Member", + "Description": "Individual value within a value set", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "Part_type_set", + "Sort_order": 8 + }, + { + "Part_ID": "comments", + "Label": "Comments", + "Description": "Stores any additional textual comments, notes, or observations related to a specific measured value", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "contact", + "Label": "Contact", + "Description": "Stores detailed personal and professional information for people involved in projects (e.g., name, affiliation, function, e-mail, phone)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "equipment", + "Label": "Equipment", + "Description": "Stores information about a specific, physical piece of equipment (e.g., serial number, owner, purchase date, storage location)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "equipment_model", + "Label": "Equipment Model", + "Description": "Stores detailed, non-redundant specifications for a specific sensor or instrument model (e.g., manufacturer, functions, method)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "equipment_model_has_Parameter", + "Label": "Equipment Model Has Parameter", + "Description": "Links equipment models to the parameters they can measure", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "equipment_model_has_procedures", + "Label": "Equipment Model Has Procedures", + "Description": "Links equipment models to the relevant maintenance procedures", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "hydrological_characteristics", + "Label": "Hydrological Characteristics", + "Description": "Stores the hydrological land use percentages (e.g., forest, wetlands, cropland, grassland) within the watershed", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "metadata", + "Label": "Metadata", + "Description": "Contains a list of all existing unique metadata combinations (represented by a series of foreign keys/IDs) that describe a single measurement", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "parameter", + "Label": "Parameter", + "Description": "Stores the different water quality or quantity parameters that are measured (e.g., pH, TSS, N-components)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "parameter_has_procedures", + "Label": "Parameter Has Procedures", + "Description": "Links parameters to the relevant measurement procedures", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "procedures", + "Label": "Procedures", + "Description": "Stores details for different measurement procedures (e.g., calibration, validation, standard operating procedures, ISO methods)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "project", + "Label": "Project", + "Description": "Stores descriptive information about the research or monitoring project for which the data was collected", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "project_has_contact", + "Label": "Project Has Contact", + "Description": "Links projects to the personnel involved in them", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "project_has_equipment", + "Label": "Project Has Equipment", + "Description": "Links projects to the specific equipment used within them", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "project_has_sampling_points", + "Label": "Project Has Sampling Points", + "Description": "Links projects to the sampling points used within them", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "purpose", + "Label": "Purpose", + "Description": "Stores information about the aim of the measurement (e.g., on-line measurement, laboratory analysis, calibration, validation, cleaning)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "sampling_points", + "Label": "Sampling Points", + "Description": "Stores the identification, specific geographical coordinates (Latitude/Longitude/GPS), and description of a particular spot where a sample or measurement is taken", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "site", + "Label": "Site", + "Description": "Stores general site information, including address, site type, and a link to the associated watershed", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "unit", + "Label": "Unit", + "Description": "Stores the SI units of measurement (or other relevant units) corresponding to the parameters (e.g., mg/L, g/L, s)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "urban_characteristics", + "Label": "Urban Characteristics", + "Description": "Stores the urban land use percentages (e.g., commercial, residential, green spaces) within the watershed", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "value", + "Label": "Value", + "Description": "Stores each measured water quality or quantity value, its time stamp, replicate identification, and the link to its specific metadata set", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "watershed", + "Label": "Watershed", + "Description": "Stores general information about the watershed area, including surface area, concentration time, and impervious surface percentage", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "weather_condition", + "Label": "Weather Condition", + "Description": "Stores descriptive information about the prevailing weather conditions when the measurement was taken (e.g., dry weather, wet weather, snow melt)", + "Part_type": "table", + "Sort_order": null + }, + { + "Part_ID": "Agricultural", + "Label": "Agricultural", + "Description": "Percentage [%] of agricultural land use. For example farm land", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 7, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Comment", + "Label": "Comment", + "Description": "Comment on the data in the Value table", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "comments": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Comment_ID", + "Label": "Comment ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "comments": { + "role": "key", + "required": false, + "order": 999 + }, + "value": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + } + } + }, + { + "Part_ID": "Commercial", + "Label": "Commercial", + "Description": "Percentage [%] of commercial areas. For example stores or bank areas", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Company", + "Label": "Company", + "Description": "Company name", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Concentration_time", + "Label": "Concentration Time", + "Description": "Concentration time in minutes [min]", + "Part_type": "property", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "watershed": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Condition_ID", + "Label": "Condition ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "weather_condition": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Contact_ID", + "Label": "Contact ID", + "Description": "Link to the Contact table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "contact": { + "role": "key", + "required": false, + "order": 999 + }, + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "project_has_contact": { + "role": "compositeKeySecond", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + } + } + }, + { + "Part_ID": "Cropland", + "Label": "Cropland", + "Description": "Percentage [%] of croplands", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "hydrological_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Email", + "Label": "Email", + "Description": "E-mail address", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 8, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Equipment_ID", + "Label": "Equipment ID", + "Description": "Link to the Equipment table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "equipment": { + "role": "key", + "required": false, + "order": 999 + }, + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "project_has_equipment": { + "role": "compositeKeySecond", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + } + } + }, + { + "Part_ID": "Equipment_identifier", + "Label": "Equipment IDentifier", + "Description": "Identification name of the equipments", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "equipment": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Equipment_model", + "Label": "Equipment Model", + "Description": "Name of the equipment model. For example: ammo::lyser", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "equipment_model": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Equipment_model_ID", + "Label": "Equipment Model ID", + "Description": "Link to the Equipment model table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "equipment": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "equipment_model": { + "role": "key", + "required": false, + "order": 999 + }, + "equipment_model_has_Parameter": { + "role": "compositeKeyFirst", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + }, + "equipment_model_has_procedures": { + "role": "compositeKeyFirst", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + } + } + }, + { + "Part_ID": "First_name", + "Label": "First Name", + "Description": "First name of the contact", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Forest", + "Label": "Forest", + "Description": "Percentage [%] of forest areas", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "hydrological_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Function", + "Label": "Function", + "Description": "More detailed description about the functions", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Functions", + "Label": "Functions", + "Description": "Description of the functions of the equipment", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "equipment_model": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Grassland", + "Label": "Grassland", + "Description": "Percentage [%] of grasslands", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 7, + "table_presence": { + "hydrological_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Green_spaces", + "Label": "Green Spaces", + "Description": "Percentage [%] of green spaces", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Impervious_surface", + "Label": "Impervious Surface", + "Description": "Percentage of the impervious surface of the watershed in percentage [%]", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "watershed": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Industrial", + "Label": "Industrial", + "Description": "Percentage [%] of industrial areas. For example factories", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Institutional", + "Label": "Institutional", + "Description": "Percentage [%] of institutional areas. For example schools, police stations or city hall", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Last_name", + "Label": "Last Name", + "Description": "Last name of the contact", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Latitude_GPS", + "Label": "Latitude GPS", + "Description": "GPS coordinates. For example: 47°54′25.103\" ", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Linkedin", + "Label": "Linkedin", + "Description": "LinkedIn account", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 11, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Longitude_GPS", + "Label": "Longitude GPS", + "Description": "GPS coordinates. For example: $73^{\\circ}47^{\\prime}00.024^{\\prime\\prime}$", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Manual_location", + "Label": "Manual Location", + "Description": "Location where the manual is stored", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "equipment_model": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Manufacturer", + "Label": "Manufacturer", + "Description": "Name of the manufacturer", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "equipment_model": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Meadow", + "Label": "Meadow", + "Description": "Percentage [%] of meadow areas", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "hydrological_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Metadata_ID", + "Label": "Metadata ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "metadata": { + "role": "key", + "required": false, + "order": 999 + }, + "value": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + } + } + }, + { + "Part_ID": "Method", + "Label": "Method", + "Description": "Method behind the equipment", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "equipment_model": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Number_of_experiment", + "Label": "Number Of Experiment", + "Description": "Number of replica of an experiment", + "Part_type": "property", + "SQL_data_type": "numeric", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "value": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Office_number", + "Label": "Office Number", + "Description": "Number of the office", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 7, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Owner", + "Label": "Owner", + "Description": "Name of the owner of the equipment", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "equipment": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Parameter", + "Label": "Parameter", + "Description": "Name of the parameter", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "parameter": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Parameter_ID", + "Label": "Parameter ID", + "Description": "Link to the Parameter table", + "Part_type": "compositeKeySecond", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "equipment_model_has_Parameter": { + "role": "compositeKeySecond", + "required": false, + "order": 999 + }, + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "parameter": { + "role": "key", + "required": false, + "order": 999 + }, + "parameter_has_procedures": { + "role": "compositeKeyFirst", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Phone", + "Label": "Phone", + "Description": "Phone number", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 9, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Picture", + "Label": "Picture", + "Description": "Picture of the site", + "Part_type": "property", + "SQL_data_type": "image(2147483647)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Pictures", + "Label": "Pictures", + "Description": "Picture of the site", + "Part_type": "property", + "SQL_data_type": "BLOB", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 8, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Procedure_ID", + "Label": "Procedure ID", + "Description": "Link to the Procedures table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "equipment_model_has_procedures": { + "role": "compositeKeySecond", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + }, + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "parameter_has_procedures": { + "role": "compositeKeySecond", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + }, + "procedures": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Procedure_location", + "Label": "Procedure Location", + "Description": "Where is the procedure stored", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "procedures": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Procedure_name", + "Label": "Procedure Name", + "Description": "Title name of the procedure", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "procedures": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Procedure_type", + "Label": "Procedure Type", + "Description": "Type of the procedure. For example, SOP", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "procedures": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Project_ID", + "Label": "Project ID", + "Description": "Link to the Project table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "project": { + "role": "key", + "required": false, + "order": 999 + }, + "project_has_contact": { + "role": "compositeKeyFirst", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + }, + "project_has_equipment": { + "role": "compositeKeyFirst", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + }, + "project_has_sampling_points": { + "role": "compositeKeyFirst", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + } + } + }, + { + "Part_ID": "Project_name", + "Label": "Project Name", + "Description": "Name of the project", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "project": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Province", + "Label": "Province", + "Description": "Address: name of the province", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 11, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Purchase_date", + "Label": "Purchase Date", + "Description": "Date when the equipment was bought: 'YYYY-MM-DD", + "Part_type": "property", + "SQL_data_type": "date", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "equipment": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Purpose", + "Label": "Purpose", + "Description": "Purpose of the data collection. For example, \"Measurement\", \"Lab_analysis\", \"Calibration\" and \"Cleaning\"", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "purpose": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Purpose_ID", + "Label": "Purpose ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "purpose": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Recreational", + "Label": "Recreational", + "Description": "Percentage [%] of recreational areas. For example parks or sport fields", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 8, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Residential", + "Label": "Residential", + "Description": "Percentage [%] of residential areas. For example houses or apartment buildings", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "urban_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Sampling_location", + "Label": "Sampling Location", + "Description": "Where the sample was taken. For example: \"Biofiltration\", \"Sewer 01\" or \"Retention Tank\"", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Sampling_point", + "Label": "Sampling Point", + "Description": "Where the sample was taken. For example: \"Inlet\", \"Outlet\" or \"Upstream\"", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Sampling_point_ID", + "Label": "Sampling Point ID", + "Description": "Link to the Sampling_point table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "project_has_sampling_points": { + "role": "compositeKeySecond", + "required": false, + "order": 999, + "relationship_type": "many-to-many" + }, + "sampling_points": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Serial_number", + "Label": "Serial Number", + "Description": "Serial number of the equipment", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "equipment": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Site_ID", + "Label": "Site ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "site": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Site_name", + "Label": "Site Name", + "Description": "Name of the site", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Site_type", + "Label": "Site Type", + "Description": "For example: \"WWTP\", \"River\" or \"Sewer_system\"", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Skype_name", + "Label": "Skype Name", + "Description": "Skype name", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 10, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Status", + "Label": "Status", + "Description": "Status of the person. For example: \"Master student\", \"Postdoc\" or \"Intern\"", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Storage_location", + "Label": "Storage Location", + "Description": "Where is the procedure stored", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "equipment": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Surface_area", + "Label": "Surface Area", + "Description": "Surface area of the watershed [ha]", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "watershed": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Timestamp", + "Label": "Timestamp", + "Description": "Unix timestamp combining date and time of collected data", + "Part_type": "property", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 6, + "table_presence": { + "value": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Unit", + "Label": "Unit", + "Description": "SI-units only", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "unit": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Unit_ID", + "Label": "Unit ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "metadata": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "parameter": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "unit": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Urban_area", + "Label": "Urban Area", + "Description": "Percentage [%] of urban areas", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "hydrological_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Value", + "Label": "Value", + "Description": "Value of collected data", + "Part_type": "property", + "SQL_data_type": "float", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "value": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Value_ID", + "Label": "Value ID", + "Description": "A unique ID is generated automatically by MySQL", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "value": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Watershed_ID", + "Label": "Watershed ID", + "Description": "Linked to the Watershed table", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 1, + "table_presence": { + "hydrological_characteristics": { + "role": "key", + "required": false, + "order": 999, + "relationship_type": "one-to-one" + }, + "site": { + "role": "property", + "required": false, + "order": 999, + "relationship_type": "one-to-many" + }, + "urban_characteristics": { + "role": "key", + "required": false, + "order": 999, + "relationship_type": "one-to-one" + }, + "watershed": { + "role": "key", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Watershed_name", + "Label": "Watershed Name", + "Description": "Name of the watershed", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "watershed": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Weather_condition", + "Label": "Weather Condition", + "Description": "Type of weather condition", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 2, + "table_presence": { + "weather_condition": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Website", + "Label": "Website", + "Description": "Website URL of the contact or organization", + "Part_type": "property", + "SQL_data_type": "nvarchar(60)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 17, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "Wetlands", + "Label": "Wetlands", + "Description": "Percentage [%] of wetlands", + "Part_type": "property", + "SQL_data_type": "real", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "hydrological_characteristics": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "contact_City", + "Label": "Contact City", + "Description": "Address: name of the city", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 14, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "contact_Country", + "Label": "Contact Country", + "Description": "Address: name of the country", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 16, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "contact_Street_name", + "Label": "Contact Street Name", + "Description": "Address: name of the street", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 13, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "contact_Street_number", + "Label": "Contact Street Number", + "Description": "Address: number of the street", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 12, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "contact_Zip_code", + "Label": "Contact Zip Code", + "Description": "Address: zip code", + "Part_type": "property", + "SQL_data_type": "nvarchar(45)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 15, + "table_presence": { + "contact": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "parameter_Description", + "Label": "Parameter Description", + "Description": "Description of the parameter", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "parameter": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "procedures_Description", + "Label": "Procedures Description", + "Description": "Description of the procedure", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 4, + "table_presence": { + "procedures": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "project_Description", + "Label": "Project Description", + "Description": "Description of the project", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "project": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "purpose_Description", + "Label": "Purpose Description", + "Description": "Description of the purpose", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "purpose": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "sampling_points_Description", + "Label": "Sampling Points Description", + "Description": "Description of the sampling point", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 7, + "table_presence": { + "sampling_points": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "site_City", + "Label": "Site City", + "Description": "Address: name of the city", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 9, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "site_Country", + "Label": "Site Country", + "Description": "Address: name of the country", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 12, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "site_Description", + "Label": "Site Description", + "Description": "Description of the site", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 5, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "site_Street_name", + "Label": "Site Street Name", + "Description": "Address: name of the street", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 8, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "site_Street_number", + "Label": "Site Street Number", + "Description": "Address: number of the street", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 7, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "site_Zip_code", + "Label": "Site Zip Code", + "Description": "Address: zip code", + "Part_type": "property", + "SQL_data_type": "nvarchar(100)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 10, + "table_presence": { + "site": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "watershed_Description", + "Label": "Watershed Description", + "Description": "Description of the watershed", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "watershed": { + "role": "property", + "required": false, + "order": 999 + } + } + }, + { + "Part_ID": "weather_condition_Description", + "Label": "Weather Condition Description", + "Description": "Description of the condition", + "Part_type": "property", + "SQL_data_type": "ntext(1073741823)", + "Is_required": false, + "Default_value": null, + "Value_set_part_ID": null, + "Sort_order": 3, + "table_presence": { + "weather_condition": { + "role": "property", + "required": false, + "order": 999 + } + } + } + ] +} \ No newline at end of file diff --git a/src/open_dateaubase/__init__.py b/src/open_dateaubase/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/open_dateaubase/data_model/__init__.py b/src/open_dateaubase/data_model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/open_dateaubase/data_model/helpers.py b/src/open_dateaubase/data_model/helpers.py new file mode 100644 index 0000000..450e0ac --- /dev/null +++ b/src/open_dateaubase/data_model/helpers.py @@ -0,0 +1,431 @@ +""" +Helper functions for manipulating the dictionary. + +Usage: + from open_dateaubase.helpers import DictionaryManager + + mgr = DictionaryManager.load("src/dictionary.json") + mgr.create_value_set("Status_set", "Valid status values") + mgr.add_value_set_member("Status_set", "active", "Active status", order=1) + mgr.save() +""" + +from pathlib import Path +from typing import Optional, Literal +import json +from .models import ( + Dictionary, + TablePart, + KeyPart, + PropertyPart, + CompositeKeyFirstPart, + CompositeKeySecondPart, + ParentKeyPart, + ValueSetPart, + ValueSetMemberPart, + TablePresence, + Part, +) + + +class DictionaryManager: + """Manages dictionary operations with validation.""" + + def __init__(self, dictionary: Dictionary, path: Path): + self.dictionary = dictionary + self.path = path + + @classmethod + def load(cls, path: str | Path | None = None) -> "DictionaryManager": + """Load dictionary from JSON file with validation.""" + if path is None: + # Default path when no path specified + from importlib.resources import files + path = files('open_dateaubase').joinpath('dictionary.json') + + path = Path(path) if isinstance(path, str) else path + with open(path, "r", encoding="utf-8") as f: + raw_data = json.load(f) + dictionary = Dictionary.model_validate(raw_data) + return cls(dictionary, path) + + def save(self, path: Optional[Path] = None) -> None: + """Save dictionary to JSON file.""" + target = path or self.path + # Export as dict, convert to JSON with PascalCase keys + data = self.dictionary.model_dump(by_alias=True) + with open(target, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + print(f"Dictionary saved to {target}") + + def _find_part(self, part_id: str) -> Optional[Part]: + """Find a part by Part_ID.""" + for part in self.dictionary.parts: + if part.part_id == part_id: + return part + return None + + def _part_exists(self, part_id: str) -> bool: + """Check if a part exists.""" + return self._find_part(part_id) is not None + + # ======================================================================== + # Value Set Operations + # ======================================================================== + + def create_value_set(self, part_id: str, label: str, description: str) -> None: + """Create a new value set.""" + if self._part_exists(part_id): + raise ValueError(f"Part '{part_id}' already exists") + + value_set = ValueSetPart( + Part_ID=part_id, Label=label, Description=description, Part_type="valueSet" + ) + self.dictionary.parts.append(value_set) + # Re-validate entire dictionary + self.dictionary = Dictionary.model_validate( + self.dictionary.model_dump(by_alias=True) + ) + print(f"Created value set '{part_id}'") + + def add_value_set_member( + self, + value_set_id: str, + member_id: str, + label: str, + description: str, + order: int = 999, + ) -> None: + """Add a member to a value set.""" + if not self._part_exists(value_set_id): + raise ValueError(f"Value set '{value_set_id}' does not exist") + + if self._part_exists(member_id): + raise ValueError(f"Part '{member_id}' already exists") + + member = ValueSetMemberPart( + Part_ID=member_id, + Label=label, + Description=description, + Part_type="valueSetMember", + Member_of_set_part_ID=value_set_id, + Sort_order=order, + ) + self.dictionary.parts.append(member) + # Re-validate + self.dictionary = Dictionary.model_validate( + self.dictionary.model_dump(by_alias=True) + ) + print(f"Added member '{member_id}' to value set '{value_set_id}'") + + # ======================================================================== + # Table Operations + # ======================================================================== + + def create_table(self, table_id: str, label: str, description: str) -> None: + """Create a new table.""" + if self._part_exists(table_id): + raise ValueError(f"Part '{table_id}' already exists") + + table = TablePart( + Part_ID=table_id, Label=label, Description=description, Part_type="table" + ) + self.dictionary.parts.append(table) + # Re-validate + self.dictionary = Dictionary.model_validate( + self.dictionary.model_dump(by_alias=True) + ) + print(f"Created table '{table_id}'") + + def add_field_to_table( + self, + table_id: str, + field_id: str, + label: str, + description: str, + role: Literal[ + "key", "property", "compositeKeyFirst", "compositeKeySecond" + ] = "property", + sql_data_type: str = "nvarchar(255)", + required: bool = False, + order: int = 999, + value_set_id: Optional[str] = None, + default_value: Optional[str] = None, + ) -> None: + """Add a field to a table (or update existing field's table_presence).""" + if not self._part_exists(table_id): + raise ValueError(f"Table '{table_id}' does not exist") + + existing_part = self._find_part(field_id) + + if existing_part: + # Field exists - update its table_presence + if not hasattr(existing_part, "table_presence"): + raise ValueError(f"Part '{field_id}' exists but is not a field type") + + # Add table presence + existing_part.table_presence[table_id] = TablePresence( + role=role, required=required, order=order + ) + print(f"Added '{field_id}' to table '{table_id}' with role '{role}'") + else: + # Create new field + presence = { + table_id: TablePresence(role=role, required=required, order=order) + } + + # Determine field class based on role + if role == "key": + field_class = KeyPart + elif role == "compositeKeyFirst": + field_class = CompositeKeyFirstPart + elif role == "compositeKeySecond": + field_class = CompositeKeySecondPart + else: + field_class = PropertyPart + + field_kwargs = { + "Part_ID": field_id, + "Label": label, + "Description": description, + "Part_type": role if role != "property" else "property", + "SQL_data_type": sql_data_type, + "Is_required": required, + "Default_value": default_value, + "table_presence": presence, + } + + if value_set_id: + field_kwargs["Value_set_part_ID"] = value_set_id + + field = field_class(**field_kwargs) + self.dictionary.parts.append(field) + print(f"Created field '{field_id}' in table '{table_id}'") + + # Re-validate entire dictionary + self.dictionary = Dictionary.model_validate( + self.dictionary.model_dump(by_alias=True) + ) + + def add_parent_key( + self, + table_id: str, + parent_key_id: str, + ancestor_key_id: str, + label: str, + description: str, + sql_data_type: str = "int", + required: bool = False, + order: int = 999, + ) -> None: + """Add a hierarchical parent key to a table.""" + if not self._part_exists(table_id): + raise ValueError(f"Table '{table_id}' does not exist") + + if not self._part_exists(ancestor_key_id): + raise ValueError(f"Ancestor key '{ancestor_key_id}' does not exist") + + if self._part_exists(parent_key_id): + raise ValueError(f"Part '{parent_key_id}' already exists") + + parent_key = ParentKeyPart( + Part_ID=parent_key_id, + Label=label, + Description=description, + Part_type="parentKey", + Ancestor_part_ID=ancestor_key_id, + SQL_data_type=sql_data_type, + Is_required=required, + table_presence={ + table_id: TablePresence(role="property", required=required, order=order) + }, + ) + self.dictionary.parts.append(parent_key) + # Re-validate + self.dictionary = Dictionary.model_validate( + self.dictionary.model_dump(by_alias=True) + ) + print(f"Added parent key '{parent_key_id}' to table '{table_id}'") + + # ======================================================================== + # Validation & Integrity + # ======================================================================== + + def validate(self) -> None: + """Explicitly validate the dictionary.""" + try: + Dictionary.model_validate(self.dictionary.model_dump(by_alias=True)) + print("Dictionary is valid!") + except Exception as e: + print(f"Validation failed: {e}") + raise + + def list_tables(self) -> list[str]: + """List all table Part_IDs.""" + return [ + part.part_id for part in self.dictionary.parts if part.part_type == "table" + ] + + def list_value_sets(self) -> list[str]: + """List all value set Part_IDs.""" + return [ + part.part_id + for part in self.dictionary.parts + if part.part_type == "valueSet" + ] + + # ======================================================================== + # Query Operations (replacing old SQL queries) + # ======================================================================== + + def get_value_set_members(self, field_id: str) -> list[dict]: + """Get all valid values for a field's value set constraint. + + Args: + field_id: The Part_ID of the field to check + + Returns: + List of dictionaries with Part_ID, Label, Description for each member + """ + field = self._find_part(field_id) + if not field: + return [] + + # Check if field has a value set constraint + value_set_id = getattr(field, "value_set_part_id", None) + if not value_set_id: + return [] + + # Find all members of this value set + members = [] + for part in self.dictionary.parts: + if ( + hasattr(part, "member_of_set_part_id") + and part.member_of_set_part_id == value_set_id + ): + members.append( + { + "Part_ID": part.part_id, + "Label": part.label, + "Description": part.description, + "Sort_order": getattr(part, "sort_order", 999), + } + ) + + # Sort by sort_order + members.sort(key=lambda x: x["Sort_order"]) + return members + + def get_table_columns(self, table_id: str) -> list[dict]: + """Get all columns that appear in a specific table. + + Args: + table_id: The Part_ID of the table + + Returns: + List of dictionaries with column metadata + """ + columns = [] + for part in self.dictionary.parts: + # Only field parts have table_presence + if hasattr(part, "table_presence") and part.table_presence: + if table_id in part.table_presence: + presence = part.table_presence[table_id] + columns.append( + { + "Part_ID": part.part_id, + "Label": part.label, + "SQL_data_type": getattr(part, "sql_data_type", None), + "Is_required": presence.required, + "Role": presence.role, + "Order": presence.order, + } + ) + + # Sort by order + columns.sort(key=lambda x: x["Order"]) + return columns + + def get_field_tables(self, field_id: str) -> list[dict]: + """Find all tables where a specific field appears. + + Args: + field_id: The Part_ID of the field + + Returns: + List of dictionaries with table and role information + """ + field = self._find_part(field_id) + if not field or not hasattr(field, "table_presence"): + return [] + + tables = [] + for table_id, presence in field.table_presence.items(): + tables.append( + { + "Table_ID": table_id, + "Role": presence.role, + "Required": presence.required, + "Order": presence.order, + } + ) + + return tables + + def get_primary_keys(self) -> list[dict]: + """Find all primary keys in the database. + + Returns: + List of dictionaries with primary key information + """ + primary_keys = [] + for part in self.dictionary.parts: + if part.part_type == "key": + # Find which table this key belongs to + tables = [] + if hasattr(part, "table_presence") and part.table_presence: + for table_id, presence in part.table_presence.items(): + if presence.role == "key": + tables.append(table_id) + + primary_keys.append( + { + "Part_ID": part.part_id, + "Label": part.label, + "SQL_data_type": getattr(part, "sql_data_type", None), + "Primary_in_tables": tables, + } + ) + + return primary_keys + + def get_shared_fields(self) -> list[dict]: + """Find fields that appear in multiple tables. + + Returns: + List of dictionaries with shared field information + """ + shared_fields = [] + for part in self.dictionary.parts: + if ( + hasattr(part, "table_presence") + and part.table_presence + and len(part.table_presence) > 1 + ): + tables = [] + for table_id, presence in part.table_presence.items(): + tables.append({"Table_ID": table_id, "Role": presence.role}) + + shared_fields.append( + { + "Part_ID": part.part_id, + "Label": part.label, + "Part_type": part.part_type, + "Table_count": len(part.table_presence), + "Tables": tables, + } + ) + + # Sort by table count (most shared first) + shared_fields.sort(key=lambda x: x["Table_count"], reverse=True) + return shared_fields diff --git a/src/open_dateaubase/data_model/models.py b/src/open_dateaubase/data_model/models.py new file mode 100644 index 0000000..555d948 --- /dev/null +++ b/src/open_dateaubase/data_model/models.py @@ -0,0 +1,355 @@ +""" +Pydantic models for the open_dateaubase dictionary. + +This module defines the type-safe schema for the dictionary using discriminated +unions to enforce Part_type-specific validation rules. +""" + +from typing import Literal, Union, Dict, Optional, List, Any, Annotated +from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict + + +# ============================================================================ +# Table Presence Metadata +# ============================================================================ + + +class TablePresence(BaseModel): + """Metadata about how a field appears in a specific table.""" + + model_config = ConfigDict(frozen=True) # Immutable for safety + + role: Literal["key", "property", "compositeKeyFirst", "compositeKeySecond"] + required: bool = False + order: int = Field(ge=1, description="Display order in table (1-indexed)") + + # Foreign key relationship metadata + relationship_type: Optional[ + Literal["one-to-one", "one-to-many", "many-to-many"] + ] = Field( + None, + description="Type of relationship this FK represents. Set when this field is a foreign key.", + ) + + @model_validator(mode="after") + def validate_fk_consistency(self): + """Ensure FK metadata is consistent.""" + has_relationship = self.relationship_type is not None + + # Validate relationship types match expected roles + if has_relationship: + # one-to-one: FK should typically be a key (though property is also valid) + # one-to-many: FK should be a property (regular column in child table) + # many-to-many: FK should be part of composite key in junction table + + if self.relationship_type == "one-to-one" and self.role not in [ + "key", + "property", + ]: + raise ValueError( + f"one-to-one relationships require role='key' or 'property', got '{self.role}'" + ) + + if self.relationship_type == "one-to-many" and self.role not in [ + "property", + "compositeKeyFirst", + "compositeKeySecond", + ]: + raise ValueError( + f"one-to-many relationships typically require role='property', got '{self.role}'" + ) + + if self.relationship_type == "many-to-many" and self.role not in [ + "compositeKeyFirst", + "compositeKeySecond", + ]: + raise ValueError( + f"many-to-many relationships require composite key roles, got '{self.role}'" + ) + + return self + + +# ============================================================================ +# Base Part Model +# ============================================================================ + + +class PartBase(BaseModel): + """Base model for all dictionary parts.""" + + model_config = ConfigDict( + populate_by_name=True + ) # Allow both snake_case and PascalCase + + part_id: str = Field(..., alias="Part_ID", min_length=1) + label: str = Field(..., alias="Label", min_length=1) + description: str = Field(..., alias="Description", min_length=1) + sort_order: Optional[int] = Field(None, alias="Sort_order", ge=1) + + +# ============================================================================ +# Table Part +# ============================================================================ + + +class TablePart(PartBase): + """Represents a database table definition.""" + + part_type: Literal["table"] = Field(alias="Part_type") + + @field_validator("part_id") + @classmethod + def validate_table_name(cls, v: str) -> str: + """Table names should be lowercase with underscores.""" + if " " in v: + raise ValueError(f"Table name '{v}' should not contain spaces") + return v + + +# ============================================================================ +# Field Parts (key, property, compositeKey*) +# ============================================================================ + + +class FieldPartBase(PartBase): + """Base for parts that represent table columns.""" + + sql_data_type: Optional[str] = Field(None, alias="SQL_data_type") + is_required: bool = Field(default=False, alias="Is_required") + default_value: Optional[str] = Field(None, alias="Default_value") + value_set_part_id: Optional[str] = Field(None, alias="Value_set_part_ID") + table_presence: Dict[str, TablePresence] = Field( + default_factory=dict, description="Maps table_name -> TablePresence metadata" + ) + + @model_validator(mode="after") + def validate_table_presence_not_empty(self): + """Field parts must appear in at least one table.""" + if not self.table_presence: + raise ValueError( + f"Field '{self.part_id}' must appear in at least one table" + ) + return self + + +class KeyPart(FieldPartBase): + """Primary key field.""" + + part_type: Literal["key"] = Field(alias="Part_type") + + @field_validator("part_id") + @classmethod + def validate_key_naming(cls, v: str) -> str: + """Primary keys should end with '_ID'.""" + if not v.endswith("_ID"): + raise ValueError(f"Key '{v}' should end with '_ID'") + return v + + @model_validator(mode="after") + def validate_key_in_tables(self): + """A key must be 'key' in at least one table.""" + has_key_role = any( + presence.role == "key" for presence in self.table_presence.values() + ) + if not has_key_role: + raise ValueError( + f"Key '{self.part_id}' must have role='key' in at least one table" + ) + return self + + +class PropertyPart(FieldPartBase): + """Regular column/field.""" + + part_type: Literal["property"] = Field(alias="Part_type") + + +class CompositeKeyFirstPart(FieldPartBase): + """First component of composite primary key.""" + + part_type: Literal["compositeKeyFirst"] = Field(alias="Part_type") + + @field_validator("part_id") + @classmethod + def validate_composite_key_naming(cls, v: str) -> str: + """Composite keys should end with '_ID'.""" + if not v.endswith("_ID"): + raise ValueError(f"Composite key '{v}' should end with '_ID'") + return v + + +class CompositeKeySecondPart(FieldPartBase): + """Second component of composite primary key.""" + + part_type: Literal["compositeKeySecond"] = Field(alias="Part_type") + + @field_validator("part_id") + @classmethod + def validate_composite_key_naming(cls, v: str) -> str: + """Composite keys should end with '_ID'.""" + if not v.endswith("_ID"): + raise ValueError(f"Composite key '{v}' should end with '_ID'") + return v + + +class ParentKeyPart(FieldPartBase): + """Hierarchical self-reference within same table.""" + + part_type: Literal["parentKey"] = Field(alias="Part_type") + ancestor_part_id: str = Field(..., alias="Ancestor_part_ID", min_length=1) + + @field_validator("ancestor_part_id") + @classmethod + def validate_ancestor_is_key(cls, v: str) -> str: + """Ancestor should be a key field (end with _ID).""" + if not v.endswith("_ID"): + raise ValueError(f"Ancestor '{v}' should be a key field ending with '_ID'") + return v + + +# ============================================================================ +# Value Set Parts +# ============================================================================ + + +class ValueSetPart(PartBase): + """Enumeration/controlled vocabulary definition.""" + + part_type: Literal["valueSet"] = Field(alias="Part_type") + + @field_validator("part_id") + @classmethod + def validate_value_set_naming(cls, v: str) -> str: + """Value sets should end with '_set' by convention.""" + if not v.endswith("_set") and not v.endswith("Set"): + raise ValueError(f"Value set '{v}' should end with '_set' or 'Set'") + return v + + +class ValueSetMemberPart(PartBase): + """Individual value within a value set.""" + + part_type: Literal["valueSetMember"] = Field(alias="Part_type") + member_of_set_part_id: str = Field(..., alias="Member_of_set_part_ID", min_length=1) + + @field_validator("member_of_set_part_id") + @classmethod + def validate_member_of_set(cls, v: str) -> str: + """Should reference a value set.""" + if not v.endswith("_set") and not v.endswith("Set"): + raise ValueError( + f"Member should belong to a value set ending with '_set' or 'Set', got '{v}'" + ) + return v + + +# ============================================================================ +# Discriminated Union +# ============================================================================ + +Part = Annotated[ + Union[ + TablePart, + KeyPart, + PropertyPart, + CompositeKeyFirstPart, + CompositeKeySecondPart, + ParentKeyPart, + ValueSetPart, + ValueSetMemberPart, + ], + Field(discriminator="part_type"), +] + + +# ============================================================================ +# Dictionary Root +# ============================================================================ + + +class Dictionary(BaseModel): + """Root dictionary model.""" + + parts: List[Part] + + @field_validator("parts") + @classmethod + def validate_unique_part_ids(cls, v: List[Part]) -> List[Part]: + """Ensure all Part_IDs are unique.""" + part_ids = [part.part_id for part in v] + duplicates = [pid for pid in set(part_ids) if part_ids.count(pid) > 1] + if duplicates: + raise ValueError(f"Duplicate Part_IDs found: {duplicates}") + return v + + @model_validator(mode="after") + def validate_cross_references(self): + """Validate that all cross-references point to existing parts.""" + part_ids = {part.part_id for part in self.parts} + + # Validate value_set_part_id references + for part in self.parts: + if isinstance(part, FieldPartBase) and part.value_set_part_id: + if part.value_set_part_id not in part_ids: + raise ValueError( + f"Field '{part.part_id}' references non-existent " + f"value set '{part.value_set_part_id}'" + ) + + # Validate member_of_set_part_id references + for part in self.parts: + if isinstance(part, ValueSetMemberPart): + if part.member_of_set_part_id not in part_ids: + raise ValueError( + f"Value set member '{part.part_id}' references " + f"non-existent set '{part.member_of_set_part_id}'" + ) + + # Validate ancestor_part_id references + for part in self.parts: + if isinstance(part, ParentKeyPart): + if part.ancestor_part_id not in part_ids: + raise ValueError( + f"Parent key '{part.part_id}' references non-existent " + f"ancestor '{part.ancestor_part_id}'" + ) + + # Validate table_presence references + table_names = { + part.part_id for part in self.parts if isinstance(part, TablePart) + } + for part in self.parts: + if isinstance(part, FieldPartBase): + for table_name in part.table_presence.keys(): + if table_name not in table_names: + raise ValueError( + f"Field '{part.part_id}' references non-existent " + f"table '{table_name}' in table_presence" + ) + + # Validate foreign key relationships by inferring targets from field names + for part in self.parts: + if isinstance(part, FieldPartBase): + for table_name, presence in part.table_presence.items(): + if presence.relationship_type: + # Infer FK target from field name (field name ending in _ID references same-named primary key) + if part.part_id.endswith("_ID"): + # Validate that the inferred target exists and is a key field + target_part = next( + (p for p in self.parts if p.part_id == part.part_id), + None, + ) + if target_part and not isinstance( + target_part, + ( + KeyPart, + CompositeKeyFirstPart, + CompositeKeySecondPart, + ), + ): + raise ValueError( + f"Field '{part.part_id}' appears to be a foreign key but is not defined as a key field" + ) + + return self diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..64ffd8f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,93 @@ +"""Shared pytest configuration and fixtures for open_dateaubase tests.""" + +import pytest +import json +import sys +from pathlib import Path + +# Add project paths to sys.path so imports work consistently +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / "src")) +sys.path.insert(0, str(project_root / "scripts")) +sys.path.insert(0, str(project_root / "tests")) + + +# Import fixtures +from fixtures.sample_dictionary import ( + sample_dictionary_data, + complex_dictionary_data, + edge_case_dictionary_data, +) + + +@pytest.fixture +def sample_json_dict(): + """Return sample dictionary data as Python dict.""" + return sample_dictionary_data() + + +@pytest.fixture +def complex_json_dict(): + """Return complex dictionary data as Python dict.""" + return complex_dictionary_data() + + +@pytest.fixture +def edge_case_json_dict(): + """Return edge case dictionary data as Python dict.""" + return edge_case_dictionary_data() + + +@pytest.fixture +def sample_json_file(tmp_path): + """Create temporary JSON file with sample dictionary data.""" + json_file = tmp_path / "sample_dictionary.json" + json_file.write_text(json.dumps(sample_dictionary_data(), indent=2)) + return json_file + + +@pytest.fixture +def complex_json_file(tmp_path): + """Create temporary JSON file with complex dictionary data.""" + json_file = tmp_path / "complex_dictionary.json" + json_file.write_text(json.dumps(complex_dictionary_data(), indent=2)) + return json_file + + +@pytest.fixture +def edge_case_json_file(tmp_path): + """Create temporary JSON file with edge case dictionary data.""" + json_file = tmp_path / "edge_case_dictionary.json" + json_file.write_text(json.dumps(edge_case_dictionary_data(), indent=2)) + return json_file + + +@pytest.fixture +def output_dirs(tmp_path): + """Create standard output directory structure for tests.""" + dirs = { + "docs": tmp_path / "docs" / "reference", + "sql": tmp_path / "sql_generation_scripts", + "assets": tmp_path / "docs" / "assets", + "root": tmp_path, + } + + for dir_path in dirs.values(): + if isinstance(dir_path, Path): + dir_path.mkdir(parents=True, exist_ok=True) + + return dirs + + +# Configure pytest +def pytest_configure(config): + """Configure pytest with custom markers.""" + config.addinivalue_line( + "markers", "integration: mark test as integration test (slower, uses multiple modules)" + ) + config.addinivalue_line( + "markers", "unit: mark test as unit test (fast, isolated)" + ) + config.addinivalue_line( + "markers", "slow: mark test as slow running" + ) diff --git a/tests/fixtures.py b/tests/fixtures.py deleted file mode 100644 index 8cb4cd0..0000000 --- a/tests/fixtures.py +++ /dev/null @@ -1,21 +0,0 @@ -import pytest - -@pytest.fixture -def sample_csv_data(): - """Create sample CSV data for testing - NEW FORMAT with parentKey support.""" - # This fixture demonstrates the new dictionary structure: - # - TestTable_ID is the primary key - # - Name, Status are regular non-prefixed fields - # - Parent_ID is a parentKey that references TestTable_ID - # - StatusSet is a value set with two members - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,Ancestor_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TestTable_present,TestTable_required,TestTable_order -TestTable,Test Table,A test table,table,,,,,,,,,, -TestTable_ID,Test Table ID,Identifier for TestTable,key,,,,int,True,,1,key,True,1 -Name,Name,Name field,property,,,,nvarchar(255),True,,2,property,True,2 -Status,Status,Status field,property,StatusSet,,,nvarchar(50),False,,3,property,False,3 -Parent_ID,Parent ID,Hierarchical reference to parent TestTable,parentKey,,,TestTable_ID,int,False,,4,property,False,4 -StatusSet,Status Set,Valid status values,valueSet,,,,,,,, -active,Active,Active status,valueSetMember,,StatusSet,,nvarchar(50),,,1,, -inactive,Inactive,Inactive status,valueSetMember,,StatusSet,,nvarchar(50),,,2,, -""" - return csv_content diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..d1b3395 --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1 @@ +"""Test fixtures package.""" diff --git a/tests/fixtures/sample_dictionary.py b/tests/fixtures/sample_dictionary.py new file mode 100644 index 0000000..07ec7a7 --- /dev/null +++ b/tests/fixtures/sample_dictionary.py @@ -0,0 +1,300 @@ +"""JSON-based test fixtures for open_dateaubase testing. + +This module provides sample dictionary data in the current JSON format +used by Pydantic models, replacing the old CSV-based fixtures. +""" + +from typing import Dict, Any + + +def sample_dictionary_data() -> Dict[str, Any]: + """Sample dictionary with basic table, fields, and value set.""" + return { + "parts": [ + # Table definition + { + "Part_ID": "test_table", + "Label": "Test Table", + "Description": "A test table for demonstration", + "Part_type": "table", + }, + # Primary key field + { + "Part_ID": "TestTable_ID", + "Label": "Test Table ID", + "Description": "Primary identifier for TestTable", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "test_table": {"role": "key", "required": True, "order": 1} + }, + }, + # Regular field with value set constraint + { + "Part_ID": "Status", + "Label": "Status", + "Description": "Current status of record", + "Part_type": "property", + "SQL_data_type": "nvarchar(50)", + "Is_required": False, + "Value_set_part_ID": "StatusSet", + "table_presence": { + "test_table": {"role": "property", "required": False, "order": 2} + }, + }, + # Regular field without constraints + { + "Part_ID": "Description", + "Label": "Description", + "Description": "Detailed description", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": True, + "table_presence": { + "test_table": {"role": "property", "required": True, "order": 3} + }, + }, + # Parent key (hierarchical self-reference) + { + "Part_ID": "Parent_ID", + "Label": "Parent ID", + "Description": "Hierarchical reference to parent record", + "Part_type": "parentKey", + "Ancestor_part_ID": "TestTable_ID", + "SQL_data_type": "int", + "Is_required": False, + "table_presence": { + "test_table": {"role": "property", "required": False, "order": 4} + }, + }, + # Value set definition + { + "Part_ID": "StatusSet", + "Label": "Status Set", + "Description": "Valid status values for records", + "Part_type": "valueSet", + }, + # Value set members + { + "Part_ID": "active", + "Label": "Active", + "Description": "Record is currently active", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "StatusSet", + "Sort_order": 1, + }, + { + "Part_ID": "inactive", + "Label": "Inactive", + "Description": "Record is currently inactive", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "StatusSet", + "Sort_order": 2, + }, + { + "Part_ID": "pending", + "Label": "Pending", + "Description": "Record is pending review", + "Part_type": "valueSetMember", + "Member_of_set_part_ID": "StatusSet", + "Sort_order": 3, + }, + ] + } + + +def complex_dictionary_data() -> Dict[str, Any]: + """Complex dictionary with multiple tables, relationships, and edge cases.""" + return { + "parts": [ + # First table + { + "Part_ID": "contact", + "Label": "Contact", + "Description": "Contact information table", + "Part_type": "table", + }, + { + "Part_ID": "Contact_ID", + "Label": "Contact ID", + "Description": "Primary key for contact", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "contact": {"role": "key", "required": True, "order": 1} + }, + }, + { + "Part_ID": "contact_Name", + "Label": "Name", + "Description": "Full name of contact", + "Part_type": "property", + "SQL_data_type": "nvarchar(255)", + "Is_required": True, + "table_presence": { + "contact": {"role": "property", "required": True, "order": 2} + }, + }, + # Second table with FK to first + { + "Part_ID": "project", + "Label": "Project", + "Description": "Project information", + "Part_type": "table", + }, + { + "Part_ID": "Project_ID", + "Label": "Project ID", + "Description": "Primary key for project", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "project": {"role": "key", "required": True, "order": 1} + }, + }, + { + "Part_ID": "Project_Contact_ID", + "Label": "Contact ID", + "Description": "Foreign key to contact", + "Part_type": "property", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "project": {"role": "property", "required": True, "order": 2} + }, + }, + # Junction table for many-to-many + { + "Part_ID": "project_has_contact", + "Label": "Project Has Contact", + "Description": "Junction table for project-contact relationships", + "Part_type": "table", + }, + { + "Part_ID": "Junction_Project_ID", + "Label": "Project ID", + "Description": "Foreign key to project in junction", + "Part_type": "compositeKeyFirst", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "project_has_contact": { + "role": "compositeKeyFirst", + "required": True, + "order": 1, + } + }, + }, + { + "Part_ID": "Junction_Contact_ID", + "Label": "Contact ID", + "Description": "Foreign key to contact in junction", + "Part_type": "compositeKeySecond", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "project_has_contact": { + "role": "compositeKeySecond", + "required": True, + "order": 2, + } + } + }, + ] + } + + +def edge_case_dictionary_data() -> Dict[str, Any]: + """Dictionary with edge cases for testing error handling.""" + return { + "parts": [ + # Table with special characters in name + { + "Part_ID": "special_table", + "Label": "Special-Table!", + "Description": "Table with special characters: @#$%", + "Part_type": "table", + }, + # Field with very long name + { + "Part_ID": "VeryLongFieldNameThatExceedsNormalDatabaseLimitsAndMightCauseIssues", + "Label": "Very Long Field Name That Exceeds Normal Database Limits", + "Description": "A field with an extremely long name for testing edge cases", + "Part_type": "property", + "SQL_data_type": "nvarchar(max)", + "Is_required": False, + "table_presence": { + "special_table": {"role": "property", "required": False, "order": 1} + }, + }, + # Field with default value + { + "Part_ID": "Created_Date", + "Label": "Created Date", + "Description": "Date when record was created", + "Part_type": "property", + "SQL_data_type": "datetime", + "Is_required": True, + "Default_value": "GETDATE()", + "table_presence": { + "special_table": {"role": "property", "required": True, "order": 2} + }, + }, + # Boolean field + { + "Part_ID": "Is_Active", + "Label": "Is Active", + "Description": "Whether record is active", + "Part_type": "property", + "SQL_data_type": "bit", + "Is_required": False, + "Default_value": "True", + "table_presence": { + "special_table": {"role": "property", "required": False, "order": 3} + }, + }, + ] + } + + +def invalid_dictionary_data() -> Dict[str, Any]: + """Invalid dictionary data for testing validation errors.""" + return { + "parts": [ + # Missing required fields + { + "Part_ID": "incomplete_table", + # Missing Label and Description + "Part_type": "table", + }, + # Invalid part type + { + "Part_ID": "invalid_part", + "Label": "Invalid Part", + "Description": "This has an invalid part type", + "Part_type": "invalid_type", + }, + # Key without _ID suffix + { + "Part_ID": "InvalidKey", + "Label": "Invalid Key", + "Description": "This key doesn't end with _ID", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": { + "incomplete_table": {"role": "key", "required": True, "order": 1} + }, + }, + # Value set without proper suffix + { + "Part_ID": "InvalidValueSet", + "Label": "Invalid Value Set", + "Description": "This value set doesn't end with _set or Set", + "Part_type": "valueSet", + }, + ] + } diff --git a/tests/integration/test_erd_integrity.py b/tests/integration/test_erd_integrity.py new file mode 100644 index 0000000..7d29c54 --- /dev/null +++ b/tests/integration/test_erd_integrity.py @@ -0,0 +1,233 @@ +""" +Tests for ERD (Entity-Relationship Diagram) integrity. + +These tests verify that: +1. All FK relationships are captured in the ERD +2. All relationships point to valid tables +3. The generated HTML contains all expected elements +""" + +import sys +from pathlib import Path +import pytest +import json + +# Add project paths +project_root = Path(__file__).parent.parent.parent +sys.path.insert(0, str(project_root / 'src')) +sys.path.insert(0, str(project_root / 'scripts')) + +from generate_erd import generate_erd_data, generate_erd_html, parse_erd_json + + +@pytest.fixture +def parts_data(): + """Load and parse the dictionary.json file.""" + json_path = project_root / 'src/dictionary.json' + return parse_erd_json(json_path) + + +@pytest.fixture +def erd_data(parts_data): + """Generate ERD data from parts data.""" + return generate_erd_data(parts_data) + + +def test_all_fk_relationships_captured(parts_data, erd_data): + """Test that all FK fields result in relationships in the ERD.""" + # Count total FK fields in dictionary + total_fks = 0 + fk_details = [] + + for table_id, table_info in parts_data['tables'].items(): + for field in table_info['fields']: + if field.get('fk_to'): + total_fks += 1 + fk_details.append({ + 'table': table_id, + 'field': field['label'], + 'fk_to': field['fk_to'] + }) + + # Count relationships in ERD + num_relationships = len(erd_data['relationships']) + + # Assert they match + assert num_relationships == total_fks, ( + f"Expected {total_fks} relationships but got {num_relationships}. " + f"FK fields: {fk_details}" + ) + + +def test_all_relationships_have_valid_tables(parts_data, erd_data): + """Test that all relationships point to existing tables.""" + table_ids = set(parts_data['tables'].keys()) + + for rel in erd_data['relationships']: + assert rel['from_table'] in table_ids, ( + f"Relationship from_table '{rel['from_table']}' does not exist" + ) + assert rel['to_table'] in table_ids, ( + f"Relationship to_table '{rel['to_table']}' does not exist" + ) + + +def test_all_tables_included_in_erd(parts_data, erd_data): + """Test that all tables from dictionary are included in ERD.""" + dict_tables = set(parts_data['tables'].keys()) + erd_tables = {table['id'] for table in erd_data['tables']} + + assert dict_tables == erd_tables, ( + f"Table mismatch. Missing from ERD: {dict_tables - erd_tables}, " + f"Extra in ERD: {erd_tables - dict_tables}" + ) + + +def test_relationship_field_names_exist(parts_data, erd_data): + """Test that relationship field names exist in their respective tables.""" + for rel in erd_data['relationships']: + # Check from_field exists in from_table + from_table = parts_data['tables'][rel['from_table']] + from_field_names = [f['label'] for f in from_table['fields']] + assert rel['from_field'] in from_field_names, ( + f"Field '{rel['from_field']}' not found in table '{rel['from_table']}'" + ) + + # Check to_field exists in to_table + to_table = parts_data['tables'][rel['to_table']] + to_field_names = [f['label'] for f in to_table['fields']] + assert rel['to_field'] in to_field_names, ( + f"Field '{rel['to_field']}' not found in table '{rel['to_table']}'" + ) + + +def test_junction_tables_not_preferred_as_targets(erd_data): + """ + Test that relationships prefer non-junction tables as targets when possible. + + Junction tables (with '_has_' in name) should generally not be the target + of FK relationships unless they are the only table with that PK. + """ + # Count how many relationships point to junction tables + junction_target_count = sum( + 1 for rel in erd_data['relationships'] + if '_has_' in rel['to_table'] + ) + + # We expect very few or no relationships to point to junction tables + # This is informational - junction tables are typically intermediate tables + total_relationships = len(erd_data['relationships']) + + # Allow up to 10% of relationships to point to junction tables + # (in case there are legitimate cases) + assert junction_target_count / total_relationships < 0.1, ( + f"Too many relationships ({junction_target_count}/{total_relationships}) " + f"point to junction tables. This may indicate incorrect FK resolution." + ) + + +def test_html_contains_clickable_arrows(erd_data, tmp_path): + """Test that generated HTML contains link click handlers for clickable arrows.""" + output_path = tmp_path / 'test_erd.html' + generate_erd_html(erd_data, output_path, library='jointjs') + + html_content = output_path.read_text() + + # Check for link click handler + assert 'link:pointerdown' in html_content, ( + "Generated HTML missing link click handler (link:pointerdown)" + ) + + # Check for relationship data storage + assert 'relationshipData' in html_content, ( + "Generated HTML missing relationshipData attribute for storing FK info" + ) + + +def test_html_contains_drag_functionality(erd_data, tmp_path): + """Test that generated HTML contains table dragging functionality.""" + output_path = tmp_path / 'test_erd.html' + generate_erd_html(erd_data, output_path, library='jointjs') + + html_content = output_path.read_text() + + # Check for drag handler + assert 'startDrag' in html_content, ( + "Generated HTML missing startDrag function for table dragging" + ) + + # Check that drag is attached to header + assert 'onmousedown' in html_content, ( + "Generated HTML missing onmousedown event for initiating drag" + ) + + +def test_html_has_all_tables(erd_data, tmp_path): + """Test that generated HTML will render all tables.""" + output_path = tmp_path / 'test_erd.html' + generate_erd_html(erd_data, output_path, library='jointjs') + + html_content = output_path.read_text() + + # The ERD data should be embedded as JSON in the HTML + # Check that it contains table data + assert 'erdData.tables' in html_content, ( + "Generated HTML missing erdData.tables reference" + ) + + +def test_relationship_count_matches_fk_count(parts_data, erd_data): + """ + Test that the number of relationships equals the number of FK fields. + This is the main integrity check - every FK should have exactly one relationship. + """ + # Count FK fields + fk_count = sum( + 1 for table in parts_data['tables'].values() + for field in table['fields'] + if field.get('fk_to') + ) + + # Count relationships + rel_count = len(erd_data['relationships']) + + assert rel_count == fk_count, ( + f"Relationship count ({rel_count}) does not match FK count ({fk_count}). " + f"Every FK field should generate exactly one relationship." + ) + + +def test_no_duplicate_relationships(erd_data): + """Test that there are no duplicate relationships in the ERD.""" + # Create a set of relationship signatures (from_table, to_table, from_field) + relationship_signatures = [ + (rel['from_table'], rel['to_table'], rel['from_field']) + for rel in erd_data['relationships'] + ] + + # Check for duplicates + unique_signatures = set(relationship_signatures) + + assert len(relationship_signatures) == len(unique_signatures), ( + f"Found duplicate relationships. Total: {len(relationship_signatures)}, " + f"Unique: {len(unique_signatures)}" + ) + + +def test_relationships_have_required_fields(erd_data): + """Test that all relationships have the required fields.""" + required_fields = ['from_table', 'to_table', 'from_field', 'to_field', 'relationship_type'] + + for i, rel in enumerate(erd_data['relationships']): + for field in required_fields: + assert field in rel, ( + f"Relationship {i} missing required field '{field}': {rel}" + ) + assert rel[field], ( + f"Relationship {i} has empty value for required field '{field}': {rel}" + ) + + +if __name__ == '__main__': + # Run tests with pytest + pytest.main([__file__, '-v']) diff --git a/tests/integration/test_orchestration.py b/tests/integration/test_orchestration.py new file mode 100644 index 0000000..6eff2c2 --- /dev/null +++ b/tests/integration/test_orchestration.py @@ -0,0 +1,174 @@ +"""Integration tests for documentation orchestration workflow.""" + +import pytest +import json +from pathlib import Path +import sys + +# Add scripts directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "tests")) +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "scripts")) + +from orchestrate_docs import main as orchestrate_main +from generate_dictionary_reference import parse_parts_json, generate_tables_markdown, generate_value_sets_markdown +from generate_erd import parse_erd_json, generate_erd_files +from generate_sql import generate_sql_schemas +from fixtures.sample_dictionary import sample_dictionary_data + + +@pytest.fixture +def sample_json_file(tmp_path): + """Create a temporary JSON file with sample dictionary data.""" + json_file = tmp_path / "dictionary.json" + json_file.write_text(json.dumps(sample_dictionary_data(), indent=2)) + return json_file + + +@pytest.fixture +def output_dirs(tmp_path): + """Create temporary output directories.""" + docs_dir = tmp_path / "docs" / "reference" + sql_dir = tmp_path / "sql_generation_scripts" + assets_dir = tmp_path / "docs" / "assets" + + docs_dir.mkdir(parents=True, exist_ok=True) + sql_dir.mkdir(parents=True, exist_ok=True) + assets_dir.mkdir(parents=True, exist_ok=True) + + return { + "docs": docs_dir, + "sql": sql_dir, + "assets": assets_dir, + "root": tmp_path, + } + + +class TestOrchestrationWorkflow: + """Test the complete documentation generation workflow.""" + + def test_generates_all_documentation_components(self, sample_json_file, output_dirs): + """Test that all documentation components are generated.""" + parts_data = parse_parts_json(sample_json_file) + + # Generate all components + tables = generate_tables_markdown(parts_data) + value_sets = generate_value_sets_markdown(parts_data) + + # Write documentation + (output_dirs["docs"] / "tables.md").write_text(tables, encoding="utf-8") + (output_dirs["docs"] / "valuesets.md").write_text(value_sets, encoding="utf-8") + + # Generate ERD + erd_parts_data = parse_erd_json(sample_json_file) + generate_erd_files(erd_parts_data, output_dirs["assets"], output_dirs["docs"]) + + # Generate SQL + generate_sql_schemas(parts_data, output_dirs["sql"], ["mssql"]) + + # Verify all files were created + assert (output_dirs["docs"] / "tables.md").exists() + assert (output_dirs["docs"] / "valuesets.md").exists() + assert (output_dirs["docs"] / "erd.md").exists() + assert (output_dirs["assets"] / "erd_interactive.html").exists() + assert len(list(output_dirs["sql"].glob("*.sql"))) == 1 + + def test_tables_markdown_contains_expected_content(self, sample_json_file, output_dirs): + """Test that generated tables.md has correct content.""" + parts_data = parse_parts_json(sample_json_file) + tables = generate_tables_markdown(parts_data) + (output_dirs["docs"] / "tables.md").write_text(tables, encoding="utf-8") + + content = (output_dirs["docs"] / "tables.md").read_text(encoding="utf-8") + + assert "# Database Tables" in content + assert "### Test Table" in content + assert '' in content + assert '' in content + + def test_valuesets_markdown_contains_expected_content(self, sample_json_file, output_dirs): + """Test that generated valuesets.md has correct content.""" + parts_data = parse_parts_json(sample_json_file) + value_sets = generate_value_sets_markdown(parts_data) + (output_dirs["docs"] / "valuesets.md").write_text(value_sets, encoding="utf-8") + + content = (output_dirs["docs"] / "valuesets.md").read_text(encoding="utf-8") + + assert "# Value Sets" in content + assert "## Status Set" in content + assert '' in content + + def test_erd_generation_creates_html(self, sample_json_file, output_dirs): + """Test that ERD generation creates the interactive HTML.""" + erd_parts_data = parse_erd_json(sample_json_file) + generate_erd_files(erd_parts_data, output_dirs["assets"], output_dirs["docs"]) + + # Check interactive HTML exists + interactive_html = output_dirs["assets"] / "erd_interactive.html" + assert interactive_html.exists() + + # Check content + content = interactive_html.read_text() + assert "JointJS" in content or "jointjs" in content + assert "Test Table" in content + + def test_sql_generation_creates_schema_file(self, sample_json_file, output_dirs): + """Test that SQL generation creates schema files.""" + parts_data = parse_parts_json(sample_json_file) + generate_sql_schemas(parts_data, output_dirs["sql"], ["mssql"]) + + sql_files = list(output_dirs["sql"].glob("*.sql")) + assert len(sql_files) == 1 + + # Check filename format + sql_file = sql_files[0] + assert "_as-designed_mssql.sql" in sql_file.name + + # Check content + content = sql_file.read_text(encoding="utf-8") + assert "CREATE TABLE [test_table]" in content + assert "PRIMARY KEY" in content + + def test_multiple_database_targets(self, sample_json_file, output_dirs): + """Test that multiple database schemas can be generated.""" + parts_data = parse_parts_json(sample_json_file) + + # Currently only mssql is supported, but test the structure + generate_sql_schemas(parts_data, output_dirs["sql"], ["mssql"]) + + sql_files = list(output_dirs["sql"].glob("*_mssql.sql")) + assert len(sql_files) == 1 + + def test_workflow_handles_empty_value_sets(self, tmp_path, output_dirs): + """Test that workflow handles dictionaries with no value sets.""" + json_data = { + "parts": [ + { + "Part_ID": "test_table", + "Label": "Test Table", + "Description": "A test table", + "Part_type": "table", + } + ] + } + json_file = tmp_path / "no_valuesets.json" + json_file.write_text(json.dumps(json_data)) + + parts_data = parse_parts_json(json_file) + value_sets = generate_value_sets_markdown(parts_data) + (output_dirs["docs"] / "valuesets.md").write_text(value_sets, encoding="utf-8") + + content = (output_dirs["docs"] / "valuesets.md").read_text(encoding="utf-8") + assert "No value sets currently appear in dictionary" in content + + def test_cross_references_between_components(self, sample_json_file, output_dirs): + """Test that cross-references between components work correctly.""" + parts_data = parse_parts_json(sample_json_file) + + tables = generate_tables_markdown(parts_data) + value_sets = generate_value_sets_markdown(parts_data) + + # Tables should link to value sets + assert "[StatusSet](valuesets.md#StatusSet)" in tables + + # Value sets should have anchors that tables link to + assert '' in value_sets diff --git a/tests/test_docs_gen.py b/tests/test_docs_gen.py deleted file mode 100644 index 4ec9ee9..0000000 --- a/tests/test_docs_gen.py +++ /dev/null @@ -1,449 +0,0 @@ -import pytest -import csv -from pathlib import Path -from io import StringIO -import sys -import os - -# Add hooks directory to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'docs/hooks')) - -from generate_docs import ( - parse_parts_table, - generate_tables_markdown, - generate_value_sets_markdown, - on_pre_build, - generate_sql_schemas -) -from fixtures import sample_csv_data - -@pytest.fixture -def sample_csv_file(tmp_path, sample_csv_data): - """Create a temporary CSV file.""" - csv_file = tmp_path / "test_parts.csv" - csv_file.write_text(sample_csv_data) - return csv_file - - -class TestParsePartsTable: - """Tests for parse_parts_table function.""" - - def test_parse_identifies_tables(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - assert 'TestTable' in data['tables'] - assert data['tables']['TestTable']['label'] == 'Test Table' - assert data['tables']['TestTable']['description'] == 'A test table' - - def test_parse_identifies_fields(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - fields = data['tables']['TestTable']['fields'] - assert len(fields) == 4 - - # Check primary key - pk_field = next(f for f in fields if f['part_id'] == 'TestTable_ID') - assert pk_field['part_type'] == 'key' - assert pk_field['is_required'] == True - assert pk_field['sql_data_type'] == 'int' - - def test_parse_identifies_foreign_keys(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - # Parent_ID is now a parentKey type with Ancestor_part_ID set - fk_field = next(f for f in data['tables']['TestTable']['fields'] - if f['part_id'] == 'Parent_ID') - assert fk_field['fk_to'] == 'TestTable_ID' - - def test_parse_identifies_value_sets(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - assert 'StatusSet' in data['value_sets'] - assert data['value_sets']['StatusSet']['label'] == 'Status Set' - assert len(data['value_sets']['StatusSet']['members']) == 2 - - def test_parse_sorts_fields_by_sort_order(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - fields = data['tables']['TestTable']['fields'] - sort_orders = [f['sort_order'] for f in fields] - assert sort_orders == sorted(sort_orders) - - def test_parse_sorts_value_set_members(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - members = data['value_sets']['StatusSet']['members'] - sort_orders = [m['sort_order'] for m in members] - assert sort_orders == sorted(sort_orders) - - def test_parse_links_value_sets_to_fields(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - status_field = next(f for f in data['tables']['TestTable']['fields'] - if f['part_id'] == 'Status') - assert status_field['value_set'] == 'StatusSet' - - -class TestGenerateTablesMarkdown: - """Tests for generate_tables_markdown function.""" - - def test_generates_valid_markdown(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - assert '# Database Tables' in markdown - assert '## Tables' in markdown - - def test_includes_table_headings(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - assert '### Test Table' in markdown - assert 'A test table' in markdown - - def test_includes_table_anchors(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Check for invisible anchor span - assert '' in markdown - - def test_includes_field_anchors(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Check for field anchors in description column - assert '' in markdown - assert '' in markdown - - def test_generates_fields_table(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Check table header - assert '| Field | SQL Type | Value Set | Required | Description | Constraints |' in markdown - assert '|-------|----------|-----------|----------|-------------|-------------|' in markdown - - def test_marks_primary_keys(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Primary key should have PK marker - assert 'int **(PK)**' in markdown - - def test_marks_required_fields(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Should have checkmarks for required fields - assert '✓' in markdown - - def test_links_to_value_sets(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Should link to value set - assert '[StatusSet](valuesets.md#StatusSet)' in markdown - - def test_links_foreign_keys(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Should link FK to target field - assert 'FK → [TestTable_ID](#TestTable_ID)' in markdown - - def test_shows_dash_when_no_value_set(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # Fields without value sets should show '-' - lines = markdown.split('\n') - name_field_line = [l for l in lines if 'Name field' in l][0] - assert '| -' in name_field_line or '- |' in name_field_line - - -class TestGenerateValueSetsMarkdown: - """Tests for generate_value_sets_markdown function.""" - - def test_generates_valid_markdown(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_value_sets_markdown(data) - - assert '# Value Sets' in markdown - - def test_includes_value_set_headings(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_value_sets_markdown(data) - - assert '## Status Set' in markdown - assert 'Valid status values' in markdown - - def test_includes_value_set_anchors(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_value_sets_markdown(data) - - # Check for invisible anchor span - assert '' in markdown - - def test_includes_member_anchors(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_value_sets_markdown(data) - - # Check for member anchors - assert '' in markdown - assert '' in markdown - - def test_generates_members_table(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_value_sets_markdown(data) - - # Check table header - assert '| Value | Description |' in markdown - assert '|-------|-------------|' in markdown - - def test_lists_all_members(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_value_sets_markdown(data) - - assert '`active`' in markdown - assert 'Active status' in markdown - assert '`inactive`' in markdown - assert 'Inactive status' in markdown - - def test_handles_empty_value_sets(self, tmp_path): - # CSV with no value sets - csv_content = """Part_ID,Label,Description,Part_type,Table_part_ID,Value_set_part_ID,Member_of_set_part_ID,FK_to_part_ID,SQL_data_type,Is_required,Default_value,Sort_order -TestTable,Test Table,A test table,table,,,,,,,, -""" - csv_file = tmp_path / "empty_valuesets.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - markdown = generate_value_sets_markdown(data) - - assert 'No value sets currently appear in the dictionary' in markdown - - -class TestIntegration: - """Integration tests checking cross-referencing between tables and value sets.""" - - def test_value_set_links_are_bidirectional(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - tables_md = generate_tables_markdown(data) - valuesets_md = generate_value_sets_markdown(data) - - # Table should link to value set - assert '[StatusSet](valuesets.md#StatusSet)' in tables_md - - # Value set should have anchor that table links to - assert '' in valuesets_md - - def test_foreign_key_links_point_to_existing_anchors(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - markdown = generate_tables_markdown(data) - - # FK link - assert 'FK → [TestTable_ID](#TestTable_ID)' in markdown - - # Target anchor exists - assert '' in markdown - - -class TestGenerateSQLSchemas: - """Tests for generate_sql_schemas function.""" - - def test_generates_sql_file_with_timestamp_and_version(self, sample_csv_file, tmp_path): - """Test that SQL files are generated with timestamp and version in filename.""" - data = parse_parts_table(sample_csv_file) - sql_path = tmp_path / "sql_output" - sql_path.mkdir() - - generate_sql_schemas(data, sql_path, ['mssql']) - - # Check that a file was created - sql_files = list(sql_path.glob('*.sql')) - assert len(sql_files) == 1 - - # Check filename format: YYYY-MM-DDTHH:MM:SS.ffffff_version_mssql.sql - sql_file = sql_files[0] - filename = sql_file.name - - # Should contain version - assert '0.1.0' in filename # Current package version - - # Should contain database type - assert 'mssql.sql' in filename - - # Should contain ISO timestamp format (just check for basic structure) - assert filename.count('_') >= 2 # timestamp_version_mssql.sql - - def test_generates_multiple_database_schemas(self, sample_csv_file, tmp_path): - """Test that multiple database schemas can be generated.""" - data = parse_parts_table(sample_csv_file) - sql_path = tmp_path / "sql_output" - sql_path.mkdir() - - # Currently only mssql is supported, but test the loop structure - generate_sql_schemas(data, sql_path, ['mssql']) - - sql_files = list(sql_path.glob('*_mssql.sql')) - assert len(sql_files) == 1 - - def test_generated_sql_contains_valid_schema(self, sample_csv_file, tmp_path): - """Test that generated SQL contains expected schema elements.""" - data = parse_parts_table(sample_csv_file) - sql_path = tmp_path / "sql_output" - sql_path.mkdir() - - generate_sql_schemas(data, sql_path, ['mssql']) - - sql_file = list(sql_path.glob('*.sql'))[0] - sql_content = sql_file.read_text(encoding='utf-8') - - # Should contain basic SQL elements - assert 'CREATE TABLE [TestTable]' in sql_content - assert 'PRIMARY KEY' in sql_content - assert 'FOREIGN KEY' in sql_content - - def test_writes_to_correct_path(self, sample_csv_file, tmp_path): - """Test that SQL files are written to the specified path.""" - data = parse_parts_table(sample_csv_file) - sql_path = tmp_path / "custom_sql_dir" - sql_path.mkdir() - - generate_sql_schemas(data, sql_path, ['mssql']) - - # Should write to custom directory - assert any(sql_path.glob('*.sql')) - assert len(list(sql_path.glob('*.sql'))) == 1 - - -class TestOnPreBuild: - """Tests for on_pre_build MkDocs hook.""" - - @pytest.fixture - def mock_config(self, tmp_path, sample_csv_data): - """Create a mock MkDocs config.""" - # Setup directory structure - project_root = tmp_path / "project" - project_root.mkdir() - docs_dir = project_root / "docs" - docs_dir.mkdir() - reference_dir = docs_dir / "reference" - reference_dir.mkdir() - sql_dir = project_root / "sql_generation_scripts" - sql_dir.mkdir() - src_dir = project_root / "src" - src_dir.mkdir() - - # Create dictionary CSV - csv_file = src_dir / "dictionary.csv" - csv_file.write_text(sample_csv_data) - - # Create mock config object - config = { - 'config_file_path': str(project_root / "mkdocs.yml"), - 'docs_dir': str(docs_dir) - } - - return config, project_root, docs_dir, reference_dir, sql_dir - - def test_generates_all_output_files(self, mock_config): - """Test that on_pre_build generates all expected output files.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - on_pre_build(config) - - # Check that markdown files were created - assert (reference_dir / "tables.md").exists() - assert (reference_dir / "valuesets.md").exists() - - # Check that SQL files were created - sql_files = list(sql_dir.glob("*.sql")) - assert len(sql_files) > 0 - assert any('mssql.sql' in f.name for f in sql_files) - - def test_tables_md_contains_expected_content(self, mock_config): - """Test that generated tables.md has correct content.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - on_pre_build(config) - - tables_content = (reference_dir / "tables.md").read_text(encoding='utf-8') - - # Should have standard elements - assert '# Database Tables' in tables_content - assert '### Test Table' in tables_content - assert 'A test table' in tables_content - assert '' in tables_content - - def test_valuesets_md_contains_expected_content(self, mock_config): - """Test that generated valuesets.md has correct content.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - on_pre_build(config) - - valuesets_content = (reference_dir / "valuesets.md").read_text(encoding='utf-8') - - # Should have standard elements - assert '# Value Sets' in valuesets_content - assert '## Status Set' in valuesets_content - assert 'Valid status values' in valuesets_content - - def test_sql_schemas_generated_with_correct_format(self, mock_config): - """Test that SQL schemas have correct filename format.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - on_pre_build(config) - - sql_files = list(sql_dir.glob("*.sql")) - assert len(sql_files) > 0 - - # Check filename format - for sql_file in sql_files: - filename = sql_file.name - # Should have format: timestamp_version_dbtype.sql - assert '_mssql.sql' in filename - assert '0.1.0' in filename # Version should be included - - def test_handles_missing_reference_directory(self, mock_config): - """Test that on_pre_build works even if reference directory doesn't exist initially.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - # Remove reference directory - import shutil - shutil.rmtree(reference_dir) - - # Create it fresh - reference_dir.mkdir() - - # Should work without errors - on_pre_build(config) - - assert (reference_dir / "tables.md").exists() - assert (reference_dir / "valuesets.md").exists() - - def test_uses_target_dbs_constant(self, mock_config): - """Test that on_pre_build respects TARGET_DBS constant.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - on_pre_build(config) - - # Should generate MSSQL schema (per TARGET_DBS = ["mssql"]) - sql_files = list(sql_dir.glob("*_mssql.sql")) - assert len(sql_files) == 1 - - def test_reads_csv_from_project_root(self, mock_config): - """Test that on_pre_build correctly locates dictionary.csv in src directory.""" - config, project_root, docs_dir, reference_dir, sql_dir = mock_config - - # CSV was created in src directory by fixture - csv_path = project_root / "src" / "dictionary.csv" - assert csv_path.exists() - - # Should read and process without errors - on_pre_build(config) - - # Verify it actually read the CSV by checking output - tables_content = (reference_dir / "tables.md").read_text(encoding='utf-8') - assert 'TestTable' in tables_content \ No newline at end of file diff --git a/tests/test_sql_gen.py b/tests/test_sql_gen.py deleted file mode 100644 index 274619a..0000000 --- a/tests/test_sql_gen.py +++ /dev/null @@ -1,390 +0,0 @@ -import pytest -import csv -from pathlib import Path -from io import StringIO -import sys -import os - -# Add hooks directory to path -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'docs/hooks')) - -from generate_docs import ( - parse_parts_table, - generate_sql_schema, - generate_field_definition, - generate_foreign_key_constraint, - validate_no_circular_fks, - get_db_config, - extract_field_name -) -from fixtures import sample_csv_data - -@pytest.fixture -def sample_csv_file(tmp_path, sample_csv_data): - """Create a temporary CSV file.""" - csv_file = tmp_path / "test_parts.csv" - csv_file.write_text(sample_csv_data) - return csv_file - - -class TestGenerateSQLSchema: - """Tests for SQL schema generation.""" - - def test_generates_create_table_statement(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - assert 'CREATE TABLE [TestTable]' in sql - - def test_includes_all_fields(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - # Field names (no table prefixes since TestTable is mixed-case) - assert '[TestTable_ID]' in sql # ID field keeps full name - assert '[Name]' in sql # Regular field - assert '[Status]' in sql # Regular field - - def test_generates_primary_key_constraint(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - assert 'CONSTRAINT [PK_TestTable] PRIMARY KEY' in sql - assert '[TestTable_ID]' in sql - - def test_marks_required_fields_not_null(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - # Required field should have NOT NULL - lines = [l for l in sql.split('\n') if '[TestTable_ID]' in l and 'PRIMARY KEY' not in l] - assert any('NOT NULL' in l for l in lines) - - def test_marks_optional_fields_null(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - # Optional field should have NULL - lines = [l for l in sql.split('\n') if '[Status]' in l] - assert any('NULL' in l for l in lines) - - def test_generates_foreign_key_constraints(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - assert 'ALTER TABLE [TestTable]' in sql - assert 'FOREIGN KEY ([Parent_ID])' in sql # Field name is Parent_ID (no prefix) - assert 'REFERENCES [TestTable] ([TestTable_ID])' in sql # References TestTable_ID - - def test_handles_composite_keys(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,JunctionTable_present,JunctionTable_required,JunctionTable_order -JunctionTable,Junction Table,Many-to-many junction,table,,,,,,,,, -Table1_ID,Table1 ID,"Identifier for Table1, used in junction",key,,,int,True,,1,compositeKeyFirst,True,1 -Table2_ID,Table2 ID,"Identifier for Table2, used in junction",key,,,int,True,,2,compositeKeySecond,True,2 -""" - csv_file = tmp_path / "composite_key.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - sql = generate_sql_schema(data) - - # Should have composite primary key - assert 'PRIMARY KEY ([Table1_ID], [Table2_ID])' in sql - - def test_sql_is_executable(self, sample_csv_file): - """Basic syntax check - should not have obvious SQL errors.""" - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - # Check balanced brackets - assert sql.count('[') == sql.count(']') - - # Check balanced parentheses - assert sql.count('(') == sql.count(')') - - # Should end statements with semicolons - assert 'CREATE TABLE' in sql - create_statements = [s for s in sql.split(';') if 'CREATE TABLE' in s] - assert all(');' in s or s.strip().endswith(')') for s in create_statements) - - def test_includes_target_db_in_header(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data, target_db='mssql') - - assert 'Target database: MSSQL' in sql - - -class TestGenerateFieldDefinition: - """Tests for individual field SQL generation.""" - - def test_generates_simple_field(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - db_config = get_db_config('mssql') - field = data['tables']['TestTable']['fields'][1] # Name field (TestTable_Name) - - field_sql = generate_field_definition(field, data, db_config) - - # TestTable_Name -> Name (table prefix removed) - assert '[Name]' in field_sql or '[TestTable_Name]' in field_sql # Accept both - assert 'nvarchar(255)' in field_sql - assert 'NOT NULL' in field_sql - - def test_handles_default_values(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TestTable_present,TestTable_order -TestTable,Test,Test,table,,,,,,,, -Active,Active,Is active,property,,,bit,False,False,1,property,1 -""" - csv_file = tmp_path / "default.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - db_config = get_db_config('mssql') - field = data['tables']['TestTable']['fields'][0] - - field_sql = generate_field_definition(field, data, db_config) - - assert 'DEFAULT 0' in field_sql - - def test_applies_type_mappings(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TestTable_present,TestTable_order -TestTable,Test,Test,table,,,,,,,, -Notes,Notes,Long text,property,,,ntext,False,,1,property,1 -""" - csv_file = tmp_path / "ntext.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - db_config = get_db_config('mssql') - field = data['tables']['TestTable']['fields'][0] - - field_sql = generate_field_definition(field, data, db_config) - - # ntext should be mapped to nvarchar(max) for MSSQL - assert 'nvarchar(max)' in field_sql - assert 'ntext' not in field_sql - - -class TestGenerateForeignKeyConstraint: - """Tests for foreign key constraint generation.""" - - def test_generates_fk_constraint(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - db_config = get_db_config('mssql') - field = next(f for f in data['tables']['TestTable']['fields'] - if f['part_id'] == 'Parent_ID') - - fk_sql = generate_foreign_key_constraint('TestTable', field, db_config) - - assert 'ALTER TABLE [TestTable]' in fk_sql - assert 'ADD CONSTRAINT' in fk_sql - assert 'FOREIGN KEY ([Parent_ID])' in fk_sql - assert 'REFERENCES [TestTable] ([TestTable_ID])' in fk_sql - - def test_returns_none_for_non_fk_field(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - db_config = get_db_config('mssql') - field = data['tables']['TestTable']['fields'][0] # ID field, no FK - - fk_sql = generate_foreign_key_constraint('TestTable', field, db_config) - - assert fk_sql is None - - -class TestCircularDependencyDetection: - """Tests for circular FK validation.""" - - def test_detects_simple_circular_dependency(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TableA_present,TableA_order,TableB_present,TableB_order -TableA,Table A,First table,table,,,,,,,,, -TableB,Table B,Second table,table,,,,,,,,, -TableA_ID,Table A ID,Identifier for TableA,key,,,int,True,,1,key,1,property,2 -TableB_ID,Table B ID,Identifier for TableB,key,,,int,True,,1,property,2,key,1 -""" - csv_file = tmp_path / "circular.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - - with pytest.raises(ValueError, match="Circular foreign key dependencies"): - generate_sql_schema(data) - - def test_allows_self_referential_fks(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TableA_present,TableA_order -TableA,Table A,Hierarchical table,table,,,,,,,, -TableA_ID,Table A ID,Identifier for TableA,key,,,int,True,,1,key,1 -TableA_Parent_ID,Parent ID,FK to parent TableA,property,,,int,False,,2,property,2 -""" - csv_file = tmp_path / "self_ref.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - - # Should NOT raise - self-referential is OK - sql = generate_sql_schema(data) - assert 'CREATE TABLE' in sql - - def test_allows_chain_dependencies(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TableA_present,TableA_order,TableB_present,TableB_order,TableC_present,TableC_order -TableA,Table A,First,table,,,,,,,,,,,, -TableA_ID,Table A ID,PK,key,,,int,True,,1,key,1,,,, -TableB,Table B,Second,table,,,,,,,,,,,, -TableB_ID,Table B ID,PK,key,,,int,True,,1,,,key,1,, -TableA_ID,Table A ID,A ref,key,,,int,False,,2,,,property,2,, -TableC,Table C,Third,table,,,,,,,,,,,, -TableC_ID,Table C ID,PK,key,,,int,True,,1,,,,,key,1 -TableB_ID,Table B ID,B ref,key,,,int,False,,2,,,,,property,2 -""" - csv_file = tmp_path / "chain.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - - # Should NOT raise - A->B->C is fine - sql = generate_sql_schema(data) - assert 'CREATE TABLE' in sql - - def test_validation_function_directly(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TableA_present,TableA_order,TableB_present,TableB_order -TableA,Table A,First table,table,,,,,,,,, -TableB,Table B,Second table,table,,,,,,,,, -TableA_ID,Table A ID,Identifier for TableA,key,,,int,True,,1,key,1,property,2 -TableB_ID,Table B ID,Identifier for TableB,key,,,int,True,,1,property,2,key,1 -""" - csv_file = tmp_path / "circular.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - - with pytest.raises(ValueError) as exc_info: - validate_no_circular_fks(data) - - error_msg = str(exc_info.value) - assert 'TableA' in error_msg - assert 'TableB' in error_msg - assert '↔' in error_msg - - -class TestDatabaseTargeting: - """Tests for multi-database support.""" - - def test_uses_mssql_bracket_quoting(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data, target_db='mssql') - - assert '[TestTable]' in sql - assert '[TestTable_ID]' in sql - - def test_converts_ntext_to_nvarchar_max(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,TableA_present,TableA_order -TableA,Table A,Test,table,,,,,,,, -TableA_ID,Table A ID,PK,key,,,int,True,,1,key,1 -Notes,Notes,Long text,property,,,ntext,False,,2,property,2 -""" - csv_file = tmp_path / "ntext.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - sql = generate_sql_schema(data, target_db='mssql') - - # ntext should be converted to nvarchar(max) - assert 'nvarchar(max)' in sql - # Check that ntext doesn't appear in actual SQL (only in comments is OK) - sql_lines = [line for line in sql.split('\n') if not line.strip().startswith('--')] - sql_without_comments = '\n'.join(sql_lines) - assert 'ntext' not in sql_without_comments.lower() - - def test_rejects_unsupported_database(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - - with pytest.raises(ValueError, match="Unsupported database"): - generate_sql_schema(data, target_db='oracle') - - def test_get_db_config_returns_correct_structure(self): - config = get_db_config('mssql') - - assert 'quote_char' in config - assert 'type_mappings' in config - assert 'supports_check_constraints' in config - assert callable(config['quote']) - - def test_db_config_quote_function(self): - config = get_db_config('mssql') - - quoted = config['quote']('TableName') - assert quoted == '[TableName]' - - -class TestExtractFieldName: - """Tests for field name extraction helper - NEW FORMAT.""" - - def test_extracts_field_from_part_id(self): - # ID fields are kept as-is (these are the actual SQL field names) - assert extract_field_name('TestTable_ID') == 'TestTable_ID' - assert extract_field_name('Equipment_ID') == 'Equipment_ID' - assert extract_field_name('Contact_ID') == 'Contact_ID' - - # Table-prefixed non-ID fields: remove lowercase table prefix - assert extract_field_name('site_City') == 'City' - assert extract_field_name('contact_City') == 'City' - assert extract_field_name('purpose_Description') == 'Description' - - def test_handles_part_id_without_underscore(self): - # Non-prefixed fields stay as-is - assert extract_field_name('SimpleField') == 'SimpleField' - assert extract_field_name('Forest') == 'Forest' - - def test_handles_multiple_underscores(self): - # Mixed case fields with underscores (not table-prefixed) - assert extract_field_name('Street_number') == 'Street_number' - assert extract_field_name('Latitude_GPS') == 'Latitude_GPS' - - -class TestSQLIntegration: - """Integration tests for complete SQL generation.""" - - def test_generates_complete_valid_schema(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql = generate_sql_schema(data) - - # Should have all major components - assert 'CREATE TABLE' in sql - assert 'PRIMARY KEY' in sql - assert 'ALTER TABLE' in sql - assert 'FOREIGN KEY' in sql - assert 'REFERENCES' in sql - - def test_table_order_is_deterministic(self, sample_csv_file): - data = parse_parts_table(sample_csv_file) - sql1 = generate_sql_schema(data, include_timestamp=False) - sql2 = generate_sql_schema(data, include_timestamp=False) - - # Should generate identical SQL on repeated calls - assert sql1 == sql2 - - def test_handles_complex_schema(self, tmp_path): - csv_content = """Part_ID,Label,Description,Part_type,Value_set_part_ID,Member_of_set_part_ID,SQL_data_type,Is_required,Default_value,Sort_order,Parent_present,Parent_order,Child_present,Child_order -Parent,Parent,Parent table,table,,,,,,,, -Parent_ID,Parent ID,Identifier for Parent,key,,,int,True,,1,key,1,, -Name,Name,Name,property,,,nvarchar(100),True,,2,property,2,, -Child,Child,Child table,table,,,,,,,, -Child_ID,Child ID,Identifier for Child,key,,,int,True,,1,,,key,1 -Parent_ID,Parent ID,Identifier for Parent,key,,,int,True,,2,,,property,2 -Status,Status,Status,property,StatusSet,,nvarchar(50),False,pending,3,,,property,3 -StatusSet,Status Set,Valid statuses,valueSet,,,,,,,,, -pending,Pending,Pending status,valueSetMember,,StatusSet,nvarchar(50),,,1,, -active,Active,Active status,valueSetMember,,StatusSet,nvarchar(50),,,2,, -""" - csv_file = tmp_path / "complex.csv" - csv_file.write_text(csv_content) - - data = parse_parts_table(csv_file) - sql = generate_sql_schema(data) - - # Parent table should be created - assert 'CREATE TABLE [Parent]' in sql - # Child table should be created - assert 'CREATE TABLE [Child]' in sql - # FK relationship should exist - assert 'REFERENCES [Parent]' in sql - # Default value should be present - assert "DEFAULT 'pending'" in sql \ No newline at end of file diff --git a/tests/unit/test_dictionary_reference.py b/tests/unit/test_dictionary_reference.py new file mode 100644 index 0000000..58cc192 --- /dev/null +++ b/tests/unit/test_dictionary_reference.py @@ -0,0 +1,336 @@ +"""Tests for dictionary reference documentation generation.""" + +import pytest +import json +from pathlib import Path +import sys + +# Add scripts directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "tests")) +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +from generate_dictionary_reference import ( + parse_parts_json, + generate_tables_markdown, + generate_value_sets_markdown, +) +from fixtures.sample_dictionary import sample_dictionary_data + + +@pytest.fixture +def sample_json_file(tmp_path): + """Create a temporary JSON file with sample dictionary data.""" + json_file = tmp_path / "test_dictionary.json" + json_file.write_text(json.dumps(sample_dictionary_data(), indent=2)) + return json_file + + +class TestParsePartsJson: + """Tests for JSON parsing.""" + + def test_parse_identifies_tables(self, sample_json_file): + data = parse_parts_json(sample_json_file) + + assert "test_table" in data["tables"] + assert data["tables"]["test_table"]["label"] == "Test Table" + assert data["tables"]["test_table"]["description"] == "A test table for demonstration" + + def test_parse_identifies_fields(self, sample_json_file): + data = parse_parts_json(sample_json_file) + + fields = data["tables"]["test_table"]["fields"] + assert len(fields) >= 3 + + # Check primary key + pk_field = next(f for f in fields if f["part_id"] == "TestTable_ID") + assert pk_field["part_type"] == "key" + assert pk_field["is_required"] is True + assert pk_field["sql_data_type"] == "int" + + def test_parse_identifies_value_sets(self, sample_json_file): + data = parse_parts_json(sample_json_file) + + assert "StatusSet" in data["value_sets"] + assert data["value_sets"]["StatusSet"]["label"] == "Status Set" + assert len(data["value_sets"]["StatusSet"]["members"]) == 3 + + def test_parse_sorts_fields_by_sort_order(self, sample_json_file): + data = parse_parts_json(sample_json_file) + + fields = data["tables"]["test_table"]["fields"] + sort_orders = [f["sort_order"] for f in fields] + assert sort_orders == sorted(sort_orders) + + def test_parse_sorts_value_set_members(self, sample_json_file): + data = parse_parts_json(sample_json_file) + + members = data["value_sets"]["StatusSet"]["members"] + sort_orders = [m["sort_order"] for m in members] + assert sort_orders == sorted(sort_orders) + + +class TestGenerateTablesMarkdown: + """Tests for tables markdown generation.""" + + def test_generates_valid_markdown(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + assert "# Database Tables" in markdown + assert "## Tables" in markdown + + def test_includes_table_headings(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + assert "### Test Table" in markdown + assert "A test table for demonstration" in markdown + + def test_includes_table_anchors(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Check for invisible anchor span + assert '' in markdown + + def test_includes_field_anchors(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Check for field anchors in description column + assert '' in markdown + assert '' in markdown + + def test_generates_fields_table(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Check table header + assert "| Field | SQL Type | Value Set | Required | Description | Constraints |" in markdown + assert "|-------|----------|-----------|----------|-------------|-------------|" in markdown + + def test_marks_primary_keys(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Primary key should have PK marker + assert "int **(PK)**" in markdown + + def test_marks_required_fields(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Should have checkmarks for required fields + assert "✓" in markdown + + def test_links_to_value_sets(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Should link to value set + assert "[StatusSet](valuesets.md#StatusSet)" in markdown + + +class TestGenerateValueSetsMarkdown: + """Tests for value sets markdown generation.""" + + def test_generates_valid_markdown(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_value_sets_markdown(data) + + assert "# Value Sets" in markdown + + def test_includes_value_set_headings(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_value_sets_markdown(data) + + assert "## Status Set" in markdown + assert "Valid status values for records" in markdown + + def test_includes_value_set_anchors(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_value_sets_markdown(data) + + # Check for invisible anchor span + assert '' in markdown + + def test_includes_member_anchors(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_value_sets_markdown(data) + + # Check for member anchors + assert '' in markdown + assert '' in markdown + + def test_generates_members_table(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_value_sets_markdown(data) + + # Check table header + assert "| Value | Description |" in markdown + assert "|-------|-------------|" in markdown + + def test_lists_all_members(self, sample_json_file): + data = parse_parts_json(sample_json_file) + markdown = generate_value_sets_markdown(data) + + assert "`active`" in markdown + assert "Record is currently active" in markdown + assert "`inactive`" in markdown + assert "Record is currently inactive" in markdown + + def test_handles_empty_value_sets(self, tmp_path): + # JSON with no value sets + json_content = { + "parts": [ + { + "Part_ID": "test_table", + "Label": "Test Table", + "Description": "A test table", + "Part_type": "table", + } + ] + } + json_file = tmp_path / "empty_valuesets.json" + json_file.write_text(json.dumps(json_content)) + + data = parse_parts_json(json_file) + markdown = generate_value_sets_markdown(data) + + assert "No value sets currently appear in dictionary" in markdown + + +class TestIntegration: + """Integration tests checking cross-referencing.""" + + def test_value_set_links_are_bidirectional(self, sample_json_file): + data = parse_parts_json(sample_json_file) + tables_md = generate_tables_markdown(data) + valuesets_md = generate_value_sets_markdown(data) + + # Table should link to value set + assert "[StatusSet](valuesets.md#StatusSet)" in tables_md + + # Value set should have anchor that table links to + assert '' in valuesets_md + + +class TestEdgeCases: + """Test edge cases and error handling for dictionary reference generation.""" + + def test_empty_description_handling(self, tmp_path): + """Test handling of empty descriptions.""" + json_data = { + "parts": [ + {"Part_ID": "test_table", "Label": "Test Table", "Description": "", "Part_type": "table"} + ] + } + json_file = tmp_path / "empty_desc.json" + json_file.write_text(json.dumps(json_data)) + + # Empty descriptions should not be allowed by validation + from pydantic import ValidationError + with pytest.raises(ValidationError): + data = parse_parts_json(json_file) + + def test_table_with_no_fields(self, tmp_path): + """Test handling of table with no fields.""" + json_data = { + "parts": [ + {"Part_ID": "empty_table", "Label": "Empty Table", "Description": "No fields", "Part_type": "table"} + ] + } + json_file = tmp_path / "no_fields.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + markdown = generate_tables_markdown(data) + + # Should handle tables with no fields + assert "### Empty Table" in markdown + assert "No fields" in markdown + + def test_value_set_with_no_members(self, tmp_path): + """Test handling of value set with no members.""" + json_data = { + "parts": [ + {"Part_ID": "EmptySet", "Label": "Empty Set", "Description": "No members", "Part_type": "valueSet"} + ] + } + json_file = tmp_path / "empty_set.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + markdown = generate_value_sets_markdown(data) + + # Should handle empty value sets + assert "## Empty Set" in markdown + assert "No members" in markdown + + def test_special_characters_in_descriptions(self, tmp_path): + """Test that special characters in descriptions are handled properly.""" + json_data = { + "parts": [ + { + "Part_ID": "test_table", + "Label": "Test Table", + "Description": "Table with special chars: < > & \" '", + "Part_type": "table", + } + ] + } + json_file = tmp_path / "special_chars.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + markdown = generate_tables_markdown(data) + + # Should include the description with special characters + assert "special chars" in markdown + + def test_very_long_table_names(self, tmp_path): + """Test handling of very long table names.""" + long_name = "VeryLongTableNameThatExceedsNormalConventionsButIsStillValid" + json_data = { + "parts": [ + { + "Part_ID": long_name, + "Label": "Very Long Table Name", + "Description": "Test long names", + "Part_type": "table", + } + ] + } + json_file = tmp_path / "long_name.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + markdown = generate_tables_markdown(data) + + # Should handle long table names + assert "Very Long Table Name" in markdown + assert f'' in markdown + + def test_missing_file_raises_error(self, tmp_path): + """Test that missing JSON file raises appropriate error.""" + non_existent = tmp_path / "nonexistent.json" + + with pytest.raises(FileNotFoundError): + parse_parts_json(non_existent) + + def test_malformed_json_raises_error(self, tmp_path): + """Test that malformed JSON raises appropriate error.""" + json_file = tmp_path / "malformed.json" + json_file.write_text("{ this is not valid json }") + + with pytest.raises(json.JSONDecodeError): + parse_parts_json(json_file) + + def test_foreign_key_display(self, sample_json_file): + """Test that foreign keys are properly displayed in markdown.""" + data = parse_parts_json(sample_json_file) + markdown = generate_tables_markdown(data) + + # Parent_ID FK should be displayed with link + assert "FK →" in markdown + assert "[TestTable_ID]" in markdown diff --git a/tests/unit/test_erd_generator.py b/tests/unit/test_erd_generator.py new file mode 100644 index 0000000..1cb689f --- /dev/null +++ b/tests/unit/test_erd_generator.py @@ -0,0 +1,228 @@ +""" +Tests for ERD generator module. +""" + +import pytest +from pathlib import Path +import json +import sys + +# Add src and scripts to path +project_root = Path(__file__).parent.parent +sys.path.insert(0, str(project_root / 'src')) +sys.path.insert(0, str(project_root / 'scripts')) + +from generate_erd import ( + generate_erd_data, + generate_erd_html, + ERDTable, + ERDField, + ERDRelationship +) + + +@pytest.fixture +def sample_parts_data(): + """Sample parsed parts data for testing.""" + return { + 'tables': { + 'contact': { + 'label': 'Contact', + 'description': 'Stores contact information', + 'fields': [ + { + 'part_id': 'Contact_ID', + 'label': 'Contact ID', + 'description': 'Primary key', + 'part_type': 'key', + 'sql_data_type': 'int', + 'is_required': True, + 'default_value': '', + 'fk_to': '', + 'value_set': '', + 'sort_order': 1 + }, + { + 'part_id': 'First_name', + 'label': 'First Name', + 'description': 'Contact first name', + 'part_type': 'property', + 'sql_data_type': 'nvarchar(255)', + 'is_required': False, + 'default_value': '', + 'fk_to': '', + 'value_set': '', + 'sort_order': 2 + } + ] + }, + 'metadata': { + 'label': 'Metadata', + 'description': 'Stores metadata', + 'fields': [ + { + 'part_id': 'Metadata_ID', + 'label': 'Metadata ID', + 'description': 'Primary key', + 'part_type': 'key', + 'sql_data_type': 'int', + 'is_required': True, + 'default_value': '', + 'fk_to': '', + 'value_set': '', + 'sort_order': 1 + }, + { + 'part_id': 'Contact_ID', + 'label': 'Contact ID', + 'description': 'Foreign key to contact', + 'part_type': 'property', + 'sql_data_type': 'int', + 'is_required': False, + 'default_value': '', + 'fk_to': 'Contact_ID', + 'value_set': '', + 'sort_order': 2 + } + ] + } + }, + 'value_sets': {}, + 'metadata': {}, + 'id_field_locations': {} + } + + +def test_generate_erd_data(sample_parts_data): + """Test ERD data generation from parts data.""" + erd_data = generate_erd_data(sample_parts_data) + + # Check structure + assert 'tables' in erd_data + assert 'relationships' in erd_data + + # Check tables + assert len(erd_data['tables']) == 2 + table_ids = [t['id'] for t in erd_data['tables']] + assert 'contact' in table_ids + assert 'metadata' in table_ids + + # Check relationships + assert len(erd_data['relationships']) == 1 + rel = erd_data['relationships'][0] + assert rel['from_table'] == 'metadata' + assert rel['to_table'] == 'contact' + assert rel['from_field'] == 'Contact ID' + + +def test_erd_field_detection(sample_parts_data): + """Test that primary and foreign keys are correctly identified.""" + erd_data = generate_erd_data(sample_parts_data) + + # Find contact table + contact_table = next(t for t in erd_data['tables'] if t['id'] == 'contact') + + # Check PK field + pk_field = next(f for f in contact_table['fields'] if f['name'] == 'Contact ID') + assert pk_field['is_pk'] is True + assert pk_field['is_fk'] is False + + # Find metadata table + metadata_table = next(t for t in erd_data['tables'] if t['id'] == 'metadata') + + # Check FK field + fk_field = next(f for f in metadata_table['fields'] if f['name'] == 'Contact ID') + assert fk_field['is_fk'] is True + assert fk_field['fk_target'] == 'contact.Contact_ID' + + +def test_generate_jointjs_html(sample_parts_data, tmp_path): + """Test JointJS HTML generation.""" + erd_data = generate_erd_data(sample_parts_data) + output_path = tmp_path / 'erd_test.html' + + generate_erd_html(erd_data, output_path, library='jointjs') + + # Check file was created + assert output_path.exists() + + # Check content + content = output_path.read_text() + assert 'JointJS' in content or 'jointjs' in content + assert 'Contact' in content + assert 'Metadata' in content + assert 'const erdData' in content + assert 'dagre' in content # Ensure layout engine is included + + +def test_invalid_library(sample_parts_data, tmp_path): + """Test that invalid library raises error.""" + erd_data = generate_erd_data(sample_parts_data) + output_path = tmp_path / 'erd_invalid.html' + + with pytest.raises(ValueError, match="Unsupported library"): + generate_erd_html(erd_data, output_path, library='invalid') + + +def test_empty_tables(): + """Test ERD generation with no tables.""" + empty_data = { + 'tables': {}, + 'value_sets': {}, + 'metadata': {}, + 'id_field_locations': {} + } + + erd_data = generate_erd_data(empty_data) + + assert erd_data['tables'] == [] + assert erd_data['relationships'] == [] + + +def test_self_referential_relationship(): + """Test handling of self-referential foreign keys (parent keys).""" + data = { + 'tables': { + 'category': { + 'label': 'Category', + 'description': 'Hierarchical categories', + 'fields': [ + { + 'part_id': 'Category_ID', + 'label': 'Category ID', + 'description': 'Primary key', + 'part_type': 'key', + 'sql_data_type': 'int', + 'is_required': True, + 'default_value': '', + 'fk_to': '', + 'value_set': '', + 'sort_order': 1 + }, + { + 'part_id': 'Parent_Category_ID', + 'label': 'Parent Category ID', + 'description': 'Parent category', + 'part_type': 'property', + 'sql_data_type': 'int', + 'is_required': False, + 'default_value': '', + 'fk_to': 'Category_ID', + 'value_set': '', + 'sort_order': 2 + } + ] + } + }, + 'value_sets': {}, + 'metadata': {}, + 'id_field_locations': {} + } + + erd_data = generate_erd_data(data) + + # Should have one self-referential relationship + assert len(erd_data['relationships']) == 1 + rel = erd_data['relationships'][0] + assert rel['from_table'] == 'category' + assert rel['to_table'] == 'category' diff --git a/tests/unit/test_helpers.py b/tests/unit/test_helpers.py new file mode 100644 index 0000000..d4572c8 --- /dev/null +++ b/tests/unit/test_helpers.py @@ -0,0 +1,547 @@ +"""Tests for DictionaryManager class in helpers.py.""" + +import pytest +import json +import tempfile +from pathlib import Path +import sys + +# Add src and fixtures to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) +sys.path.insert(0, str(Path(__file__).parent.parent / "fixtures")) + +from open_dateaubase.data_model.helpers import DictionaryManager +from open_dateaubase.data_model.models import Dictionary +from sample_dictionary import ( + sample_dictionary_data, + complex_dictionary_data, + edge_case_dictionary_data, + invalid_dictionary_data, +) + + +class TestDictionaryManager: + """Test basic DictionaryManager functionality.""" + + def test_load_valid_dictionary(self, tmp_path): + """Test loading a valid dictionary JSON file.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + assert manager.dictionary is not None + assert ( + len(manager.dictionary.parts) == 9 + ) # Count parts in sample data (including value set members) + assert manager.path == dict_file + + def test_load_invalid_json(self, tmp_path): + """Test loading invalid JSON raises error.""" + dict_file = tmp_path / "invalid.json" + dict_file.write_text("{ invalid json }") + + with pytest.raises(json.JSONDecodeError): + DictionaryManager.load(dict_file) + + def test_save_dictionary(self, tmp_path): + """Test saving dictionary to JSON file.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + # Modify and save + save_file = tmp_path / "saved_dict.json" + manager.save(save_file) + + assert save_file.exists() + saved_data = json.loads(save_file.read_text()) + + # Should have PascalCase keys + assert "Part_ID" in saved_data["parts"][0] + assert "Part_type" in saved_data["parts"][0] + + def test_save_to_original_path(self, tmp_path): + """Test saving dictionary to original path when no path specified.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.save() # Save to original path + + # File should be updated + modified_time = dict_file.stat().st_mtime + assert modified_time > 0 + + +class TestValueSetOperations: + """Test value set creation and management.""" + + def test_create_value_set(self, tmp_path): + """Test creating a new value set.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.create_value_set("NewStatusSet", "New Status Values", "Test status set") + + # Check value set was created + new_vs = manager._find_part("NewStatusSet") + assert new_vs is not None + assert new_vs.part_id == "NewStatusSet" + assert new_vs.label == "New Status Values" + assert hasattr(new_vs, "part_type") and new_vs.part_type == "valueSet" + + def test_create_duplicate_value_set_fails(self, tmp_path): + """Test creating duplicate value set raises error.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + with pytest.raises(ValueError, match="Part 'StatusSet' already exists"): + manager.create_value_set("StatusSet", "Duplicate", "Should fail") + + def test_add_value_set_member(self, tmp_path): + """Test adding a member to existing value set.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.add_value_set_member( + "StatusSet", "suspended", "Suspended", "Suspended status", order=4 + ) + + # Check member was added + new_member = manager._find_part("suspended") + assert new_member is not None + assert new_member.part_id == "suspended" + assert new_member.label == "Suspended" + assert ( + hasattr(new_member, "member_of_set_part_id") + and new_member.member_of_set_part_id == "StatusSet" + ) + assert new_member.sort_order == 4 + + def test_add_member_to_nonexistent_value_set_fails(self, tmp_path): + """Test adding member to non-existent value set raises error.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + with pytest.raises( + ValueError, match="Value set 'NonExistentSet' does not exist" + ): + manager.add_value_set_member( + "NonExistentSet", "test", "Test", "Test member" + ) + + def test_add_duplicate_member_fails(self, tmp_path): + """Test adding duplicate member raises error.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + with pytest.raises(ValueError, match="Part 'active' already exists"): + manager.add_value_set_member( + "StatusSet", "active", "Duplicate Active", "Should fail" + ) + + +class TestTableOperations: + """Test table creation and field management.""" + + def test_create_table(self, tmp_path): + """Test creating a new table.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.create_table("new_table", "New Table", "A new test table") + + # Check table was created + new_table = manager._find_part("new_table") + assert new_table is not None + assert new_table.part_id == "new_table" + assert new_table.label == "New Table" + assert hasattr(new_table, "part_type") and new_table.part_type == "table" + + def test_create_duplicate_table_fails(self, tmp_path): + """Test creating duplicate table raises error.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + with pytest.raises(ValueError, match="Part 'test_table' already exists"): + manager.create_table("test_table", "Duplicate", "Should fail") + + def test_add_field_to_table(self, tmp_path): + """Test adding a new field to existing table.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.add_field_to_table( + table_id="test_table", + field_id="NewField", + label="New Field", + description="A new test field", + role="property", + sql_data_type="nvarchar(100)", + required=True, + order=5, + ) + + # Check field was created + new_field = manager._find_part("NewField") + assert new_field is not None + assert new_field.part_id == "NewField" + assert new_field.label == "New Field" + assert hasattr(new_field, "part_type") and new_field.part_type == "property" + assert ( + hasattr(new_field, "table_presence") + and "test_table" in new_field.table_presence + ) + + def test_add_field_to_nonexistent_table_fails(self, tmp_path): + """Test adding field to non-existent table raises error.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + with pytest.raises(ValueError, match="Table 'NonExistentTable' does not exist"): + manager.add_field_to_table( + "NonExistentTable", "TestField", "Test Field", "Test description" + ) + + def test_add_key_field(self, tmp_path): + """Test adding a primary key field.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.add_field_to_table( + table_id="test_table", + field_id="NewTable_ID", + label="New Table ID", + description="Primary key for new table", + role="key", + sql_data_type="int", + required=True, + order=6, + ) + + # Check key field was created + new_key = manager._find_part("NewTable_ID") + assert new_key is not None + assert new_key.part_id == "NewTable_ID" + assert hasattr(new_key, "part_type") and new_key.part_type == "key" + assert ( + hasattr(new_key, "table_presence") + and "test_table" in new_key.table_presence + ) + + def test_add_parent_key(self, tmp_path): + """Test adding a parent key for hierarchical relationships.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + manager.add_parent_key( + table_id="test_table", + parent_key_id="Parent_TestTable_ID", + ancestor_key_id="TestTable_ID", + label="Parent TestTable ID", + description="Parent reference", + sql_data_type="int", + required=False, + order=5, + ) + + # Check parent key was created + parent_key = manager._find_part("Parent_TestTable_ID") + assert parent_key is not None + assert parent_key.part_id == "Parent_TestTable_ID" + assert hasattr(parent_key, "part_type") and parent_key.part_type == "parentKey" + assert ( + hasattr(parent_key, "ancestor_part_id") + and parent_key.ancestor_part_id == "TestTable_ID" + ) + + +class TestQueryOperations: + """Test query methods for retrieving dictionary information.""" + + def test_get_value_set_members(self, tmp_path): + """Test retrieving members of a value set.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + members = manager.get_value_set_members("Status") + + assert len(members) == 3 + member_ids = [m["Part_ID"] for m in members] + assert "active" in member_ids + assert "inactive" in member_ids + assert "pending" in member_ids + + # Check sorting by sort_order + sort_orders = [m["Sort_order"] for m in members] + assert sort_orders == sorted(sort_orders) + + def test_get_value_set_members_nonexistent_field(self, tmp_path): + """Test getting members for non-existent field returns empty list.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + members = manager.get_value_set_members("NonExistentField") + + assert members == [] + + def test_get_value_set_members_field_without_value_set(self, tmp_path): + """Test getting members for field without value set returns empty list.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + members = manager.get_value_set_members( + "Description" + ) # This field has no value set + + assert members == [] + + def test_get_table_columns(self, tmp_path): + """Test retrieving all columns for a table.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + columns = manager.get_table_columns("test_table") + + assert len(columns) == 4 # TestTable_ID, Status, Description, Parent_ID + column_ids = [c["Part_ID"] for c in columns] + assert "TestTable_ID" in column_ids + assert "Status" in column_ids + assert "Description" in column_ids + assert "Parent_ID" in column_ids + + # Check sorting by order + orders = [c["Order"] for c in columns] + assert orders == sorted(orders) + + def test_get_table_columns_nonexistent_table(self, tmp_path): + """Test getting columns for non-existent table returns empty list.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + columns = manager.get_table_columns("NonExistentTable") + + assert columns == [] + + def test_get_primary_keys(self, tmp_path): + """Test retrieving all primary keys in dictionary.""" + dict_data = complex_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + primary_keys = manager.get_primary_keys() + + assert len(primary_keys) == 2 # Contact_ID, Project_ID + key_ids = [k["Part_ID"] for k in primary_keys] + assert "Contact_ID" in key_ids + assert "Project_ID" in key_ids + + def test_list_tables(self, tmp_path): + """Test listing all tables in dictionary.""" + dict_data = complex_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + tables = manager.list_tables() + + assert len(tables) == 3 # contact, project, project_has_contact + assert "contact" in tables + assert "project" in tables + assert "project_has_contact" in tables + + def test_list_value_sets(self, tmp_path): + """Test listing all value sets in dictionary.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + value_sets = manager.list_value_sets() + + assert len(value_sets) == 1 + assert "StatusSet" in value_sets + + +class TestEdgeCasesAndErrorHandling: + """Test edge cases and error handling in DictionaryManager.""" + + def test_handle_very_long_field_names(self, tmp_path): + """Test handling of fields with very long names.""" + dict_data = edge_case_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + # Should be able to load and work with long field names + long_field = manager._find_part( + "VeryLongFieldNameThatExceedsNormalDatabaseLimitsAndMightCauseIssues" + ) + assert long_field is not None + assert len(long_field.part_id) > 50 + + def test_handle_special_characters_in_names(self, tmp_path): + """Test handling of special characters in table/field names.""" + dict_data = edge_case_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + # Should handle special characters in labels but not in part IDs + special_table = manager._find_part("special_table") + assert special_table is not None + assert "Special-Table!" in special_table.label + assert "@" in special_table.description + + def test_validate_method(self, tmp_path): + """Test explicit validation method.""" + dict_data = sample_dictionary_data() + dict_file = tmp_path / "test_dict.json" + dict_file.write_text(json.dumps(dict_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + # Should not raise for valid data + manager.validate() + + # Corrupt the data in a way that will fail validation + original_part_id = manager.dictionary.parts[0].part_id + manager.dictionary.parts[0].part_id = "" # Invalid empty ID + with pytest.raises(ValueError): + manager.validate() + + # Restore for cleanup + manager.dictionary.parts[0].part_id = original_part_id + + def test_build_complete_dictionary_workflow(self, tmp_path): + """Test building a complete dictionary from scratch.""" + # Start with empty dictionary + empty_data = {"parts": []} + dict_file = tmp_path / "empty_dict.json" + dict_file.write_text(json.dumps(empty_data, indent=2)) + + manager = DictionaryManager.load(dict_file) + + # Create value set + manager.create_value_set( + "PrioritySet", "Priority Levels", "Task priority levels" + ) + manager.add_value_set_member( + "PrioritySet", "high", "High", "High priority", order=1 + ) + manager.add_value_set_member( + "PrioritySet", "medium", "Medium", "Medium priority", order=2 + ) + manager.add_value_set_member( + "PrioritySet", "low", "Low", "Low priority", order=3 + ) + + # Create table + manager.create_table("task", "Task", "Task management table") + + # Add fields to table + manager.add_field_to_table( + "task", + "Task_ID", + "Task ID", + "Primary key", + role="key", + sql_data_type="int", + required=True, + order=1, + ) + manager.add_field_to_table( + "task", + "Title", + "Title", + "Task title", + role="property", + sql_data_type="nvarchar(255)", + required=True, + order=2, + ) + manager.add_field_to_table( + "task", + "Priority", + "Priority", + "Task priority", + role="property", + sql_data_type="nvarchar(20)", + required=False, + value_set_id="PrioritySet", + order=3, + ) + manager.add_field_to_table( + "task", + "Created_Date", + "Created Date", + "Creation timestamp", + role="property", + sql_data_type="datetime", + required=True, + default_value="GETDATE()", + order=4, + ) + + # Verify the complete structure + tables = manager.list_tables() + value_sets = manager.list_value_sets() + task_columns = manager.get_table_columns("task") + priority_members = manager.get_value_set_members("Priority") + + assert len(tables) == 1 + assert len(value_sets) == 1 + assert len(task_columns) == 4 # Task_ID, Title, Priority, Created_Date + assert len(priority_members) == 3 + + # Verify specific relationships + priority_field = manager._find_part("Priority") + assert ( + hasattr(priority_field, "value_set_part_id") + and priority_field.value_set_part_id == "PrioritySet" + ) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py new file mode 100644 index 0000000..73a0514 --- /dev/null +++ b/tests/unit/test_models.py @@ -0,0 +1,145 @@ +"""Tests for Pydantic models.""" + +import pytest +import sys +from pathlib import Path + +# Add src to path +sys.path.insert(0, str(Path(__file__).parent.parent / "src")) + +from open_dateaubase.data_model.models import ( + Dictionary, + TablePart, + KeyPart, + PropertyPart, + TablePresence, + ValueSetPart, + ValueSetMemberPart, + ParentKeyPart, +) + + +class TestTablePresence: + def test_valid_table_presence(self): + presence = TablePresence( + role="key", required=True, order=1, relationship_type=None + ) + assert presence.role == "key" + assert presence.required is True + assert presence.order == 1 + + +class TestTablePart: + def test_valid_table(self): + table = TablePart( + Part_ID="test_table", + Label="Test Table", + Description="A test table", + Part_type="table", + Sort_order=None, + ) + assert table.part_id == "test_table" + + +class TestKeyPart: + def test_valid_key(self): + key = KeyPart( + Part_ID="Test_ID", + Label="Test ID", + Description="Test identifier", + Part_type="key", + SQL_data_type=None, + Is_required=False, + Default_value=None, + Value_set_part_ID=None, + table_presence={ + "test_table": TablePresence( + role="key", required=True, order=1, relationship_type=None + ) + }, + ) + assert key.part_id == "Test_ID" + + def test_key_without_id_suffix(self): + with pytest.raises(ValueError, match="should end with '_ID'"): + KeyPart( + Part_ID="TestKey", + Label="Test", + Description="Test", + Part_type="key", + SQL_data_type=None, + Is_required=False, + Default_value=None, + Value_set_part_ID=None, + table_presence={ + "test": TablePresence( + role="key", required=True, order=1, relationship_type=None + ) + }, + ) + + +class TestParentKeyPart: + def test_valid_parent_key(self): + parent = ParentKeyPart( + Part_ID="Parent_ID", + Label="Parent", + Description="Hierarchical parent", + Part_type="parentKey", + Ancestor_part_ID="Test_ID", + SQL_data_type=None, + Is_required=False, + Default_value=None, + Value_set_part_ID=None, + table_presence={ + "test": TablePresence( + role="property", required=False, order=2, relationship_type=None + ) + }, + ) + assert parent.ancestor_part_id == "Test_ID" + + +class TestDictionary: + def test_valid_dictionary(self): + data = { + "parts": [ + { + "Part_ID": "test_table", + "Label": "Test", + "Description": "Test table", + "Part_type": "table", + }, + { + "Part_ID": "Test_ID", + "Label": "Test ID", + "Description": "Test key", + "Part_type": "key", + "table_presence": { + "test_table": {"role": "key", "required": True, "order": 1} + }, + }, + ] + } + dictionary = Dictionary.model_validate(data) + assert len(dictionary.parts) == 2 + + def test_duplicate_part_ids(self): + data = { + "parts": [ + { + "Part_ID": "duplicate", + "Label": "Dup 1", + "Description": "First", + "Part_type": "table", + }, + { + "Part_ID": "duplicate", + "Label": "Dup 2", + "Description": "Second", + "Part_type": "table", + }, + ] + } + with pytest.raises(ValueError, match="Duplicate Part_IDs"): + Dictionary.model_validate(data) diff --git a/tests/unit/test_sql_generator.py b/tests/unit/test_sql_generator.py new file mode 100644 index 0000000..b3d15d5 --- /dev/null +++ b/tests/unit/test_sql_generator.py @@ -0,0 +1,323 @@ +"""Tests for SQL schema generation from dictionary.""" + +import pytest +import json +from pathlib import Path +import sys + +# Add scripts directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "tests")) +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + +from generate_sql import ( + parse_parts_json, + generate_sql_schema, + generate_field_definition, + generate_foreign_key_constraint, + validate_no_circular_fks, + get_db_config, + extract_field_name, + generate_sql_schemas, +) +from fixtures.sample_dictionary import sample_dictionary_data + + +@pytest.fixture +def sample_json_file(tmp_path): + """Create a temporary JSON file with sample dictionary data.""" + json_file = tmp_path / "test_dictionary.json" + json_file.write_text(json.dumps(sample_dictionary_data(), indent=2)) + return json_file + + +class TestParsePartsJson: + """Tests for JSON parsing function.""" + + def test_parse_identifies_tables(self, sample_json_file): + data = parse_parts_json(sample_json_file) + assert "test_table" in data["tables"] + assert data["tables"]["test_table"]["label"] == "Test Table" + + def test_parse_identifies_fields(self, sample_json_file): + data = parse_parts_json(sample_json_file) + fields = data["tables"]["test_table"]["fields"] + + # Should have: ID, Status, Description, Parent_ID + assert len(fields) >= 3 + + # Check primary key field exists + pk_field = next((f for f in fields if f["part_id"] == "TestTable_ID"), None) + assert pk_field is not None + assert pk_field["part_type"] == "key" + assert pk_field["is_required"] is True + + def test_parse_identifies_value_sets(self, sample_json_file): + data = parse_parts_json(sample_json_file) + assert "StatusSet" in data["value_sets"] + assert len(data["value_sets"]["StatusSet"]["members"]) == 3 + + +class TestGenerateSQLSchema: + """Tests for SQL schema generation.""" + + def test_generates_create_table_statement(self, sample_json_file): + data = parse_parts_json(sample_json_file) + sql = generate_sql_schema(data) + assert "CREATE TABLE [test_table]" in sql + + def test_includes_all_fields(self, sample_json_file): + data = parse_parts_json(sample_json_file) + sql = generate_sql_schema(data) + + assert "[TestTable_ID]" in sql + assert "[Status]" in sql + assert "[Description]" in sql + + def test_generates_primary_key_constraint(self, sample_json_file): + data = parse_parts_json(sample_json_file) + sql = generate_sql_schema(data) + + assert "CONSTRAINT [PK_test_table] PRIMARY KEY" in sql + assert "[TestTable_ID]" in sql + + def test_marks_required_fields_not_null(self, sample_json_file): + data = parse_parts_json(sample_json_file) + sql = generate_sql_schema(data) + + lines = sql.split("\n") + testtable_id_line = next((l for l in lines if "[TestTable_ID]" in l and "PRIMARY KEY" not in l), None) + assert testtable_id_line is not None + assert "NOT NULL" in testtable_id_line + + def test_generates_foreign_key_constraints(self, sample_json_file): + data = parse_parts_json(sample_json_file) + sql = generate_sql_schema(data) + + assert "ALTER TABLE [test_table]" in sql + assert "FOREIGN KEY ([Parent_ID])" in sql + assert "REFERENCES [TestTable] ([TestTable_ID])" in sql + + +class TestExtractFieldName: + """Tests for field name extraction helper.""" + + def test_extracts_field_from_part_id(self): + # ID fields are kept as-is + assert extract_field_name("TestTable_ID") == "TestTable_ID" + assert extract_field_name("Equipment_ID") == "Equipment_ID" + + # Table-prefixed non-ID fields: remove lowercase table prefix + assert extract_field_name("site_City") == "City" + assert extract_field_name("contact_City") == "City" + + def test_handles_part_id_without_underscore(self): + assert extract_field_name("SimpleField") == "SimpleField" + + def test_handles_multiple_underscores(self): + assert extract_field_name("Street_number") == "Street_number" + + +class TestDatabaseTargeting: + """Tests for multi-database support.""" + + def test_uses_mssql_bracket_quoting(self, sample_json_file): + data = parse_parts_json(sample_json_file) + sql = generate_sql_schema(data, target_db="mssql") + + assert "[test_table]" in sql + assert "[TestTable_ID]" in sql + + def test_rejects_unsupported_database(self, sample_json_file): + data = parse_parts_json(sample_json_file) + + with pytest.raises(ValueError, match="Unsupported database"): + generate_sql_schema(data, target_db="oracle") + + def test_get_db_config_returns_correct_structure(self): + config = get_db_config("mssql") + + assert "quote_char" in config + assert "type_mappings" in config + assert "supports_check_constraints" in config + assert callable(config["quote"]) + + +class TestEdgeCases: + """Test edge cases and error handling for SQL generation.""" + + def test_deprecated_ntext_type_mapping(self, tmp_path): + """Test that deprecated ntext type is mapped to nvarchar(max).""" + json_data = { + "parts": [ + {"Part_ID": "test_table", "Label": "Test", "Description": "Test", "Part_type": "table"}, + { + "Part_ID": "Notes", + "Label": "Notes", + "Description": "Long text", + "Part_type": "property", + "SQL_data_type": "ntext", + "Is_required": False, + "table_presence": {"test_table": {"role": "property", "required": False, "order": 1}}, + }, + ] + } + json_file = tmp_path / "ntext.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + sql = generate_sql_schema(data) + + # ntext should be converted to nvarchar(max) + assert "nvarchar(max)" in sql + # ntext should not appear in actual SQL (only in comments is OK) + sql_lines = [line for line in sql.split("\n") if not line.strip().startswith("--")] + sql_without_comments = "\n".join(sql_lines) + assert "ntext" not in sql_without_comments.lower() + + def test_boolean_default_value_conversion(self, tmp_path): + """Test that boolean default values are converted to 0/1.""" + json_data = { + "parts": [ + {"Part_ID": "test_table", "Label": "Test", "Description": "Test", "Part_type": "table"}, + { + "Part_ID": "IsActive", + "Label": "Is Active", + "Description": "Boolean field", + "Part_type": "property", + "SQL_data_type": "bit", + "Default_value": "True", + "Is_required": False, + "table_presence": {"test_table": {"role": "property", "required": False, "order": 1}}, + }, + ] + } + json_file = tmp_path / "bool.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + sql = generate_sql_schema(data) + + # Boolean default should be converted to 1 + assert "DEFAULT 1" in sql + + def test_very_long_field_names(self): + """Test that very long field names are handled correctly.""" + long_name = "VeryLongFieldNameThatExceedsNormalDatabaseLimitsAndMightCauseIssues" + assert extract_field_name(long_name) == long_name + + def test_field_names_with_numbers(self): + """Test field names containing numbers.""" + assert extract_field_name("Field_1") == "Field_1" + assert extract_field_name("table_Field123") == "Field123" + assert extract_field_name("Field123_ID") == "Field123_ID" + + def test_empty_string_field_name(self): + """Test handling of empty string field name.""" + assert extract_field_name("") == "" + + def test_single_character_field_names(self): + """Test single character field names.""" + assert extract_field_name("A") == "A" + assert extract_field_name("X") == "X" + + def test_empty_tables_list(self, tmp_path): + """Test handling of dictionary with no tables.""" + json_data = {"parts": []} + json_file = tmp_path / "empty.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + sql = generate_sql_schema(data) + + # Should generate valid SQL header even with no tables + assert "Auto-generated SQL schema" in sql + + def test_missing_json_file_raises_error(self, tmp_path): + """Test that missing JSON file raises appropriate error.""" + non_existent = tmp_path / "nonexistent.json" + + with pytest.raises(FileNotFoundError): + parse_parts_json(non_existent) + + def test_malformed_json_raises_error(self, tmp_path): + """Test that malformed JSON raises appropriate error.""" + json_file = tmp_path / "malformed.json" + json_file.write_text("{ this is not valid json }") + + with pytest.raises(json.JSONDecodeError): + parse_parts_json(json_file) + + def test_self_referential_fk_allowed(self, tmp_path): + """Test that self-referential FKs (hierarchical) are allowed.""" + json_data = { + "parts": [ + {"Part_ID": "category", "Label": "Category", "Description": "Hierarchical", "Part_type": "table"}, + { + "Part_ID": "Category_ID", + "Label": "Category ID", + "Description": "PK", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": {"category": {"role": "key", "required": True, "order": 1}}, + }, + { + "Part_ID": "Parent_Category_ID", + "Label": "Parent Category ID", + "Description": "Parent", + "Part_type": "parentKey", + "Ancestor_part_ID": "Category_ID", + "SQL_data_type": "int", + "Is_required": False, + "table_presence": {"category": {"role": "property", "required": False, "order": 2}}, + }, + ] + } + json_file = tmp_path / "self_ref.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + + # Should NOT raise - self-referential FKs are allowed + sql = generate_sql_schema(data) + assert "CREATE TABLE" in sql + + def test_table_with_many_fields(self, tmp_path): + """Test handling of tables with many fields.""" + parts = [ + {"Part_ID": "big_table", "Label": "Big Table", "Description": "Table with many fields", "Part_type": "table"}, + # Add primary key + { + "Part_ID": "BigTable_ID", + "Label": "Big Table ID", + "Description": "Primary key", + "Part_type": "key", + "SQL_data_type": "int", + "Is_required": True, + "table_presence": {"big_table": {"role": "key", "required": True, "order": 1}}, + } + ] + + # Add 50 fields + for i in range(50): + parts.append({ + "Part_ID": f"Field_{i}", + "Label": f"Field {i}", + "Description": f"Field number {i}", + "Part_type": "property", + "SQL_data_type": "int", + "Is_required": False, + "table_presence": {"big_table": {"role": "property", "required": False, "order": i + 2}}, + }) + + json_data = {"parts": parts} + json_file = tmp_path / "big_table.json" + json_file.write_text(json.dumps(json_data)) + + data = parse_parts_json(json_file) + sql = generate_sql_schema(data) + + # Should generate SQL without errors + assert "CREATE TABLE [big_table]" in sql + assert "[Field_0]" in sql + assert "[Field_49]" in sql diff --git a/uv.lock b/uv.lock deleted file mode 100644 index b55939d..0000000 --- a/uv.lock +++ /dev/null @@ -1,538 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" - -[[package]] -name = "babel" -version = "2.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, -] - -[[package]] -name = "backrefs" -version = "5.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/a7/312f673df6a79003279e1f55619abbe7daebbb87c17c976ddc0345c04c7b/backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59", size = 5765857, upload-time = "2025-06-22T19:34:13.97Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/4d/798dc1f30468134906575156c089c492cf79b5a5fd373f07fe26c4d046bf/backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f", size = 380267, upload-time = "2025-06-22T19:34:05.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/07/f0b3375bf0d06014e9787797e6b7cc02b38ac9ff9726ccfe834d94e9991e/backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf", size = 392072, upload-time = "2025-06-22T19:34:06.743Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/4f345407259dd60a0997107758ba3f221cf89a9b5a0f8ed5b961aef97253/backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa", size = 397947, upload-time = "2025-06-22T19:34:08.172Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/fa31834dc27a7f05e5290eae47c82690edc3a7b37d58f7fb35a1bdbf355b/backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b", size = 399843, upload-time = "2025-06-22T19:34:09.68Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/b29af34b2c9c41645a9f4ff117bae860291780d73880f449e0b5d948c070/backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9", size = 411762, upload-time = "2025-06-22T19:34:11.037Z" }, - { url = "https://files.pythonhosted.org/packages/41/ff/392bff89415399a979be4a65357a41d92729ae8580a66073d8ec8d810f98/backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60", size = 380265, upload-time = "2025-06-22T19:34:12.405Z" }, -] - -[[package]] -name = "certifi" -version = "2025.10.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "click" -version = "8.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "markdown" -version = "3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8d/37/02347f6d6d8279247a5837082ebc26fc0d5aaeaf75aa013fcbb433c777ab/markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a", size = 364585, upload-time = "2025-09-04T20:25:22.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/ae/44c4a6a4cbb496d93c6257954260fe3a6e91b7bed2240e5dad2a717f5111/markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280", size = 107441, upload-time = "2025-09-04T20:25:21.784Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.22" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/5d/317e37b6c43325cb376a1d6439df9cc743b8ee41c84603c2faf7286afc82/mkdocs_material-9.6.22.tar.gz", hash = "sha256:87c158b0642e1ada6da0cbd798a3389b0bc5516b90e5ece4a0fb939f00bacd1c", size = 4044968, upload-time = "2025-10-15T09:21:15.409Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/82/6fdb9a7a04fb222f4849ffec1006f891a0280825a20314d11f3ccdee14eb/mkdocs_material-9.6.22-py3-none-any.whl", hash = "sha256:14ac5f72d38898b2f98ac75a5531aaca9366eaa427b0f49fc2ecf04d99b7ad84", size = 9206252, upload-time = "2025-10-15T09:21:12.175Z" }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "open-dateaubase" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "mkdocs" }, - { name = "mkdocs-material" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, -] - -[package.metadata] -requires-dist = [ - { name = "mkdocs", specifier = ">=1.6.1" }, - { name = "mkdocs-material", specifier = ">=9.6.22" }, -] - -[package.metadata.requires-dev] -dev = [{ name = "pytest", specifier = ">=8.4.2" }] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -]