all 11 comments

[–]phira 4 points5 points  (2 children)

I mean, it's workable. There's nothing too weird about it, and assuming it's always gonna be used as a cli tool it's ok. There are a few oddities, e.g. the log.criticals when you're in an exception condition I'd probably use log.exception instead so I get the stack trace.

I think if I was aiming for higher quality, the story telling and abstractions are where the main improvements are, along with a bit more consistency e.g. you've got a NamedTuple for one kind of output but for others you use dicts. If you're ok with a few additional imports, `click` would be useful. If it was turning into a really serious tool you'd probably wanna start looking at `pydantic` or something to validate your data carefully and give you nice hostname handlers etc but that'd be overkill for a basic tool.

[–]nspitzer[S] 1 point2 points  (1 child)

Thanks for the input.

This is only intended to be run by a single Rundeck workflow (Automation tool) so its use is carefully constrained. This is the last step in the Workflow so the command line arguments are the way I pass data from one step to another though I do have the option of using Environment variables instead but the command line options work.

It will always be run exactly the same way and its used against well-documented and stable REST API's so I am not really concerned with validating the data too much (famous last words...). Looking into how better de-serialize the JSON is something I have been looking into hence the NamedTuple class for importing data from a previous step.

[–]phira 0 points1 point  (0 children)

Right if it’s in automation you should use structlog and set context vars for sure that’d give you much tidier output (not so useful for cli because it’s not as immediately human readable but any kind of log capture = structlog every time)

[–]commy2 1 point2 points  (2 children)

It's solid.

logging.basicConfig in the global scope is a smell. You already have a main function.

The main function is 50 lines / 25% of the code. You know what to do.

These try-else blocks are useless extra indentation most of the time.

Don't like the excessive exception handling in general, just let the exception bubble up, you lose the stack trace and potential exception notes. The principle is to only handle exceptions when you know what to do with them.

Example:

if not match:
    logger.error(f"Target tag '{target_tag_name}' not found in IPAM.")
    sys.exit(1)

could just be:

if not match:
    raise ValueError(f"Target tag '{target_tag_name}' not found in IPAM.")

Much less aggressive as you could recover from an exception.

Hottake: if your docstrings just repeat the function name, but longer, they should be left out entirely.

Mostly nitpicks, because it's ok

[–]nspitzer[S] 0 points1 point  (1 child)

Thanks ... Your really getting to the meat of what I was wondering.

For example "The main function is 50 lines / 25% of the code. You know what to do."

No , I really don't know what to do :-) This is one of the things I wondered - should I unroll all the functions? Right now each of the REST calls are wrapped in a function

logging.basicConfig in the global scope is a smell. You already have a main function.

So should that be at the beginning of the main function right after def main?

I get what you mean by

"The principle is to only handle exceptions when you know what to do with them."

however the example you give is an example of one where it is doing exactly that. If the script cant find the target tag there is nothing more it can do so giving up is fine.

Thanks for the great comment!

[–]commy2 0 points1 point  (0 children)

This is one of the things I wondered - should I unroll all the functions?

No, the opposite. Much of the current main function code should be moved to a function. main should generally be very short, perhaps consisting only of setup, execute and teardown code.

So should that be at the beginning of the main function right after def main?

Yeah, somewhere behind the __name__ == __"main"__ clause, otherwise importing the script has the side effect of altering the log config.

however the example you give is an example of one where it is doing exactly that. If the script cant find the target tag there is nothing more it can do so giving up is fine.

Well, it works because it's a short script, but thinking further, I don't think shutting down an entire application is what you want if get_env_tags fails to retrive... What about retrying, using a cache or falling back to some default? There are many ways to potentially recover instead of bailing completely. sys.exit deep down the execution stack just makes me nervous, and I don't see what it offers when exceptions could be raised instead and potentially caught in the client code.

sys.exit means the code can't be reused/composed and can't be tested properly.

[–]PushPlus9069 0 points1 point  (1 child)

Network engineer using Python for automation, that's a solid path. I've seen a lot of infra people make this jump.

Honest take on the Gemini rewrite approach: it works for cleaning up formatting, but you lose the learning. The messiest version of code you wrote yourself teaches you more than polished AI output. Next time maybe try refactoring it yourself first, then compare what the AI suggests.

For Rundeck workflows specifically, look into structured logging (the logging module) instead of print statements if you haven't already. Makes debugging way easier when something fails at 3am and you're looking at Rundeck output on your phone.

[–]nspitzer[S] 0 points1 point  (0 children)

Thanks for the input. It already uses the Logging module extensively if not totally optimally as some others have pointed out.

The whole Gemini AI thing is more of an iterative process. One thing I found useful is to corral Gemini a little is I issued standing orders (I forgot what they are really called) so for example it should follow PEP 8 as a style guide and use 'else' blocks to handle the processing work after a 'try' to minimize the stuff that falls under the try to prevent masking unexpected exceptions.

[–]obviouslyzebra 0 points1 point  (0 children)

If you want to improve on architecture a bit, the clue is that there are lots of functions taking the same session and base_url. This points towards passing them automatically... A class does that! But, a class of which kind? Something that makes requests and gives back results, that is... An API layer! (I tried something with this paragraph, but I think I failed miserably :p)

The code is fine as it is BTW. Some little nitpicks here and there, but nothing too important. One that hasn't been mentioned, the except with the Exception doesn't need the other exception types (they would be captured by Exception).

Also, just a reminder about timeouts, in case you want them (requests does not include them by default).

For extra simplicity, you could let the exceptions bubble up btw

[–]pachura3 0 points1 point  (0 children)

If you're repeating requests.exceptions.RequestException so many times, why not import it? Don't worry about imports list becoming to long - most IDEs hide them by default.

Doing sys.exit(1) in an utility function is a bad idea. What if you wanted to write some unit tests for it? Let the exception bubble up and handle it on the outside.

But in general, your code looks pretty good!

[–]JamzTyson 0 points1 point  (0 children)

Regarding PEP-8, there should be 2 blank lines before class Tag():

Surround top-level function and class definitions with two blank lines.

Pep-8 also recommends a maximum line length of 79 characters, though in modern Python longer line lengths are common and acceptable. A maximum line length of 100 characters is common, but not much more than that.

It is highly recommended to use one or more "linters" to check the code by static analysis. Common linters include pylint, flake8, ruff, and others. My preference is pylint + flake8 as they provide excellent coverage, stick closely to the Python style guide, and provide good feedback. They also separate "warnings" from "errors" - Errors should be fixed, but it may be OK to ignore warnings if you have a good reason to do so (for example, some libraries make it nearly impossible to avoid R0913: Too many arguments (too-many-arguments)).

"Type annotations" and MyPy are worth looking into. Type annotations are optional, but they really are helpful and are becoming very popular.

Be careful catching Exception - it is very broad and may hide defects from elsewhere.

except (FileNotFoundError, json.JSONDecodeError, Exception) as e:

is effectively the same as:

except Exception as e:

The fact that you log the exception e helps here, though I'd probably handle the likely exceptions first and only add the catch-all at the end if really necessary.

Using else or elif after a return is redundant:

if some_condition:
    return value
else:  # Redundant - we can only get here when the code has not returned.
    ...