 Hi, my name is Oliver and I'm a software engineer at Bloomberg in New York City And I'm here today to talk to you about a tool my team built and some of the design that went into it Today will mostly be discussing REST APIs, CLIs and API documentation But before we do I want to make you aware of some prerequisites To follow this talk it'll be helpful to have a basic understanding of what a REST API is the example code shown here is Is a basic implementation of a REST API endpoint and fast API But when you see something like this just know I'm talking about an API endpoint in general For example, this endpoint is used to get information about servers It uses a get method and its path is slash device slash compute slash servers It'll also be helpful to know a little bit about CLIs or command line interfaces When you see this classic green text on a black background just know I'm talking about CLI And finally we'll be discussing API documentation. I don't expect you to know the intricacies of the open API spec But when you see something like what's shown here, no, I'm referring to API docs With that other way, let's get started So at Bloomberg, I'm on a team that builds automation to make our infrastructure more reliable scalable and efficient Without getting into too many details a while back. We were tasked with building a new tool Its purpose was to unify the existing tools that our operations teams use And in doing so increase repeatability and efficiency Now for the toolset we knew we wanted one API to host it But we also needed a CLI for individuals to interact with it to achieve this There's a fairly common pattern that you might be familiar with on the left here You have a REST API with various methods paths It speaks JSON and is designed to be interacted with programmatically and on the right You have a CLI made for human interaction and it translates between the REST API Operations and more human-friendly commands syntax and output Usually these applications are tightly coupled and the CLI is purpose written specifically for the latest API spec However from past experience, we knew that this design presented a couple challenges We knew our API would have quite a few operations and might indeed never stop growing depending on the requirements of our infrastructure While building a tightly coupled API and CLI might be easy at first Keeping the two in sync can be cumbersome as the project grows. Now, let's talk about why So a tightly coupled API and CLI is a simple and straightforward approach The API is for programmatic access the CLI is for human interaction and CLI code translates between the two But when an API and CLI are tightly coupled Every time you modify one you likely need to modify the other You also need to continually maintain two separate code bases with different automation deployments and team knowledge Plus API and CLI versions can get out of sync and often require you to synchronize your deployments to avoid compatibility issues It was at this point that we had an idea We were already planning on maintaining good API docs for other API clients using the open API spec So what if the CLI commands were just based on the API documentation? With this model the CLI would generate its commands by simply reading the docs every time it starts But what are the advantages of this approach? So when your CLI is auto-generated new features and fixes only require an API change Because of this the CLI and API can never get out of sync and you avoid a bunch of the issues we just discussed However, it's a bit more complex and it should be noted that with this design You still have two code bases and need to deploy the API and CLI separately But the advantage comes from how this scales over time In the beginning the CLI and API will likely both be getting changed very frequently because they're under active development But when the CLI matures the API can still get continual updates with code fixes New features etc and the CLI should hopefully only require. Thank you minimal maintenance effectively Effectively at a certain point you can stop deploying the CLI and continue changing the API, but the CLI gets updated for free So we put together a first draft with simple one-to-one mappings between the API and CLI API paths would map to space separated CLI commands Each API parameter would map to a single CLI flag and the CLI would just print the JSON API response We weren't the first people to think of something like this. It's been done a couple times in the past You can just Google it or ask your favorite generator the AI to do it for you So let's look at the scheme in a little more detail Here's a sample API endpoint that's used to list all the servers the API knows about It uses a get method its path is slash device slash compute slash servers And it takes an optional cluster underscore name query parameter to filter results by cluster and it responds with JSON containing a list of found servers Now if we were to naively generate a CLI command from this endpoint it would look something like this to use it You need to fully type out get device compute servers Specify your cluster with the dash-dash cluster underscore name flag and you get back the HTTP status code and this beautiful JSON blob in response Unfortunately, if you can't already tell this scheme leads to a bad CLI and it is not much better than just using curl So we realized that simply bolting the API and CLI together with some simple mappings was not going to work Doing so created a really poor CLI user experience The problem is that good APIs and CLIs are designed for very different purposes APIs are made for programmers writing computer code But CLIs are fundamentally a human interface the underlying issue was that we were trying to map API concepts to the CLI domain But rest API paths were never meant to be typed CLI commands API parameter names were never meant to be typed CLI flags and usually API response data is not meant to be consumed raw by humans As we developed our first draft and we kept running into these issues We tried to think of different ways to make the directly or the auto-generated CLI More human-friendly by changing the API. Maybe we have shorter API paths. So the commands are easier to type Maybe API path parameters could always become positional arguments on the CLI We had a couple more ideas like this But ultimately we're looking at it in the wrong way the end result would have been a bad API a bad CLI or both If our CLI was going to auto-generate its commands from the API, we needed a translation layer to bridge the two So then it occurred to us. Maybe we can include this translation layer in our open API docs The docs already contain all the basic information a client needs to use the API So why don't we include a little additional information on how to define the CLI interface the API as well? Let's discuss how to do this So one problem we saw was that typing out long API paths as a CLI command is a lot of work for a human and a very poor CLI experience Ideally long API paths could be abstracted to succinct intuitive CLI commands So why don't we have the docs just to find a CLI command for an API endpoint then you go from this To this Shorter commands mean less typing, but they're also cleaner and easier to understand Another obvious problem we saw was the API output formatting Rest API's often speak JSON, but JSON is not very human friendly. You can see that here Even if we pretty printed this JSON, it's not going to be easy for a human to read So what if the docs included instructions on how to translate the JSON into something easier to read then you can go from this To this Into accomplish this translation, but still keep the CLI completely decoupled from the API We use Jinja templates If you're not familiar with Jinja, it's a relatively simple templating language that is widespread use in the Python community So the docs would contain a server-defined Jinja template for each API endpoint and the CLI simply has to pass the API response through the template to get the human friendly output There's a little more nuance to the output formatting problem though We observed that CLI users often wanted the ability to display the same essential data, but in slightly different ways For example here instead of the table. What if the user wanted the output is CSV? Well, it would be silly to have multiple virtual identical API endpoints whose only difference is how the output is displayed to the CLI user But we could have multiple Jinja templates for a single API endpoint and give the user a way to choose between them We could have one template to display the output of CSV triggered by the dash dash CSV flag Maybe another template just to display network information triggered by the dash dash net flag And of course all these options would be shown to the users in the command help text You also might notice that dash dash JSON flag there, too That gives the CLI user the option to display the raw JSON output from the API and pipe it to a file or JQ or something else Another issue we noticed in volto arguments were passed to the API Originally, we just planned on having one CLI flag per API argument However, take a look at this get server command generated from the API endpoint code shown to the right The dash dash hostname flag isn't really buying us much There's only one required argument to the API endpoint and it's just more for the user to type So why not include the ability to just make arguments positional like this? That's a relatively simple change, but it definitely makes the command more human-friendly Staying on the topic of arguments. We noticed another unfortunate consequence of mapping API arguments directly to the CLI It's often handy and good code hygiene to have descriptive variable names in your code Here on the right We've an API endpoint that collects logs for a given server and saves them to a file system to represent the log file Destination we've a descriptive variable called destination underscore file path Well, this variable name might be helpful to programmers It's an awfully long and cumbersome CLI flag to type not to mention that the use of underscores in CLI flags doesn't follow POSIX conventions So why don't we have the ability to specifically define the CLI flag and short flag used to represent a given API parameter like this To specify destination file path now you can use dash dash desk path or just dash P Again little improvements like this go a long way in improving the CLI users experience The final problem we observed with API and CLI argument mapping was that sometimes arguments just couldn't easily be represented using simple strings What if the CLI command need to upload a local file? What about Boolean CLI flags whose simple presence represents true? What about list arguments with a fixed or dynamic number of entries? We knew we needed to build a dynamic system to represent unusual API arguments on the CLI but in a human-friendly way All right. Now that we have a clear understanding of the problem. It's time to implement some solutions While we use many of the great open-source projects you see here. We're going to primarily focus on fast API If you aren't familiar with fast API, it's a great open-source web framework for Python Well, there are numerous good reasons to use it one feature. We love is the support for automatic open API dock generation Here on the left. We've a simple fast API implementation with two endpoints with just this code fast API Automatically generates the open API docks you see on the right However, what we see on the right is not the actual documentation itself, but just a pretty rendering of it and the actual documentation is defined in JSON and looks like this This JSON is the actual open API spec which contains all the information a client needs to know and as you can see The JSON contains a section for each path on the API. This is what our CLI will be parsing to generate its commands So let's take a closer look at the open API JSON Here on the right, you can see the open API JSON generated for the get hello endpoint on the left It contains a summary a description of parameters and a little bit more information than I left out for simplicity For our use case we need to define extra information for each API endpoint so the CLI can configure itself So we just added an extra section like this Each endpoint gets a CLI section that can define things like the actual CLI command the ginger formatters and more Fortunately for us fast API is a neat keyword ARG called open API extra That allows the users to embed arbitrary data in the open API spec for an individual endpoint and it's used like this You just include your arbitrary object in the endpoint decorator and it's automatically nested in the generated open API spec Now we could just shoehorn a huge dictionary into every endpoint decorator to define the entire CLI spec But that would be crude and not very readable So instead we decided to implement our own child class of the fast API API router But we added some additional arguments to include the CLI specification as well Something like what's shown here and with that we can now define our API endpoints and associated associated CLI commands like this We write our endpoints just like we normally would in fast API with the option of specifying a CLI command in just a few extra lines Of course, you still need to define the specific ginger templates But we often found that they could be reused between different endpoints and they could live somewhere else Well, this is all you need to describe the CLI command for an endpoint. We still haven't covered how I'll handle CLI arguments Fast API already offers a rich set of tools to define API argument behavior Here we have the query class to define a query parameter and the field class to define individual fields and a JSON request body with pedantic There are similar classes to define other types of API arguments as well and the information defined in these classes is also reflected in the open API JSON Like the fast API endpoint decorators, it's also easy to tack on extra information So just like the endpoint CLI specification We use pedantic to define our model Added to the argument declaration and it automatically shows up in the open API JSON where it can be consumed by the CLI All right, so now that we have all the pieces to implement a CLI command for the API endpoint Let's put everything together Here on the left we have an endpoint from earlier that's used to get a list of servers and on the right We have a CLI command that we want to be generated for this endpoint to implement the CLI command. All you have to add is this That's it. Just a small amount of information in the CLI command is generated by fast API open API in our dynamic CLI Now that you have a good understanding of the design and implementation, let's see it in action I don't want to bore you with more monospace text on a black background. We've all seen enough During the conference, but I think a live demonstration is a great way to showcase the utility of what we built All right, bear with me for a moment where well, I reconfigured my screen And I hope that the live demo gods are just All right, I'm gonna run a few things here these top two windows don't worry if you can't read them It's not important. I'm just gonna say what's happening there so in the upper left here, I'm gonna run something called Infra API infra API is just a demo API I wrote for interacting with fake infrastructure It's written in fast API. It's got all those extra annotations We talked about to implement a CLI interface for it and it's running locally on my machine on port 8001 But instead pretend a it's running in some server and a data center in Virginia In this window and it is this readable for everyone Okay, in this window, we're gonna run infra CLI Infra CLI is the CLI that interacts with infra API and it's auto-generated from Infra API running above so it's got a couple sub commands here get server delete server new server and list servers if You look closely when I run infra CLI Up here, you can see a call come in to infra API That's the CLI on start grabbing the open API JSON parsing it and generating its commands So let's see what infra CLI can do So we've got the list servers command let me show you the help text It's used to display a list of known servers in the system It's got a dash-dash cluster flag to only show servers in this cluster And it's got a couple formatters a JSON formatter Just play the raw JSON a CSV formatter to display the output in CSV formatter in CSV format and a net dash-dash net flag to display server network information So if we run it like we did before we can see our server output in a nice column separated table If we specify the dash-dash cluster flag We can see just the servers in that cluster if we specify the CSV flag We see that same output but in CSV format So We've got our list server command and we got some feedback from our users. They say First of all list servers is too much to type instead. We just want to type LS servers Okay, and they also say you have some horrible typos in your description and you should fix those Okay, so let's see what it takes to fix those So I'm going to open up the source code for info API here. It's not important that you read all of it I'm just going to point out a couple of key things So here on line 72. Yeah, you can read that Here on line 72 is where the decorator is to describe the to define the API endpoint behind the LS servers command You can see the path here slash device slash compute slash servers Then on line 76. We have the CLI spec just like what we talked about and Within the CLI spec is how we define the behavior of our CLI. So on line 77 We've got the help text with our typos in it. So let's fix those All right, and then on line 78 We have our command list servers and they said they wanted it to be LS servers save Exit out of that if you were looking closely above the API restarted That's because I have it configured to automatically restart when its source code changes But picture instead you cut a branch you made a PR you merged it you deployed it It went through CI and you deployed it to some server somewhere So we're going to run in for CLI again now LS list servers is LS servers and if you look at the LS servers help text No more typos So we get a little bit more feedback from our users they say this csv flag is great But what we really want is a tsv flag or tabs separated values Okay, let's see what it takes to implement that Go back to our source code. We're going to open a file called format.py. This is where we just Define all our formatters. So on line 121. We have our csv formatter TSV is a lot like csv. So let's just copy that We're going to change it. We'll change all our commas to tab We're going to change our flag from dash dash csv to dash dash tsv Why don't we add a short flag? That's just a dash t and then of course our description should be display servers in tsv format There's two more quick things we got to do in our formatter section. We have to add the tsv formatter for the API endpoint and Then we have to import it Save we saw our API restarted. So that means it worked successfully and Let's see the command now LS servers now has a dash dash tsv flag And if we specify it we can see that same output but in tsv format To recap what we just did we changed three different types of CLI behavior just from the API code The CLI code was completely static the whole time All we did was change the API code and the automated automatically generated docks that it creates and the CLI pulled those in I want to show you one more quick thing So in this window up here, we're going to run something called meal API. It's a mock API built to interact with meal ideas It's written in fast API. It's got the same annotations to implement a CLI interface for it And it's running locally on port 8002 So let's see meal CLI All right, it's got a couple subcommands We'll just run meal CLI meals and it prints out a list of meals a lot like in for CLI I Don't want to show you anything more about CLI meal CLI, but I want to show you what's behind it In for CLI and meal CLI are the same thing They both are aliased to a program called auto CLI where we pass them the API URL So instead of running in for API. I could just run Auto CLI API URL local host 8001 and that's in for CLI We change the port by one number change the URL That's meal CLI You can have one physical CLI code base that can implement any number of virtual CLIs depending on what API you pointed out and when you want to update your API Update your CLI you just change your API and the CLI gets updated for free. That's all I got Thank you Oliver for quite an informative session So if we have some questions, there are two microphones on that side of the room If you can please just walk by there that would be nice And also for others the lunch might have been served in the lobby. So that's another interesting thing Hey Thank you for your talk. Can you hear me a little bit? I'll guess I'll just that's better. Yeah I'm wondering so so API calls are typically very atomic and On the CLI side, I would like to just kind of chain them together. Is it possible to kind of add? Just like chains of commands on the CLI without changing the API itself So add multiple API endpoints behind a single CLI call. Yes, so for example add a server and list them so Sort of we've talked about that and we haven't added it to ours yet Basically, what we talked about is having so called like a virtual CLI command where I could define a CLI command in the API Code and maybe we'd have a ginger template that says call edit server Then call list server and so the CLI reads that and it reads the virtual endpoint and then it knows Okay, I can call edit and then I can call CLI List servers so that we can still keep everything defined in the API code But the CLI behavior can change, but we haven't implemented that yet. Okay. Thanks. Yeah. Hi. Thank you very cool stuff Can you give us some details on how the code generation is working on the CLI side? It's just pulling down the open API JSON and then basically I'm running it I'm using arg parse. I probably should use some cooler new CLI thing I saw a really great talk about typer yesterday And maybe one day we'll use that but for now we're just using our parse sub commands And then there's some simple translation in there where we read the specific specification like I'm for one of the arguments If it's got a flag name that you want to use we have to use that for our parse pass them to our parse Then map that back to the API argument All right, cool. Thank you. Sure Thanks for the talk. It's very interesting. Thanks. I just had a quick question So you were making command shorter and the argument shorter and that that was very nice, but you are always Typing infra CLI at the very beginning Mm-hmm, and I was wondering is there way to not type that where you could you like enters into sub CLI system and just A lot services enough to do it like sort of so I think what you're talking about is sort of like an interactive mode Like you drop into a sub shell. Yes, I Didn't do that in this program in our internal one that isn't public We have something like that where there's a sub shell and it wouldn't be very hard to add In this in fact in the slide where I showed the The open-source projects that we use one in there is called prompt toolkit Which is a really nice open-source library for doing just that and creating like dynamic Sort of captive CLIs and so it's very possible. But in the program I wrote for this What's this called again from the program? Yeah, the open source one. Oh, it's not open source. Sorry. Oh, sorry But it's called auto CLI, but yeah, okay So if this CLI like dynamically fetches the open API it's that up How do you deal with breaking changes like if somebody's build a script on the CLI and you do a backwards incompatible change on the CLI spec is there any versioning or pinning or maybe like embedding a particular you can imagine this CLI can change Independently from the endpoints right and all this CLI should still work I'm not sure I understand your question because I think what we're trying to do is basically Avoid that all together like there is no CLI sort of it's only what's defined in the API now if you mean like someone wrote a script Yeah, exactly. Okay, that would be up to the API writers to make sure they don't like change the command from list servers To LS servers or something like that But beyond that there's no pinning on it because that old version of the API doesn't exist anymore It's whatever the latest docs of the API are sure but in your example where you change the list servers to LS Servers the endpoint didn't change right the endpoint remained the same you only change the CLI So I see what you're saying So if you like if you did literally that change and I can imagine somebody having a script with list servers And it should just still run because your API didn't change. That's a great idea actually we could We probably wouldn't make a change like that once we release something publicly But if we were it wouldn't be hard to add like a set of aliases so that the CLI will recognize any one of those I'm in still work, but great idea. Cool. Thanks So thank you for the presentation. So we have the infra API infra CLI and When we will have infra chat so you can chat with your API Maybe Europe I found next year I gave this talk somewhere else and someone asked about like shoving the open API JSON into something like a generative AI and saying hey generate a CLI for that That would be pretty cool. I don't think Bloomberg would let me do that right now But yeah, it'd be pretty cool Yeah, so I was just wondering Is it possible to include also completion there for the arguments? Oh Yeah, it could be I didn't I Didn't include it in ours in the demo program I wrote for this but Like in the the talk from typer yesterday They had some really neat stuff to just automatically install the completions for your CLI That would be super neat to do for something like this But I didn't do it for my program Hi, thanks for the presentation If I'm not mistaken every time that you fire a CLI command. There is a call first to fetch the API documentation and then the actual call Could this be become like a performance issue if the API Becomes too large and have you maybe considered a different approach that you only have to periodically Fetch the specs like Maybe run a command once there is an update on the specs and then it's somehow cast or in Different mechanism. So for the first part, I mean, I don't think it would be a performance issue fast API As far as I can tell generates the docs once and then it's cached in memory And so it's just serving up a static thing of JSON and I assume it would be lower impact than whatever API call you're doing But in terms of caching the version caching the docs locally on the CLI we've discussed that And that would be something easy to do but then we have to deal with versioning So we talked about like maybe we pull down the open API JSON once and we save it locally But then every time the CLI calls the API it includes the API version in a header So the API could see the CLI version and say oh you're behind Update your docs before you make this call because we need some way to automatically update the CLI when the API gets updated So there are a couple caching schemes in there and if it ever became a problem, we might do something like that Thanks. I was also referring to performance issues regarding the CLI not not fixing the documents But actually parsing the documents every time and Oh Registering the whole functionality. I think you do this on every call, right? Yeah, I guess if the JSON got Suitably large it could I haven't looked at it But I profiled this once and like most of the startup time was importing a ton of modules on the CLI Well, the parsing the JSON was just like instantaneous If anything I feel like Performance on the CLI could be improved by doing something like what the other guy said of having a captive one So you start it up and then you don't have to import all your modules right at start But it's something we would consider doing like saving it in a more like I Don't know computer-friendly format and caching it locally if it ever became a problem Great. Thanks. Yeah So I think that will be the end of session. Thank you everyone for joining here and thank you Oliver