From 6c24e249f2132b5919ba6482a4d4530e051c61b4 Mon Sep 17 00:00:00 2001 From: Vanessasaurus <814322+vsoch@users.noreply.github.com> Date: Mon, 7 Mar 2022 20:15:28 -0800 Subject: [PATCH] Upload new version (#6) * add the ability to make a new version on an existing doi * add readme updates for specifying a doi * add doi to the the action interface * add debug log * use conceptdoi * one last readme update * more docs about versions * add alt text for png * refactoring deploy.sh to be more organized, also tested to deploy different version * zenodo json not hard requirement Signed-off-by: vsoch Co-authored-by: Jeff Ohrstrom Co-authored-by: vsoch --- .github/workflows/release.yaml | 2 +- README.md | 88 +++++++++- action.yml | 15 +- img/zenodo_versions.png | Bin 0 -> 18830 bytes scripts/deploy.py | 285 +++++++++++++++++++++++++-------- 5 files changed, 317 insertions(+), 73 deletions(-) create mode 100644 img/zenodo_versions.png diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f3e7dff..84f6233 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -28,6 +28,7 @@ jobs: zenodo_json: .zenodo.json archive: ${{ env.archive }} token: ${{ secrets.ZENODO_TOKEN }} + doi: 10.5281/zenodo.6326822 - name: View Outputs env: @@ -50,4 +51,3 @@ jobs: echo "latest html ${latest_html}" echo "record ${record}" echo "record html ${record_html}" - diff --git a/README.md b/README.md index 59ea590..8862dc4 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,24 @@ I think this is kind of silly, but that's just me. ## Usage +When looking at artifacts in Zenodo you'll see a versions card like the image below. This artifact has +only one version, 0.0.15. By default, this is the behavior of this action - to create brand new artifacts +with only one version. + +If, however, you'd like to make new versions you can specify the doi that represents *all* +versions. In this image you would specify `10.5281/zenodo.6326822`. This action will then +create new versions tied to this DOI. + +![Zenodo card for versions. '0.0.15' is the only version and a DOI of 10.5281/zenodo.6326823. The footer of the card has a site all versions with DOI 10.5281/zenodo.6326822](img/zenodo_versions.png) + ### GitHub Action -After you complete the steps above to create the metadata file, you might create a release -action as follows: +After you complete the steps above to create the metadata file, you have two options. + +#### Existing DOI + +If you have an existing DOI that is of the **all versions** type meaning we can update it, you should provide it to the action. +The example below shows running a release workflow and providing an archive to update to a new version (**released under the same DOI**) ```yaml name: Zenodo Release @@ -68,13 +82,73 @@ jobs: with: token: ${{ secrets.ZENODO_TOKEN }} version: ${{ github.event.release.tag_name }} - zenodo_json: .zenodo.json + zenodo_json: .zenodo.json # optional archive: ${{ env.archive }} + + # Optional DOI for all versions. Leaving this blank (the default) will create + # a new DOI on every release. Use a DOI that represents all versions will + # create a new version for this existing DOI. + # + # Newer versions have their own DOIs, but they're also linked to this DOI + # as a different version. When using this, use the DOI for all versions. + doi: '10.5281/zenodo.6326822' ``` -Notice how we are choosing to use the .tar.gz (you could use the zip too at `${{ github.event.release.zipball_url }}`) -and using the default zenodo.json that is obtained from the checked out repository. +Notice how we are choosing to use the .tar.gz (you could use the zip too at `${{ github.event.release.zipball_url }}`). +Note that the "zenodo.json" is optional only if you've already created the record with some metadata. If you provide it, +it will be used to update metadata found with the previous upload. If you don't provide it, the previous upload will +only be updated for the date and version. Note that we are considering adding an ability to upload from new authors found +in the commit history, but this is not implemented yet. + +#### New DOI + +If you want to be creating fresh DOIs and releases (with no shared DOI for all versions) for each one, just remove the doi variable. Note +that for this case, the .zenodo.json is required as there isn't a previous record to get it from. + +```yaml +name: Zenodo Release + +on: + release: + types: [published] + +jobs: + deploy: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v3 + - name: download archive to runner + env: + tarball: ${{ github.event.release.tarball_url }} + run: | + name=$(basename ${tarball}) + curl -L $tarball > $name + echo "archive=${name}" >> $GITHUB_ENV + + - name: Run Zenodo Deploy + uses: rseng/zenodo-release@main + with: + token: ${{ secrets.ZENODO_TOKEN }} + version: ${{ github.event.release.tag_name }} + zenodo_json: .zenodo.json # required + archive: ${{ env.archive }} +``` + +#### Archives + +For both of the above, instead of an exact archive path you can also use a pattern to give to Python's `glob.glob`. E.g.,: + +```yaml + with: + archive: "files/*.tar.gz" +``` + +Note that we will be testing support for more than one path or pattern soon. We also grab the version as the release tag. We are also running on the publication of a release. + +#### Outputs + If you want to see or do something with the outputs, add an `id` to the deploy step and do: ```yaml @@ -119,7 +193,7 @@ the following: ```bash export ZENODO_TOKEN=xxxxxxxxxxxxxxxxxxxx - # archive # identifier # version -$ python scripts/deploy.py upload 0.0.0.tar.gz 6326700 --version 0.0.0 + # archive # multi-version DOI # new version +$ python scripts/deploy.py upload 0.0.0.tar.gz --doi 10.5281/zenodo.6326822 --version 0.0.0 ``` diff --git a/action.yml b/action.yml index e275d34..1a266e7 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,8 @@ inputs: required: true zenodo_json: description: Path to zenodo.json to upload with metadata (must exist) + doi: + descripton: The DOI to create a new version from outputs: badge: @@ -54,5 +56,16 @@ runs: version: ${{ inputs.version }} ACTION_PATH: ${{ github.action_path }} ZENODO_TOKEN: ${{ inputs.token }} - run: python ${{ github.action_path }}/scripts/deploy.py upload ${archive} --zenodo-json ${zenodo_json} --version ${version} + doi: ${{ inputs.doi }} + run: | + command="python ${{ github.action_path }}/scripts/deploy.py upload ${archive} --version ${version}" + if [[ "${doi}" != "" ]]; then + command="$command --doi ${doi}" + fi + if [[ "${zenodo_json}" != "" ]]; then + command="$command --zenodo-json ${zenodo_json}" + fi + printf "$command\n" + $command + shell: bash diff --git a/img/zenodo_versions.png b/img/zenodo_versions.png new file mode 100644 index 0000000000000000000000000000000000000000..dacef5fe8e4c91f93633c06329e5d47075b4eb66 GIT binary patch literal 18830 zcmd6PbyO5w+$Km$swf=_(%lV$2+~MO4M=x)3j)$eHzM8L4Bg$`DJ|WwcfQ^IWB=Oy z&VJ|Ya^CYI3^Q}@@BZ?6o;yJbauS$mglKSZaF~*x#T4P-;4Q$vS5c9{|EZI-O29Wn zdr?VcRPg12YVZ^MOyKb8tAmoYk%RL$J3}~QD{D(bMtglbLqjWj6KjWKgcczG%7m?qe6XRXiGjV72_QOuO?ZSK!E z*S5K+)HH9MV#uNKqoOA7eniQ+RY-F6#jTJ@2Us{M(XbX>c|DR>RvtI8H{K2Mk6;{A zu;XJOBZ|EEfs)pg&;J(H_bo>#>a6cuR8>*+hltZtJ2Z6k{JcDqf=BQWs!wq-V@XNL z?A)B>fPo6{0eHNkva-Cg@*@)-f;D)RoUECBT4v^+fdOf>dBxUd*E1@%L<}M#qL*bz zA}^8>Af>q#6|el-d}?dC6&NWMzkK-~9gTzL|EyjsT!zZLA{@>4?L9MClOl#{;ncWY zG;xg<6JDFf3&f0|J_EK*J6_K82Cf0mpSp~wzN6<1U3$NYa1lkyUXkmf`@a49{6F5N z=*R!q0iuP-3q<;N&uYNFVqd^(zCdh~`so9nsY@x=`v3ZAVwp$>5kj{=6B6*{(uK75 z`9=hUS%ss$Mo)$iF~b^D?XQ5p?*g0OxePYO z7B9Fkko~%9%-O%SBdV4{!ctS0w5^qZf%7@`6zqE=-)(Mf*=`RKRL+1m3M(w67j(aj zi8C}ZI^9mTX4C6L4ZtGlKi{2@l#`2t2z%Z0u(Gn=U$s0Lj%P_?5c5QO-d(~gKp?|A z>E6S2wj1~YE-V{85ylU<7bevjyAL6_jO0{Q-C1OWVF7RGRau-4zUt}g69{_=By(A| z>#`(RI*+6YdfcD%2;Becz#tP!Az(L(OiVN#hd4QLq2n=aj!BRi&X#?XN#XhRaCfz` z@clKS$f`5yES&r00jqjiT%5R`9ytOEmRP>>Gn=z*St_}d-g^5Tm%aR4m)qT3u+s?BF!k&p~!Nk*e$VHw?=Zh4%KNmO6T z7fzjyct1vC5^;@I8q0%M{TUH~P3C>in(lELG!t+{-S0B>YZ`L+`-iBWZ{#ZOO>U8XR`p!C|>> z#j0g3+N$tn(T!l1*=Zv|#ZVAV&akO)WAJ(w+T zdbnJ$n6C=@))6q);An38fJMSrrrY++RM>%!M6<>sBQFmX4-aoRO;B1&3azx|k*^~F z6Ja$YkW>#m+hVyXQN#~<=}zPg;+nT==89*bREV;-hQUmUHiz|c%D;a$t@t)qhYJ$Q z%CC|AzAr9n+1cBd*VaQr$H5;K0H(rU}@plFP%>bAK^F zG2dXiNSy)Flu)kUGhU+2-{5>aJeu+GIVx)2g{P^hDIyY5BGUQq-)}7}EIvz17uq+f z92fRS2L=YRd)>QCmuL?hEj8{==Ho!!uZ;e!wzVuovw%Ei%jwqYWM@at$Cm_xf)yNy z)8RZ)f)EKvd+!*D1YFA0iE3(UF081jr~-O>KVy*!eAdz;Uh#es)bYBZjHZ%ZKj|T> zy(aZ*)3rUwjqWkkDcS=k&FEqkU^;O!((DzJMg7Sk`a+pQwK&xz4G<*Bj9&p z(C-PS6ckJa5uXM^x=gPtn17iYR0Fz}fv2}Y!NKiIOQ|5L?6&&fL??&o68k&vj{5_X z{7OhLIi0_~*q4-&DyVz_KG7XPHp1L;PtI6bTbpY*5C>95|G)rabE)^!bQ;s7J# zBCLV17?fs)cEY}ca6GxXqGn=}lgh5Ln5&q+bU*(q0;24?<>^j%d}87`8rnKo*!}fJ zgqXPa`q7cqiKl*d=(nD5k|vNOw3=Lgt*+_<>+?ALeE}ZGkWJ=tu1b#(I7Mx_KSiI& zl{Y^BJ66}wV0V9QTcTC37gdReC{jn%BHfCtuVFBeBP%8$v9Y;naOnmz;@5ILq;$_q zf@E&%oQjI5$jBGlBk3cp-x2mLH1m~D+fc~L%%W25P_3UoKQ01=IN`s z`T5heK(Y$(Fi6)!AV#Hhb>D)F^KCGJ)wwAG9v&W6Nk)Y3Lam!`0)m6lo;`c!axtam zdc78STqg)lY0n@)^C_E|lk*LT^ex z8&}ReLeG#NIs{D*cU+!ooZp67eJiOtb8fJP(kbi8hmJe$VOgV*L=nQ%z;a@DYMW-O_x;yuQsF~Fu-$(` zk7NYj*rX^g1tBQ*IMyjcc4=y4rX%X|w8Z)SyDBPO&VbeRD^0i)nxfsn*Glf#-OTnQ zQ|;8{)+n8*=Qe6kik{;mJAkN-iKtRv}d7(_od(;#+Gfo&tfA&S52v*L^C`! z?nIeSw-FjMV==LI4l}Cfd?fSzQW_Et@9I?eH_WpsiI=M1z(M8YYajh=og6!KS#0#4 zlPCe6G;6`YLvk>%*M&C7iZASo4N=Lf89iqsjKT}U!#&n7>)dGU@}$|$`U5VF+pQ9 zFvx}k62dnsYlk+J!CAMox-SRFqMuL=50&AAOShEgBn#w@zK0g-sFn_jEs61R5Q*#84Nr*Xgq> zb9Hx2OpgR5`@g}x60wF;54#VO2{<`jB?WeoYhwyM+8YS{!g#I3>^)5)Ly(K<;4WlURh%IT34s7e$Ya-ILXB^(@eb zbQ)Tw&RSO+=)>PvODkE}U7wJ68tXGBb`|JkdvdYlhGSmiy(FDOh4D() zWsd&^>D)&Ri0Ys2wv$KUDjVt~haZgbB8_m=uM|UqFl@Ivt3?AGIKtCH`sw;;OID*T z)!?&o-=0tX(Ntlo>(A8oRIjG?SFlFmL*||6o_Vf@jxap<3{zxCMhEq*V&@=Cbl2LL ze}c$0K*P5{%jjNjQfo#L>roEuU7~TR#BA-*tY;LJ8B1r_SUGECiIWD^xo>zG9x5VV zpM$|4E11Y#0uHQm^KLm`Ei`7ltXk<^^a?36azM4%mhO#+9}bGs81yNU|7gNrsmEka z2#khuhM)XzbS0TSmU2TIhWb8#q>5hKn+r`^zdT~$eaGQX)uKPe*%|wQcV>dOD(2Q4 zP^KAwj*NillIP-TV&b=ec4DV{n}?gV8TZmV^Jlue!O4B=TP+e9ztXVr`*u-~MWG>s zKl)X_H)u&*yinK5>6#k5A6@QiaPF9yijyYTyBm*tyZF-Yd)y(v?UskY+AqP+e9i%R zkAAd+1F@=KXkt@lYomkh!fekq6PTe?@tV!EEnYXzmc72uuB`QPZFF-=S%;;;zeYoH zgYHJFB17Tp*DtM0m9+=w^aKBL+l8vT8;2iI=cy2-Xe}M(kzHvfmn6-Hv@??r9%8 zCU#PY1(<=7^%3!xr+&7fo&U4cVFxkQ0;V`cVo)N&4`xM2M6lnfB|z$Rpe|ShY`dn> zIg^|4noNF|@J#nMcFF7gp<#w%BBXkm>N?>{}8dQGt2-`437JWvI<*h@@kO!Z zle0O>k-^d}$5q*mM?unkloT9YwVPA=2xns(9cl|+qD(-%4qTw~z0?3!>L7%fp0RCi z=sHH(RzwJimwVr`@+q%pms<~mm4cMgs58^$_IASgnxZs+*%%0D_IY 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 literal 0 HcmV?d00001 diff --git a/scripts/deploy.py b/scripts/deploy.py index d83a180..1eb5894 100644 --- a/scripts/deploy.py +++ b/scripts/deploy.py @@ -10,6 +10,7 @@ import os import json import sys +from glob import glob from datetime import datetime import requests @@ -27,81 +28,231 @@ def read_json(filename): ZENODO_TOKEN = os.environ.get("ZENODO_TOKEN") -ZENODO_HOST = "zenodo.org" if not ZENODO_TOKEN: sys.exit("A ZENODO_TOKEN is required to be exported in the environment!") -def upload_archive(archive, zenodo_json, version): +class Zenodo: """ - Upload an archive to zenodo + Zenodo client to handle shared API calls. """ - archive = os.path.abspath(archive) - if not os.path.exists(archive): - sys.exit("Archive %s does not exist." % archive) - headers = {"Accept": "application/json"} - params = {"access_token": ZENODO_TOKEN} + def __init__(self, sandbox=False): + self.headers = {"Accept": "application/json"} + self.params = {"access_token": ZENODO_TOKEN} + self.set_host(sandbox) - # Create an empty upload - response = requests.post( - "https://zenodo.org/api/deposit/depositions", - params=params, - json={}, - headers=headers, - ) - if response.status_code not in [200, 201]: - sys.exit( - "Trouble requesting new upload: %s, %s" - % (response.status_code, response.json()) + def set_host(self, sandbox=False): + """ + Given a preference for sandbox (or not) set the API host + """ + self.host = "zenodo.org" + if sandbox: + self.host = "sandbox.zenodo.org" + + def get(self, url): + """ + Wrapper to get to handle adding host and adding params or headers + """ + if not url.startswith("http"): + url = "https://%s%s" % (self.host, url) + return requests.get(url, params=self.params, headers=self.headers) + + def post(self, url): + """ + Wrapper to post to handle adding host and adding params or headers + """ + if not url.startswith("http"): + url = "https://%s%s" % (self.host, url) + return requests.post(url, params=self.params, headers=self.headers) + + def get_depositions(self): + """ + Get all current depositions. + """ + response = self.get("/api/deposit/depositions") + if response.status_code not in [200, 201]: + sys.exit( + "Cannot query depositions: %s, %s" + % (response.status_code, response.json()) + ) + return response.json() + + def find_deposit(self, doi): + """ + Given a doi, find the deposit, return None if no match + """ + deposits = self.get_depositions() + + # Look for the matching DOI + target_deposit = None + for deposit in deposits: + if "doi" not in deposit: + continue + print("looking at deposit %s" % deposit["doi"]) + if deposit["conceptdoi"] == doi: + print("Found deposit %s! 🎉️" % doi) + target_deposit = deposit + break + + return target_deposit + + def new_doi(self): + """ + Create a new (empty) upload for a DOI + """ + response = requests.post( + "https://zenodo.org/api/deposit/depositions", + params=params, + json={}, + headers=headers, ) + if response.status_code not in [200, 201]: + sys.exit( + "Trouble requesting new upload: %s, %s" + % (response.status_code, response.json()) + ) + return response.json() + + def update_doi(self, doi): + """ + Given an existing DOI, update with a new archives (or pattern of files). + """ + target_deposit = self.find_deposit(doi) + if not target_deposit: + sys.exit( + "Cannot find deposit with doi: '%s'. Are you currently editing it?" + % doi + ) + + # If we have an unpublished draft - continue working onit + if not target_deposit["submitted"]: + draft = target_deposit + else: + # found the existing deposit - so let's make a new version. + response = self.post( + "%s/actions/newversion" % target_deposit["links"]["self"] + ) + if response.status_code not in [200, 201]: + sys.exit( + "Cannot create a new version for doi '%s'. %s" + % (doi, response.json()) + ) + draft = response.json() + + # this is actually the next draft. cannot edit the existing doi above + response = self.get(draft["links"]["latest_draft"]) + if response.status_code not in [200, 201]: + sys.exit("Cannot create a draft for doi '%s'. %s" % (doi, response.json())) + new_version = response.json() + + # this draft is based off of version N-1, so let's remove N-1's artifacts to make room + # for version N. + for file in new_version.get("files", []): + response = requests.delete( + file["links"]["self"], params=self.params, headers=self.headers + ) + if response.status_code not in [200, 204]: + print( + "could not delete file %s: %s" % (file["filename"], response.json()) + ) + return new_version + + def upload_archive(self, upload, archive): + """ + Given an upload response and archive, upload the new file! + """ + # Using requests files indicates multipart/form-data + # Here we are uploading the new release file + url = "https://zenodo.org/api/deposit/depositions/%s/files" % upload["id"] + bucket_url = upload["links"]["bucket"] + + with open(archive, "rb") as fp: + response = requests.put( + "%s/%s" % (bucket_url, os.path.basename(archive)), + data=fp, + params=self.params, + ) + if response.status_code != 200: + sys.exit("Trouble uploading artifact %s to bucket" % archive) + + def publish(self, data): + """ + Given a data response from a new metadata upload, publish it. + """ + publish_url = data["links"]["publish"] + r = requests.post(publish_url, params=self.params) + if r.status_code not in [200, 201, 202]: + sys.exit("Issue publishing record: %s, %s" % (r.status_code, r.json())) + + published = r.json() + print("::group::Record") + print(json.dumps(published, indent=4)) + print("::endgroup::") + for k, v in published["links"].items(): + print("::set-output name=%s::%s" % (k, v)) - upload = response.json() + def upload_metadata(self, upload, zenodo_json, version): + """ + Given an upload response and zenodo json, upload new data - # Using requests files indicates multipart/form-data - # Here we are uploading the new release file - url = "https://zenodo.org/api/deposit/depositions/%s/files" % upload["id"] - bucket_url = upload["links"]["bucket"] + Note that if we don't have a zenodo.json we could use the old one. + """ + metadata = upload["metadata"] - with open(archive, "rb") as fp: + # updates from zenodo.json + if zenodo_json: + metadata.update(read_json(zenodo_json)) + metadata["version"] = version + metadata["publication_date"] = str(datetime.now()) + + # New .zenodo.json may be missing this + if "upload_type" not in metadata: + metadata["upload_type"] = "software" + self.headers.update({"Content-Type": "application/json"}) + + # Make the deposit! + url = "https://zenodo.org/api/deposit/depositions/%s" % upload["id"] response = requests.put( - "%s/%s" % (bucket_url, os.path.basename(archive)), - data=fp, - params=params, + url, + data=json.dumps({"metadata": metadata}), + params=self.params, + headers=self.headers, ) if response.status_code != 200: - sys.exit("Trouble uploading artifact %s to bucket" % archive) + sys.exit( + "Trouble uploading metadata %s, %s" % response.status_code, + response.json(), + ) + return response.json() - # Finally, load .zenodo.json and add version - metadata = read_json(zenodo_json) - metadata["version"] = version - metadata["publication_date"] = str(datetime.now()) - if "upload_type" not in metadata: - metadata["upload_type"] = "software" - url = "https://zenodo.org/api/deposit/depositions/%s" % upload["id"] - headers["Content-Type"] = "application/json" - response = requests.put( - url, data=json.dumps({"metadata": metadata}), params=params, headers=headers - ) - if response.status_code != 200: - sys.exit( - "Trouble uploading metadata %s, %s" % response.status_code, response.json() - ) - data = response.json() - publish_url = data["links"]["publish"] - r = requests.post(publish_url, params=params) - if r.status_code not in [200, 201, 202]: - sys.exit( - "Issue publishing record: %s, %s" % (response.status_code, response.json()) - ) +def upload_archive(archive, version, zenodo_json=None, doi=None, sandbox=False): + """ + Upload an archive to an existing Zenodo "versions DOI" + """ + archive = os.path.abspath(archive) + if not os.path.exists(archive): + sys.exit("Archive %s does not exist." % archive) + + cli = Zenodo(sandbox=sandbox) + + if doi: + upload = cli.update_doi(doi=doi) + else: + if not zenodo_json: + sys.exit("You MUST provided a .zenodo.json template to create a new DOI.") + upload = cli.new_doi() + + # Use a glob matching pattern to upload new files (also ensures exist) + for path in glob(archive): + cli.upload_archive(upload, path) - published = r.json() - print("::group::Record") - print(json.dumps(published, indent=4)) - print("::endgroup::") - for k, v in published["links"].items(): - print("::set-output name=%s::%s" % (k, v)) + # Finally, load .zenodo.json and add version + data = cli.upload_metadata(upload, zenodo_json, version) + + # Finally, publish + cli.publish(data) def get_parser(): @@ -118,9 +269,9 @@ def get_parser(): "--zenodo-json", dest="zenodo_json", help="path to .zenodo.json (defaults to .zenodo.json)", - default=".zenodo.json", ) upload.add_argument("--version", help="version to upload") + upload.add_argument("--doi", help="an existing DOI to add a new version to") return parser @@ -136,18 +287,24 @@ def help(return_code=0): if not args.command: help() - if not args.zenodo_json or not os.path.exists(args.zenodo_json): - sys.exit( - "You must provide an existing .zenodo.json as the first positional argument." - ) + if args.zenodo_json and not os.path.exists(args.zenodo_json): + sys.exit("%s does not exist." % args.zenodo_json) if not args.archive: sys.exit("You must provide an archive as the second positional argument.") if not args.version: sys.exit("You must provide a software version to upload.") - # Prepare drafts if args.command == "upload": - upload_archive(args.archive, args.zenodo_json, args.version) + upload_archive( + archive=args.archive, + zenodo_json=args.zenodo_json, + version=args.version, + doi=args.doi, + ) + + # We should not get here :) + else: + sys.exit("Unrecognized command %s" % args.command) if __name__ == "__main__":