From c2094144591aa83ac2f8f9b1fdbd74a020b7bf54 Mon Sep 17 00:00:00 2001 From: Jess Frazelle Date: Fri, 5 May 2023 12:19:19 -0700 Subject: [PATCH] more fixes Signed-off-by: Jess Frazelle --- .github/workflows/black.yml | 4 +- .github/workflows/mypy.yml | 38 ++++++ .github/workflows/ruff.yml | 23 +++- generate/generate.py | 265 +++++++++++++++++++++++++----------- generate/run.sh | 2 +- kittycad/examples_test.py | 210 ++++++++++++++++++++++++---- pyproject.toml | 1 + 7 files changed, 437 insertions(+), 106 deletions(-) create mode 100644 .github/workflows/mypy.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 39df52794..778de06f1 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,4 +1,4 @@ -name: Black +name: black on: push: branches: main @@ -35,4 +35,4 @@ jobs: - name: Run black shell: bash run: | - poetry run black --check --diff . + poetry run black --check --diff . generate/generate.py docs/conf.py kittycad/client_test.py kittycad/examples_test.py diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 000000000..dc0f8bb31 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,38 @@ +name: mypy +on: + push: + branches: main + paths: + - '**.py' + - .github/workflows/mypy.yml + - 'pyproject.toml' + pull_request: + paths: + - '**.py' + - .github/workflows/mypy.yml + - 'pyproject.toml' +jobs: + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + + # Installation instructions are from: https://python-poetry.org/docs/ + - name: Install dependencies + shell: bash + run: | + pip install \ + poetry + + - name: Build + shell: bash + run: | + poetry install + poetry build + + - name: Run mypy + shell: bash + run: | + poetry run mypy . diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index b44f6ec0a..4fea93d71 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -1,4 +1,4 @@ -name: Ruff +name: ruff on: push: branches: main @@ -16,4 +16,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: chartboost/ruff-action@v1 + - name: Set up Python + uses: actions/setup-python@v4 + + # Installation instructions are from: https://python-poetry.org/docs/ + - name: Install dependencies + shell: bash + run: | + pip install \ + poetry + + - name: Build + shell: bash + run: | + poetry install + poetry build + + - name: Run ruff + shell: bash + run: | + poetry run ruff check . diff --git a/generate/generate.py b/generate/generate.py index bd34f2a40..937778b4c 100755 --- a/generate/generate.py +++ b/generate/generate.py @@ -129,17 +129,61 @@ def generatePaths(cwd: str, parser: dict) -> dict: return data -def generateTypeAndExamplePython(schema: dict, data: dict) -> Tuple[str, str, str]: +def generateTypeAndExamplePython( + name: str, schema: dict, data: dict +) -> Tuple[str, str, str]: parameter_type = "" parameter_example = "" example_imports = "" if "type" in schema: if "format" in schema and schema["format"] == "uuid": - parameter_type = "str" - parameter_example = '""' + if name != "": + parameter_type = name + example_imports = example_imports + ( + "from kittycad.models." + + camel_to_snake(parameter_type) + + " import " + + parameter_type + + "\n" + ) + parameter_example = parameter_type + '("")' + else: + parameter_type = "str" + parameter_example = '""' + elif ( + schema["type"] == "string" and "enum" in schema and len(schema["enum"]) > 0 + ): + if name == "": + logging.error("schema: %s", json.dumps(schema, indent=4)) + raise Exception("Unknown type name for enum") + + parameter_type = name + + example_imports = example_imports + ( + "from kittycad.models." + + camel_to_snake(parameter_type) + + " import " + + parameter_type + + "\n" + ) + + parameter_example = ( + parameter_type + "." + camel_to_screaming_snake(schema["enum"][0]) + ) elif schema["type"] == "string": - parameter_type = "str" - parameter_example = '""' + if name != "": + parameter_type = name + example_imports = example_imports + ( + "from kittycad.models." + + camel_to_snake(parameter_type) + + " import " + + parameter_type + + "\n" + ) + parameter_example = parameter_type + '("")' + else: + parameter_type = "str" + parameter_example = '""' elif schema["type"] == "integer": parameter_type = "int" parameter_example = "10" @@ -150,33 +194,80 @@ def generateTypeAndExamplePython(schema: dict, data: dict) -> Tuple[str, str, st ): parameter_type = "float" parameter_example = "3.14" + elif schema["type"] == "array" and "items" in schema: + items_type, items_example, items_imports = generateTypeAndExamplePython( + "", schema["items"], data + ) + example_imports = example_imports + items_imports + parameter_type = "List[" + items_type + "]" + if "minItems" in schema and schema["minItems"] > 1: + parameter_example = "[" + for i in range(schema["minItems"] - 1): + parameter_example = parameter_example + items_example + ", " + parameter_example = parameter_example + "]" + else: + parameter_example = "[" + items_example + "]" + elif schema["type"] == "object" and "properties" in schema: + if name == "": + logging.error("schema: %s", json.dumps(schema, indent=4)) + raise Exception("Unknown type name for object") + + parameter_type = name + + example_imports = example_imports + ( + "from kittycad.models." + + camel_to_snake(parameter_type) + + " import " + + parameter_type + + "\n" + ) + parameter_example = name + "(" + for property_name in schema["properties"]: + prop = schema["properties"][property_name] + if "nullable" in prop: + # We don't care if it's nullable + continue + else: + ( + prop_type, + prop_example, + prop_imports, + ) = generateTypeAndExamplePython("", prop, data) + example_imports = example_imports + prop_imports + parameter_example = parameter_example + ( + "\n" + property_name + "=" + prop_example + ",\n" + ) + + parameter_example = parameter_example + ")" + elif ( + schema["type"] == "object" + and "additionalProperties" in schema + and schema["additionalProperties"] is not False + ): + items_type, items_example, items_imports = generateTypeAndExamplePython( + "", schema["additionalProperties"], data + ) + example_imports = example_imports + items_imports + parameter_type = "Dict[str, " + items_type + "]" + parameter_example = '{"": ' + items_example + "}" else: logging.error("schema: %s", json.dumps(schema, indent=4)) raise Exception("Unknown parameter type") + elif "oneOf" in schema and len(schema["oneOf"]) > 0: + # Check if each of these only has a object w 1 property. + if isNestedObjectOneOf(schema): + properties = schema["oneOf"][0]["properties"] + for prop in properties: + return generateTypeAndExamplePython(prop, properties[prop], data) + break + + return generateTypeAndExamplePython(name, schema["oneOf"][0], data) elif "$ref" in schema: parameter_type = schema["$ref"].replace("#/components/schemas/", "") - example_imports = example_imports + ( - "from kittycad.models." - + camel_to_snake(parameter_type) - + " import " - + parameter_type - + "\n" - ) # Get the schema for the reference. ref_schema = data["components"]["schemas"][parameter_type] - if "type" in ref_schema and ref_schema["type"] == "object": - parameter_example = parameter_type + "()" - elif ( - "type" in ref_schema - and ref_schema["type"] == "string" - and "enum" in ref_schema - ): - parameter_example = ( - parameter_type + "." + camel_to_screaming_snake(ref_schema["enum"][0]) - ) - else: - logging.error("schema: %s", json.dumps(ref_schema, indent=4)) - raise Exception("Unknown ref schema") + + return generateTypeAndExamplePython(parameter_type, ref_schema, data) else: logging.error("schema: %s", json.dumps(schema, indent=4)) raise Exception("Unknown parameter type") @@ -202,7 +293,7 @@ def generatePath(path: str, name: str, method: str, endpoint: dict, data: dict) endpoint_refs = getEndpointRefs(endpoint, data) parameter_refs = getParameterRefs(endpoint) request_body_refs = getRequestBodyRefs(endpoint) - request_body_type = getRequestBodyType(endpoint) + (request_body_type, request_body_schema) = getRequestBodyTypeSchema(endpoint, data) success_type = "" if len(endpoint_refs) > 0: @@ -234,7 +325,7 @@ from kittycad.types import Response parameter_type, parameter_example, more_example_imports, - ) = generateTypeAndExamplePython(parameter["schema"], data) + ) = generateTypeAndExamplePython("", parameter["schema"], data) example_imports = example_imports + more_example_imports if "nullable" in parameter["schema"] and parameter["schema"]["nullable"]: @@ -254,17 +345,20 @@ from kittycad.types import Response params_str += optional_arg if request_body_type: - if request_body_type != "bytes": - params_str += "body=" + request_body_type + ",\n" - example_imports = example_imports + ( - "from kittycad.models." - + camel_to_snake(request_body_type) - + " import " - + request_body_type - + "\n" - ) - else: + if request_body_type == "str": + params_str += "body='',\n" + elif request_body_type == "bytes": params_str += "body=bytes('some bytes', 'utf-8'),\n" + else: + # Generate an example for the schema. + rbs: dict = request_body_schema + ( + body_type, + body_example, + more_example_imports, + ) = generateTypeAndExamplePython(request_body_type, rbs, data) + params_str += "body=" + body_example + ",\n" + example_imports = example_imports + more_example_imports example_variable = "" if ( @@ -346,6 +440,7 @@ async def test_""" # Make pretty. line_length = 82 + short_sync_example = example_imports + short_sync_example cleaned_example = black.format_str( isort.api.sort_code_string( short_sync_example, @@ -1048,15 +1143,7 @@ def generateEnumType( def generateOneOfType(path: str, name: str, schema: dict, data: dict): logging.info("generating type: ", name, " at: ", path) - is_enum_with_docs = False - for one_of in schema["oneOf"]: - if one_of["type"] == "string" and "enum" in one_of and len(one_of["enum"]) == 1: - is_enum_with_docs = True - else: - is_enum_with_docs = False - break - - if is_enum_with_docs: + if isEnumWithDocsOneOf(schema): additional_docs = [] enum = [] # We want to treat this as an enum with additional docs. @@ -1085,26 +1172,7 @@ def generateOneOfType(path: str, name: str, schema: dict, data: dict): f.write("from ." + camel_to_snake(ref_name) + " import " + ref_name + "\n") all_options.append(ref_name) - is_nested_object = False - for one_of in schema["oneOf"]: - # Check if each are an object w 1 property in it. - if ( - one_of["type"] == "object" - and "properties" in one_of - and len(one_of["properties"]) == 1 - ): - for prop_name in one_of["properties"]: - nested_object = one_of["properties"][prop_name] - if "type" in nested_object and nested_object["type"] == "object": - is_nested_object = True - else: - is_nested_object = False - break - else: - is_nested_object = False - break - - if is_nested_object: + if isNestedObjectOneOf(schema): # We want to write each of the nested objects. for one_of in schema["oneOf"]: # Get the nested object. @@ -1847,9 +1915,9 @@ def getRequestBodyRefs(endpoint: dict) -> List[str]: return refs -def getRequestBodyType(endpoint: dict) -> Optional[str]: - type_name = None - +def getRequestBodyTypeSchema( + endpoint: dict, data: dict +) -> Tuple[Optional[str], Optional[dict]]: if "requestBody" in endpoint: requestBody = endpoint["requestBody"] if "content" in requestBody: @@ -1859,21 +1927,29 @@ def getRequestBodyType(endpoint: dict) -> Optional[str]: json = content[content_type]["schema"] if "$ref" in json: ref = json["$ref"].replace("#/components/schemas/", "") - return ref + type_schema = data["components"]["schemas"][ref] + return ref, type_schema + elif json != {}: + logging.error("not a ref: ", json) + raise Exception("not a ref") elif content_type == "text/plain": - return "bytes" + return "str", None elif content_type == "application/octet-stream": - return "bytes" + return "bytes", None elif content_type == "application/x-www-form-urlencoded": - json = content[content_type]["schema"] - if "$ref" in json: - ref = json["$ref"].replace("#/components/schemas/", "") - return ref + form = content[content_type]["schema"] + if "$ref" in form: + ref = form["$ref"].replace("#/components/schemas/", "") + type_schema = data["components"]["schemas"][ref] + return ref, type_schema + elif form != {}: + logging.error("not a ref: ", form) + raise Exception("not a ref") else: logging.error("unsupported content type: ", content_type) raise Exception("unsupported content type") - return type_name + return None, None def to_camel_case(s: str): @@ -1931,6 +2007,41 @@ def getOneOfRefType(schema: dict) -> str: raise Exception("Cannot get oneOf ref type for schema: ", schema) +def isNestedObjectOneOf(schema: dict) -> bool: + is_nested_object = False + for one_of in schema["oneOf"]: + # Check if each are an object w 1 property in it. + if ( + one_of["type"] == "object" + and "properties" in one_of + and len(one_of["properties"]) == 1 + ): + for prop_name in one_of["properties"]: + nested_object = one_of["properties"][prop_name] + if "type" in nested_object and nested_object["type"] == "object": + is_nested_object = True + else: + is_nested_object = False + break + else: + is_nested_object = False + break + + return is_nested_object + + +def isEnumWithDocsOneOf(schema: dict) -> bool: + is_enum_with_docs = False + for one_of in schema["oneOf"]: + if one_of["type"] == "string" and "enum" in one_of and len(one_of["enum"]) == 1: + is_enum_with_docs = True + else: + is_enum_with_docs = False + break + + return is_enum_with_docs + + # generate a random letter in the range A - Z # do not use O or I. def randletter(): diff --git a/generate/run.sh b/generate/run.sh index a250b0d12..4d95c25b1 100755 --- a/generate/run.sh +++ b/generate/run.sh @@ -15,7 +15,7 @@ poetry run python generate/generate.py # Format and lint. poetry run isort . -poetry run black . generate/generate.py docs/conf.py kittycad/client_test.py +poetry run black . generate/generate.py docs/conf.py kittycad/client_test.py kittycad/examples_test.py poetry run ruff check --fix . # We ignore errors here but we should eventually fix them. poetry run mypy . diff --git a/kittycad/examples_test.py b/kittycad/examples_test.py index d421b13fc..833542007 100644 --- a/kittycad/examples_test.py +++ b/kittycad/examples_test.py @@ -103,6 +103,8 @@ from kittycad.models.api_call_status import ApiCallStatus from kittycad.models.billing_info import BillingInfo from kittycad.models.code_language import CodeLanguage from kittycad.models.created_at_sort_mode import CreatedAtSortMode +from kittycad.models.draw_circle import DrawCircle +from kittycad.models.drawing_cmd_id import DrawingCmdId from kittycad.models.drawing_cmd_req import DrawingCmdReq from kittycad.models.drawing_cmd_req_batch import DrawingCmdReqBatch from kittycad.models.email_authentication_form import EmailAuthenticationForm @@ -629,13 +631,17 @@ def test_auth_email(): auth_email.sync( client=client, - body=EmailAuthenticationForm, + body=EmailAuthenticationForm( + email="", + ), ) # OR if you need more info (e.g. status_code) auth_email.sync_detailed( client=client, - body=EmailAuthenticationForm, + body=EmailAuthenticationForm( + email="", + ), ) @@ -648,13 +654,17 @@ async def test_auth_email_async(): await auth_email.asyncio( client=client, - body=EmailAuthenticationForm, + body=EmailAuthenticationForm( + email="", + ), ) # OR run async with more info await auth_email.asyncio_detailed( client=client, - body=EmailAuthenticationForm, + body=EmailAuthenticationForm( + email="", + ), ) @@ -745,13 +755,33 @@ def test_cmd(): cmd.sync( client=client, - body=DrawingCmdReq, + body=DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ), ) # OR if you need more info (e.g. status_code) cmd.sync_detailed( client=client, - body=DrawingCmdReq, + body=DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ), ) @@ -764,13 +794,33 @@ async def test_cmd_async(): await cmd.asyncio( client=client, - body=DrawingCmdReq, + body=DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ), ) # OR run async with more info await cmd.asyncio_detailed( client=client, - body=DrawingCmdReq, + body=DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ), ) @@ -781,13 +831,43 @@ def test_cmd_batch(): cmd_batch.sync( client=client, - body=DrawingCmdReqBatch, + body=DrawingCmdReqBatch( + cmds={ + "": DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ) + }, + file_id="", + ), ) # OR if you need more info (e.g. status_code) cmd_batch.sync_detailed( client=client, - body=DrawingCmdReqBatch, + body=DrawingCmdReqBatch( + cmds={ + "": DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ) + }, + file_id="", + ), ) @@ -800,13 +880,43 @@ async def test_cmd_batch_async(): await cmd_batch.asyncio( client=client, - body=DrawingCmdReqBatch, + body=DrawingCmdReqBatch( + cmds={ + "": DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ) + }, + file_id="", + ), ) # OR run async with more info await cmd_batch.asyncio_detailed( client=client, - body=DrawingCmdReqBatch, + body=DrawingCmdReqBatch( + cmds={ + "": DrawingCmdReq( + cmd=DrawCircle( + center=[ + 3.14, + 3.14, + ], + radius=3.14, + ), + cmd_id=DrawingCmdId(""), + file_id="", + ) + }, + file_id="", + ), ) @@ -2601,13 +2711,27 @@ def test_update_user_self(): update_user_self.sync( client=client, - body=UpdateUser, + body=UpdateUser( + company="", + discord="", + first_name="", + github="", + last_name="", + phone="", + ), ) # OR if you need more info (e.g. status_code) update_user_self.sync_detailed( client=client, - body=UpdateUser, + body=UpdateUser( + company="", + discord="", + first_name="", + github="", + last_name="", + phone="", + ), ) @@ -2620,13 +2744,27 @@ async def test_update_user_self_async(): await update_user_self.asyncio( client=client, - body=UpdateUser, + body=UpdateUser( + company="", + discord="", + first_name="", + github="", + last_name="", + phone="", + ), ) # OR run async with more info await update_user_self.asyncio_detailed( client=client, - body=UpdateUser, + body=UpdateUser( + company="", + discord="", + first_name="", + github="", + last_name="", + phone="", + ), ) @@ -3025,13 +3163,19 @@ def test_create_payment_information_for_user(): create_payment_information_for_user.sync( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) # OR if you need more info (e.g. status_code) create_payment_information_for_user.sync_detailed( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) @@ -3044,13 +3188,19 @@ async def test_create_payment_information_for_user_async(): await create_payment_information_for_user.asyncio( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) # OR run async with more info await create_payment_information_for_user.asyncio_detailed( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) @@ -3061,13 +3211,19 @@ def test_update_payment_information_for_user(): update_payment_information_for_user.sync( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) # OR if you need more info (e.g. status_code) update_payment_information_for_user.sync_detailed( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) @@ -3080,13 +3236,19 @@ async def test_update_payment_information_for_user_async(): await update_payment_information_for_user.asyncio( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) # OR run async with more info await update_payment_information_for_user.asyncio_detailed( client=client, - body=BillingInfo, + body=BillingInfo( + name="", + phone="", + ), ) diff --git a/pyproject.toml b/pyproject.toml index 811bd524f..3a9145286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" exclude = [] show_error_codes = true ignore_missing_imports = true +check_untyped_defs = true [tool.pytest.ini_options] addopts = "--doctest-modules"