"If [some fact] in the code base needs to change, how many places would we have to change it in?"
If the answer is > 1, you have a very good DRY case. Otherwise, when [some fact] changes, it will probably not be changed in one of the places, and the system will be broken.
This often coincides with having an "elegant codebase", but that's not the most important part.
For a living codebase, nowadays my general rule of thumb is to consciusly duplicate code until it covers 3 different cases, and only then refactor (unless the DRY way is as fast and obvious).
It takes more than that to yield spaghetti and a lot of time is saved on premature generalization. Plus the generalization is often way more straightforward once the explicit cases are already implemented.
I follow this too for "style" refactorings.
I followed every principle of good code, except one, DRY. I tried to make generic parts to connectors, because they do have similarities. But this is a work of at least a year, and the price to make it generic was increasingly more complex configuration files (Just the pagination alone added 3 variables for two different APIs, and the number of app i am supposed to interact with should grow to ~40). I decided after a few days of reflexion that the idea was not that dumb in principle, but unworkable in my case, and decided that one connector for one API, even with a lot of repetition.