{"id":865,"date":"2023-10-10T13:33:21","date_gmt":"2023-10-10T13:33:21","guid":{"rendered":"https:\/\/tbekk.com\/devstream\/?p=865"},"modified":"2023-10-10T13:33:21","modified_gmt":"2023-10-10T13:33:21","slug":"setup-gitlab-ci-cd-for-machine-learning-project","status":"publish","type":"post","link":"https:\/\/tbekk.com\/devstream\/2023\/10\/10\/setup-gitlab-ci-cd-for-machine-learning-project\/","title":{"rendered":"Setup Gitlab CI\/CD for Machine Learning Project"},"content":{"rendered":"\n<hr class=\"wp-block-separator has-text-color has-light-gray-color has-alpha-channel-opacity has-light-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<ul class=\"wp-block-list\">\n<li><em><strong>Link:<\/strong><\/em> <a href=\"https:\/\/medium.com\/data-folks-indonesia\/setup-gitlab-ci-cd-for-machine-learning-project-6a1e1193f4a\"><em>Gitlab CI\/CD for ML<\/em><\/a><\/li>\n\n\n\n<li><em><strong>Author:<\/strong><\/em> <a href=\"https:\/\/medium.com\/@andreaschandra?source=post_page-----6a1e1193f4a--------------------------------\"><em>AC<\/em><\/a><\/li>\n\n\n\n<li><em><strong>Publication date:<\/strong> June 24, 2023<\/em><\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-text-color has-light-gray-color has-alpha-channel-opacity has-light-gray-background-color has-background is-style-wide\"\/>\n\n\n\n<p id=\"ae20\">This article demonstrates a basic configuration for CI\/CD on Gitlab. Continous Integration and Continous Development are terms that describe an end-to-end process from several changes in development to production environment. CI\/CD automates all the work for code integration such as integration test, unit test, and regression test, as well as deployment process with a set of predefined criteria. Hence, CI\/CD reduce manual effort in order to maintain the quality of the software.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/0*n58yWtZBBSyMKHXA\" alt=\"\"\/><figcaption class=\"wp-element-caption\">Photo by&nbsp;<a href=\"https:\/\/unsplash.com\/@quinten149?utm_source=medium&amp;utm_medium=referral\" rel=\"noreferrer noopener\" target=\"_blank\">Quinten de Graaf<\/a>&nbsp;on&nbsp;<a href=\"https:\/\/unsplash.com\/?utm_source=medium&amp;utm_medium=referral\" rel=\"noreferrer noopener\" target=\"_blank\">Unsplash<\/a><\/figcaption><\/figure>\n\n\n\n<p id=\"f64b\">This article focuses on inference part and you may need to add more layers for model development part. But, it almost the same process.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"46ac\"><strong>Prerequisites<\/strong><\/h1>\n\n\n\n<p id=\"d5ab\">Please read these pages to grasp the concept of CI\/CD<\/p>\n\n\n\n<p><a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\"><\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\">GitLab CI\/CD | GitLab<\/a><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\">Learn how to use GitLab CI\/CD, the GitLab built-in Continuous Integration, Continuous Deployment, and Continuous\u2026<\/a><\/h3>\n\n\n\n<p><a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\">docs.gitlab.com<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/cloud.google.com\/architecture\/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\"><\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><a href=\"https:\/\/cloud.google.com\/architecture\/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\">MLOps: Continuous delivery and automation pipelines in machine learning | Cloud Architecture Center\u2026<\/a><\/h2>\n\n\n\n<h3 class=\"wp-block-heading\"><a href=\"https:\/\/cloud.google.com\/architecture\/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\">Last reviewed 2023-05-18 UTC This document discusses techniques for implementing and automating continuous integration\u2026<\/a><\/h3>\n\n\n\n<p><a href=\"https:\/\/cloud.google.com\/architecture\/mlops-continuous-delivery-and-automation-pipelines-in-machine-learning?source=post_page-----6a1e1193f4a--------------------------------\" rel=\"noreferrer noopener\" target=\"_blank\">cloud.google.com<\/a><\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*uHkLtCb6TrBt-69vM_RdYw.jpeg\" alt=\"\"\/><figcaption class=\"wp-element-caption\">andd yeap, this is me writing technical tutorial while thinking about going to the beach<\/figcaption><\/figure>\n\n\n\n<p id=\"65c7\">Let\u2019s continue:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Create Gitlab account at gitlab.com<\/li>\n\n\n\n<li>Create&nbsp;<a href=\"http:\/\/fly.io\/\" rel=\"noreferrer noopener\" target=\"_blank\">fly.io<\/a>&nbsp;account for web api deployment<\/li>\n\n\n\n<li>Create repository on gitlab.com<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*ebAY5bd4DfatUhSP50IC0A.png\" alt=\"\"\/><\/figure>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Clone Repository<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*y2mmbifjODtdGbXXaRgQuQ.png\" alt=\"\"\/><\/figure>\n\n\n\n<p id=\"0a07\">run&nbsp;<code>git clone git@gitlab.com:&lt;your-username&gt;\/iris-api.git<\/code><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Build a simple RESTFUL API<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:670\/1*WSMKEb1eZSCZr4qqUYDMFA.png\" alt=\"\"\/><figcaption class=\"wp-element-caption\">This is the file tree for this project<\/figcaption><\/figure>\n\n\n\n<p id=\"abf5\">Create&nbsp;<code>src\/main.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\"\"\"Iris Web API Service.\"\"\"<br>from fastapi import FastAPI<br>from pydantic import BaseModel<br>import numpy as np<br>from src import distance, iris<br><br>dataset = iris.get_iris_data()<br><br>app = FastAPI()<br><br><br>class Item(BaseModel):<br>    \"\"\"Input class for predict endpoint.<br><br>    Args:<br>        BaseModel (BaseModle): Inherited from pydantic<br>    \"\"\"<br><br>    sepal_length: float<br>    sepal_width: float<br>    petal_length: float<br>    petal_width: float<br><br><br>@app.get(\"\/\")<br>def homepage():<br>    \"\"\"Homepage for the web.<br><br>    Returns:<br>        str: Homepage<br>    \"\"\"<br>    return \"Homepage Iris Flower - tags 0.0.2\"<br><br><br>@app.post(\"\/predict\/\")<br>async def predict(item: Item):<br>    \"\"\"Predict function for inference.<br><br>    Args:<br>        item (Item): dictionary of sepal dan petal data<br><br>    Returns:<br>        str: predict the target<br>    \"\"\"<br>    sepal_length = item.sepal_length<br>    sepal_width = item.sepal_width<br>    petal_length = item.petal_length<br>    petal_width = item.petal_width<br><br>    data_input = np.array([[sepal_length, sepal_width, petal_length, petal_width]])<br><br>    result = distance.calculate_manhattan(dataset, data_input)<br>    return result<\/pre>\n\n\n\n<p id=\"5847\">Create&nbsp;<code>src\/iris.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\"\"\"Load iris dataset from scikit-learn.\"\"\"<br>from sklearn import datasets<br><br><br>def get_iris_data():<br>    \"\"\"Load iris dataset.<br><br>    Returns:<br>        set: consists of X, y, feature names, and target_names<br>    \"\"\"<br>    iris = datasets.load_iris()<br>    x_data = iris.data<br>    y_label = iris.target<br>    features_names = [\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"]<br>    target_names = iris.target_names<br><br>    return x_data, y_label, features_names, target_names<br><br><br>if __name__ == \"__main__\":<br>    x_data, y_label, features, target_names = get_iris_data()<br><br>    print(\"X\", x_data)<br>    print(\"y\", y_label)<br>    print(\"features\", features)<br>    print(\"target_names\", target_names)<\/pre>\n\n\n\n<p id=\"2716\">Create&nbsp;<code>src\/distance.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\"\"\"Distance module for calculating distance between data input and dataset.\"\"\"<br>import numpy as np<br><br><br>def calculate_manhattan(iris_data: np.ndarray, input_data: np.ndarray):<br>    \"\"\"Calculate the distance between 2 vectors using manhattan distance.<br><br>    Args:<br>        dataset (np.ndarray): Iris dataset<br>        input_data (np.ndarray): 1x4 matrix data input<br><br>    Returns:<br>        string: Return prediction<br>    \"\"\"<br>    x_data, y_label, _, target_names = iris_data<br><br>    distance = np.sqrt(np.sum(np.abs(x_data - input_data), axis=1))<br>    distance_index = np.argsort(distance)<br>    y_pred = target_names[y_label[distance_index[0]]]<br><br>    return y_pred<br><br><br>if __name__ == \"__main__\":<br>    dataset = [<br>        np.array([[4.9, 3.0, 1.4, 0.2], [4.9, 3.0, 1.4, 0.9]]),<br>        [0, 0],<br>        [\"sepal_length\", \"sepal_width\", \"petal_length\", \"petal_width\"],<br>        [\"setosa\", \"versicolor\", \"virginica\"],<br>    ]<br>    sample_data = np.array([[4.9, 3.0, 1.4, 0.2]])<br>    print(calculate_manhattan(dataset, sample_data))<\/pre>\n\n\n\n<p id=\"0899\">Create&nbsp;<code>test\/test_distance.py<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">import numpy as np<br>from src.iris import get_iris_data<br>from src.distance import calculate_manhattan <br><br>def test_calculate_manhattan():<br>    dataset = get_iris_data()<br>    input_data = np.array([[4.9, 3.0, 1.4, 0.2]])<br>    result = calculate_manhattan(dataset, input_data)<br>    assert result == 'setosa'<\/pre>\n\n\n\n<p id=\"d408\">Create Dockerfile<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">FROM python:3.10<br><br>EXPOSE 8000<br><br>WORKDIR \/app<br><br>COPY . .<br><br>RUN pip install -r requirements.txt<br><br>ENTRYPOINT [\"uvicorn\", \"src.main:app\", \"--host\", \"0.0.0.0\", \"--port\",  \"8000\"]<\/pre>\n\n\n\n<p id=\"2495\">Make&nbsp;<code>requirements.txt<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\"># python<br>pydoclint&gt;=0.0.10<br>pylint&gt;=2.17.0<br>black&gt;=22.6.0<br>pydocstyle&gt;=6.1.1<br>pytest&gt;=7.1.2<br><br># web app<br>fastapi&gt;=0.98.0<br>uvicorn&gt;=0.22.0<br><br># models<br>numpy&gt;=1.21.6<br>scikit-learn&gt;=1.2.2<\/pre>\n\n\n\n<p id=\"f459\">Create&nbsp;<code>fly.toml<\/code><\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">app = \"iris-api-demo-stg\"<br>primary_region = \"sin\"<br><br>[build]<br>  dockerfile = \"Dockerfile\"<br><br>[http_service]<br>  internal_port = 8000<br>  force_https = true<br>  auto_stop_machines = true<br>  auto_start_machines = true<br>  min_machines_running = 0<\/pre>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"db73\">Setup Web App on Fly.io<\/h1>\n\n\n\n<p id=\"de47\">First thing first, please install flyctl on your computer by following this link&nbsp;<a href=\"https:\/\/fly.io\/docs\/hands-on\/install-flyctl\/\" rel=\"noreferrer noopener\" target=\"_blank\">https:\/\/fly.io\/docs\/hands-on\/install-flyctl\/<\/a><\/p>\n\n\n\n<p id=\"054b\">Then, do the authentication&nbsp;<code>flyctl auth login<\/code><\/p>\n\n\n\n<p id=\"3ec6\">Then create your personal access token for Gitlab here&nbsp;<a href=\"https:\/\/fly.io\/user\/personal_access_tokens\" rel=\"noreferrer noopener\" target=\"_blank\">https:\/\/fly.io\/user\/personal_access_tokens<\/a>&nbsp;save the token to your notepad, later we will add the token to gitlab environment.<\/p>\n\n\n\n<p id=\"f87f\">Now, you need to create 2 app: staging and production.<\/p>\n\n\n\n<p id=\"aece\">Staging app<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">flyctl launch --auto-confirm --copy-config --dockerfile Dockerfile --name iris-api-demo-stg --now --org personal --region sin<\/pre>\n\n\n\n<p id=\"7141\">Production app<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">flyctl launch --auto-confirm --copy-config --dockerfile Dockerfile --name iris-api-demo--now --org personal --region sin<\/pre>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*VVUOKYTlRYrDAYVE9HxQjw.png\" alt=\"\"\/><figcaption class=\"wp-element-caption\">Eventually, you will look something like this in your fly.io dashboard<\/figcaption><\/figure>\n\n\n\n<p id=\"8ed6\">Don\u2019t forget to add access token fly.io to gitlab environment for deployment purposes. Add variable and named it as FLY_TOKEN.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*u5haJQVk3uCf9D9k13pdPg.png\" alt=\"\"\/><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"32b7\">Setup CI\/CD<\/h1>\n\n\n\n<p id=\"350a\">*drum-roll*<\/p>\n\n\n\n<p id=\"2264\">Now, let\u2019s focus the main content here, configurating the CI\/CD pipline.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Let\u2019s create a new file named<code>gitlab-ci.yml<\/code>&nbsp;and call this v1<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-preformatted\">image: python:latest<br><br>docker-build:<br>  stage: build<br>  script:<br>  - echo \"Build Docker\"<br><br>code-test:<br>  stage: test<br>  script:<br>  - echo \"Run Code Test\"<br><br>production:<br>  stage: deploy<br>  environment: production<br>  script:<br>  - echo \"Deploy to fly.io\"<\/pre>\n\n\n\n<p id=\"cd40\">This is a simple gitlab-ci that runs every single push that you make to remote repo. What it does is when you push a change, 3 jobs will be triggered. docker-build, code-test, and production.<\/p>\n\n\n\n<p id=\"0c91\">Let\u2019s dive in on how the things work.<\/p>\n\n\n\n<p id=\"1bb7\"><code>image: python:latest<\/code>&nbsp;means that all these jobs run on top of docker image of python latest version which you can find on&nbsp;<a href=\"https:\/\/hub.docker.com\/_\/python\/tags\" rel=\"noreferrer noopener\" target=\"_blank\">docker hub<\/a>.<\/p>\n\n\n\n<p id=\"42c0\"><code>docker-build<\/code>&nbsp;is the name of the job. The name of the job can be anything and you can create numerous jobs in a single .yml file.<\/p>\n\n\n\n<p id=\"d94f\"><code>stage<\/code>&nbsp;means which stage this job falls into. There are 3 common stages in the CI\/CD pipeline, build, test and deploy.<\/p>\n\n\n\n<p id=\"d6ef\"><code>environment<\/code>&nbsp;is used for specify which environment this job will run. You will get a list of jobs that has specific environments. This allow you to deploy which commit you want to redeploy. Hence, this makes easier if something went south in the staging or production environment.<\/p>\n\n\n\n<p id=\"9b18\"><code>script<\/code>&nbsp;allows you to write a shell command in the container. Think like a set of script will run in the terminal.<\/p>\n\n\n\n<p id=\"e8c7\">Once you done:<\/p>\n\n\n\n<p id=\"c081\"><code>git add gitlab-ci.yml<\/code><\/p>\n\n\n\n<p id=\"fbfd\"><code>git commit -m \"add gitlab-ci.yml<\/code><\/p>\n\n\n\n<p id=\"dadc\"><code>git push<\/code><\/p>\n\n\n\n<p id=\"489d\">Then, you can navigate to pipeline tab<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*kTn70Z_nK0ZahtCoTw6wfQ.png\" alt=\"\"\/><\/figure>\n\n\n\n<p id=\"6d29\">As you can see, there are 3 green check mark that shows successful jobs had been run. If it fails, the icon will be red cross.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*jbLQBOiIcn0hYJinQEIniA.png\" alt=\"\"\/><figcaption class=\"wp-element-caption\">Pipeline detailed page<\/figcaption><\/figure>\n\n\n\n<p id=\"98ed\">Now, you have created a simple pipeline.<\/p>\n\n\n\n<p id=\"22bb\">Let\u2019s create a pipeline that usually used for ML API development.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">image: python:latest<br><br>code-check:<br>  stage: build<br>  only:<br>    - merge_requests<br>  script:<br>  - echo \"Build Docker\"<br>  - pip install -r requirements.txt<br>  - pylint src --rcfile=.pylintrc<br>  - black src --check<br>  - pydocstyle src<br><br>code-test:<br>  stage: test<br>  only:<br>    - merge_requests<br>  script:<br>  - echo \"Run Code Test\"<br>  - pip install -r requirements.txt<br>  - pytest<br><br>staging:<br>  stage: deploy<br>  environment: staging<br>  only:<br>    - staging<br>  script:<br>  - echo \"Deploy to fly.io in staging environment\"<br>  - curl -L https:\/\/fly.io\/install.sh | sh<br>  - bash<br>  - \/root\/.fly\/bin\/flyctl deploy --app iris-api-demo-stg --access-token $FLY_TOKEN<br><br>production:<br>  stage: deploy<br>  environment: production<br>  only:<br>    - tags<br>  script:<br>  - echo \"Deploy to fly.io in production environment\"<br>  - curl -L https:\/\/fly.io\/install.sh | sh<br>  - \/root\/.fly\/bin\/flyctl deploy --app iris-api-demo --access-token $FLY_TOKEN<\/pre>\n\n\n\n<p id=\"b6b7\">We have 4 jobs:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>code-check<\/code>&nbsp;this job runs code quality check such as linting using pylint, formatter using black, and docstring using pydocstyle. This is used to make sure that the written code follow the guidelines. This job will only run on merge request. If you just push to the remote branch, it won\u2019t trigger this job.<\/li>\n\n\n\n<li><code>code-test<\/code>&nbsp;Then, we have code test, we have already created a simple unit test above in the&nbsp;<code>test_main.py<\/code>This is to ensure that the module that we created run as expected.<\/li>\n\n\n\n<li><code>staging<\/code>&nbsp;this job will run if the merge request has been approved into staging branch. This will be automatically deployed to fly.io using stagging application. This allows you to do user acceptance test.<\/li>\n\n\n\n<li><code>production<\/code>&nbsp;Finally, we have production job. The purpose is quite similar with staging one. This job will be triggered if you create a tag in the repository.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1400\/1*1eEOPOi3ZvRLk3wMTJ0cBw.png\" alt=\"\"\/><figcaption class=\"wp-element-caption\">Create tag for deploying into production web app<\/figcaption><\/figure>\n\n\n\n<p id=\"7d6b\">Once you create merge request and merge into staging branch. it will deploy to staging app. If it is as expected, you can proceed to merge request to main branch, then approve. Once done, you can create tag to deploy into production web app.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\" id=\"be2a\">Conclusion<\/h1>\n\n\n\n<p id=\"0b42\">That\u2019s more or less to setup CI\/CD on Gitlab. This may seems simplified, I will create more and more complex pipeline that involves MLOps such as model tracking, data versioning, model registry, model monitoring, etc. Hit the follow button and please connect on Linkedin at&nbsp;<a href=\"https:\/\/www.linkedin.com\/in\/chandraandreas\/\" rel=\"noreferrer noopener\" target=\"_blank\">https:\/\/www.linkedin.com\/in\/chandraandreas\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This article demonstrates a basic configuration for CI\/CD on Gitlab. Continous Integration and Continous Development are terms that describe an end-to-end process from several changes in development to production environment&#8230;. <a class=\"read-more-link\" href=\"https:\/\/tbekk.com\/devstream\/2023\/10\/10\/setup-gitlab-ci-cd-for-machine-learning-project\/\">Read more &raquo;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[51,19,158],"tags":[262,28,317],"class_list":["post-865","post","type-post","status-publish","format-standard","hentry","category-article","category-ml","category-mlops","tag-cd","tag-ci","tag-ci-cd-2"],"_links":{"self":[{"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/posts\/865","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/comments?post=865"}],"version-history":[{"count":1,"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/posts\/865\/revisions"}],"predecessor-version":[{"id":866,"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/posts\/865\/revisions\/866"}],"wp:attachment":[{"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/media?parent=865"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/categories?post=865"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tbekk.com\/devstream\/wp-json\/wp\/v2\/tags?post=865"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}